Три месяца пилю свой менеджер паролей, потому что задолбался с info.txt в каждой папке проекта
Я веб-разработчик. Годами у меня повторялся один и тот же сценарий: в папке очередного проекта лежал файл info.txt. Доступ к базе, ключ от хостинга, API-токен, пароль от админки заказчика, всё вперемешку, зато по папкам и всегда под рукой.
Проблемы начинались, когда доступ надо было кому-то передать. Найти актуальную версию пароля, не перепутать со старой, отправить нужному человеку и не оставить секрет навсегда болтаться в переписке. Каждый раз небольшая археологическая экспедиция по собственным старым сообщениям, и заканчивалась она обычно тем, что пароль всё-таки оставался в чате.
Плюс к файлу секреты расползались по SSH-конфигам, .env, настройкам IDE, автозаполнению браузера. После смены пароля надо было вспомнить, где ещё лежит старое значение, и я почти всегда что-нибудь пропускал.
Готовых менеджеров хватает, но я занимаюсь ещё и безопасностью, поэтому вопросы к ним у меня возникали не самые удобные. Может ли сотрудник сервиса прочитать мой сейф? Что именно сервер видит во время обычной работы? Что останется у атакующего, если он утащит базу вместе с бэкапами? И насколько сильно я вообще завишу от чужого сервиса, если в нём лежат ключи от инфраструктуры.
В итоге сел писать свой, с одним жёстким требованием к себе: сервер не должен физически иметь возможность прочитать содержимое сейфа. Не «мы обещаем не читать», а «мы технически не можем». Шифрование и расшифровка происходят только на устройстве, мастер-пароль на сервер не уходит вообще никогда.
Отсюда сразу вылезает неочевидная задача. Войти можно тремя способами: мастер-паролем, по passkey (биометрия или физический ключ) и одноразовым кодом восстановления. Все три должны в итоге открыть один и тот же ключ хранилища, но ни один из них не должен дать этот ключ серверу. Решается это тем, что ключ лежит на сервере в трёх разных зашифрованных обёртках, и каждый способ входа умеет развернуть свою.
За три месяца получилось следующее: личные и командные сейфы, вход по паролю или по passkey, гостевые ссылки для разовой передачи доступа, экстренный доступ для доверенного человека. Архитектуру переписывал трижды, причём не ради красоты кода, а потому что каждый раз появлялось требование, которое старая модель просто не могла выразить.
По дороге нашёл у себя восемь неприятных багов. И вот что интересно: ни один из них не был ошибкой в криптографии. Argon2id, AES-GCM, X25519 это готовые библиотеки, они работали ровно так, как написано в документации. Ломалось всё, что я писал сам вокруг них.
Самый обидный баг: при сериализации публичного ключа терялся один-единственный нулевой байт. Координата должна занимать ровно 32 байта, а функция возвращала её как число, то есть без ведущих нулей, и иногда получался 31 байт. Итог: примерно у одного процента пользователей passkey успешно регистрировался, а войти по нему было нельзя никогда. Сначала я неделю грешил на кривые браузеры и конкретные модели ключей.
Ещё был сценарий, где название организации попадало в тему письма-приглашения, и через перевод строки в этом названии можно было дописать в письмо скрытую копию на чужой адрес. Поле проверялось, но только на длину, а очистку я мысленно доверил форме. Классическая ошибка: валидация формы решает задачу формы, а проверять надо там, где собирается заголовок письма.
И был механизм экстренного доступа, который был написан, покрыт тестами, тесты зелёные, а на проде не работал вообще никак. По трём независимым причинам сразу, каждая из которых давала один и тот же симптом «ничего не происходит». Три захода на отладку, и каждый заканчивался ощущением «ну вот теперь точно починил».
Отдельно скажу то, о чём производители менеджеров паролей обычно молчат. Клиентское шифрование защищает данные от кражи базы. Оно не защищает от заражённого устройства, не защищает от XSS в самом приложении, и не защищает от ситуации, когда владелец сервиса подменит вам JavaScript в браузере. Последнее вообще нерешаемо в вебе, и любой, кто говорит обратное, либо не разобрался, либо продаёт.
Если зайдёт, распишу отдельным постом, как искал эти восемь багов и что из этого следует про разработку в одиночку, без ревьюера за спиной.


