Дневник разработки · День 5

Аудит нашёл двенадцать дыр в приватности

Всё зелёное.

Сборка проходит, тесты проходят, четырнадцать задач закрыты.

Вчера у меня было четыре вопроса. Третий — чего будет стоить это поправить. Четвёртый — вывезет ли это ИИ.

Зелёная сборка не отвечает ни на один. Она говорит только, что никто не спрашивал.

Поэтому я отдал код другой модели. Не с просьбой найти ошибки — с просьбой сказать, туда ли мы идём.

🔍 Не аудитор, а штурман

Причина была не та, которая красиво звучит.

У меня было сомнение, которое я до сегодняшнего дня вслух не проговаривал. Не «а нет ли тут багов». А — туда ли мы вообще идём.

И была возможность позвать модель сильнее той, которая пишет код. Я позвал Fable.

Не аудитора. Штурмана.

Задача формулировалась не как «посмотри, всё ли хорошо», а как список областей: криптография, работа с буфером, гонки и ошибки, соответствие замороженным решениям. И с требованием на выходе: отчёт и список правок — для той, которая писала.

Одна пишет, вторая читает, правки делает первая. Это разделение труда, а не фокус.

🧨 Первая находка

Она единственная помечена как критическая, и объяснить её можно без единого технического слова.

У вкладки в режиме сейфа есть соль — случайный кусок данных, из которого вместе с паролем получается ключ.

Переводишь вкладку из сейфа обратно в обычную — содержимое честно расшифровывается и ложится открытым текстом. А соль остаётся.

Переводишь ту же вкладку обратно в сейф. Приложение видит соль и делает вывод: сейф тут уже был, значит человек знает пароль, надо проверить. Проверяет единственным способом, который умеет, — пытается расшифровать первый блок.

А первый блок уже открытый текст. Расшифровать его нельзя.

И проверка не проходит. Ни с каким паролем.

Включая правильный.

Двенадцать пунктов. Первый — единственный, помеченный как критический.
Двенадцать пунктов. Первый — единственный, помеченный как критический.

Вкладка запирается навсегда, и ключа от неё не существует в природе. Не «трудно восстановить», не «нужен бэкап» — просто нет.

Сколько человек должен сделать, чтобы это случилось? Два переключения. Туда и обратно.

И вот что я про себя замечу, пока не забыл. Это потеря данных без возврата, и читается она страшно. А моя реакция оказалась заметно спокойнее, чем находка заслуживает.

Причина простая: терять нечего и некому. Пользователь у приложения один, это я, и я в сейф пока ничего не клал.

Починить надо, и починю. Но сегодня это строчка в списке, а не катастрофа. Катастрофой она станет ровно в тот день, когда появится первый человек, который положит туда что-нибудь нужное.

🕳 Остальные одиннадцать

Дальше начинается интересное, и оно не про криптографию.

Криптография как раз оказалась в порядке. Алгоритмы правильные, ключ выводится правильно, у каждой вкладки своя соль, в памяти лежит ключ, а не пароль, для каждого шифрования берётся новый одноразовый номер. Этот раздел аудита называется «положительное», и он самый короткий.

Дыры были не в замке. Они были в дверных проёмах.

Пароль создаётся без подтверждения. Экран всегда говорит «введите пароль, чтобы разблокировать» — и когда сейф уже есть, и когда его ещё нет. Значит при первой настройке всё, что человек напечатал, молча становится постоянным паролем. Включая опечатку. Восстановить его нельзя — это было моё же замороженное решение.

Пустой сейф пускает кого угодно. Соль есть, блоков ещё нет, проверять нечего — заходит любой пароль, и он же становится настоящим.

Ошибка шифрования стирала данные. Не смогли зашифровать при сохранении — записали пустое значение. То есть неудачная правка сниппета молча удаляла сниппет.

Запертую вкладку можно открыть, не открывая. Вкладка под отпечатком пальца заперта, содержимое скрыто. Но меню настроек этой вкладки рисуется всё равно, и сменить ей уровень приватности можно не прикладывая палец. Меняешь на обычный — содержимое на экране. Замок никто не ломал, его обошли сбоку.

Общего у всех четырёх ровно одно. Ни одна не про математику. Все четыре про то, что бывает по краям: когда только начал, когда уже пусто, когда что-то пошло не так, когда заходят не с той стороны.

Приговор аудита говорит это одной строчкой: ни одна из находок не в самой криптографии, все — в переходах между состояниями, которых не покрывает ни один тест.

Вот почему сборка была зелёная. Замок можно сделать идеальным и всё равно оставить дверь открытой, если не подумать, как через неё ходят.

🎛 Переключатель, который никуда не вёл

И ещё одна, отдельно, потому что она из вчерашнего дня.

Вчера я с удовольствием рассказывал про тумблер, которым можно выключить стекло. Он был.

Он писал значение в настройки, и это значение не читал никто: во всех девяти местах, где рисуется поверхность, режим был прописан намертво.

Переключатель переключался. Больше ничего не происходило.

Вчера и сегодня одна и та же болезнь: функция есть в интерфейсе и больше нигде. Вчера сейф без шифрования, сегодня тумблер в никуда.

Вчера я записал это как единственное, чего в плане не было.

Беру слова назад. Это не случай.

Не плохой код — код как раз неплохой. Бояться при такой скорости надо одного: убедительной демонстрации того, чего нет.

📏 День миллиметров

Потом был день, в котором не случилось ничего, кроме мелочей.

Косметику аудит не смотрел вообще: в его же рамках записано, что внешний вид остаётся за мной. Так что эти находки — мои.

Плейсхолдер в поле ввода рисовался на восемь пунктов ниже, чем появлялся текст, который в это поле печатают. Восемь пунктов.

Заметить это на глаз нельзя, можно только поймать на попиксельном сравнении двух снимков: пустое поле и то же поле с буквой. Буква прыгает.

Системная палитра цветов игнорировала размер, который ей задали, и наезжала на соседние кружки. И она же писала в историю недавних цветов каждый промежуточный оттенок, пока тянешь ползунок, — история забивалась десятком почти одинаковых серых.

Это не героическая работа. Но между «двенадцать дыр в приватности» и «плейсхолдер на восемь пунктов ниже» разница только в цене ошибки, а не в том, насколько внимательно надо было смотреть.

🔌 Engineer honesty moment

Три вещи.

Первая: после того как я закрыл все двенадцать пунктов, я попросил проверить ещё раз. Второй проход нашёл ещё семь.

Разного происхождения. Одна появилась вместе с моими же исправлениями — после настройки сейфа поиск переставал видеть вкладку, пока её не тронешь ещё раз. Одну первый проход нашёл, но я отложил её сам, как несрочную. Остальные лежали там с самого начала и просто не попали в первый список.

Так это и работает. Закрыть список — не то же самое, что закончить, и второй читатель того же кода находит не остатки первого, а другое.

Вторая: в проекте были два UI-теста. Заглушки из шаблона Xcode, которых никто никогда не открывал: они ничего не проверяли, запускали приложение и не могли его потом закрыть, роняя весь прогон целиком.

Я не стал их чинить. Я их удалил.

Мёртвый тест хуже отсутствующего: он создаёт ощущение покрытия там, где его нет. Ровно та же болезнь, что и тумблер, только с другой стороны.

И вот отсюда начинается то, в чём признаваться неприятнее.

Получив список, я начал рубить. Мёртвый тест — удалить. Мёртвый код — удалить. Не читается — переписать. Каждое отдельное решение выглядело целесообразным, и я до сих пор не могу ткнуть пальцем в то, которое было неправильным.

А в сумме проект к вечеру выглядит куда более раскуроченным, чем выглядел утром.

Третья, и она стоила мне отдельного часа.

Я добавил в модель новое поле и выставил ему значение по умолчанию — в конструкторе, как сделал бы любой нормальный человек. Приложение упало при запуске. Не на новом хранилище — на уже существующем, с реальными данными.

Оказалось, что значение по умолчанию должно стоять прямо в объявлении поля, а не в конструкторе. Иначе при миграции существующего хранилища его просто нет, и база отказывается открываться.

Час на строчку. Зато теперь это записано.

💬 Вопрос

Кто читает ваш код, кроме вас?

Я до этого проекта был уверен, что ответ «ревьюер в пулл-реквесте», и что в одиночку эта роль просто не существует. Оказалось, существует — просто её надо кому-то назначить, и лучше не тому, кто писал.

А вчерашний четвёртый вопрос сегодня получил ответ с условием. Вывезет. Если не давать ему проверять себя самого.

Расскажите, как у вас 👇

Дальше по списку то, на что я не смотрел вообще ни разу.

Оно и видно 😉