The Daily Newsstand · Free, Always
Sunday, October 18, 2026

Как я убрал из своего менеджера паролей ошибку «неверный пароль»

Translate

Несколько лет назад я написал себе маленький менеджер паролей: Electron, React и какое-то симметричное шифрование из npm: ключ из пароля, им же зашифровал, им же расшифровал, что именно, уже не вспомню. Я бэкендер на Java, на фронте мне было интересно поковыряться. Пользовался им сам, потом забросил. В этом году вернулся, перевёл его на Tauri и Rust и заодно посмотрел свежим взглядом на то, как он хранит данные. Было стыдно. Но в процессе обнаружилось, что одна странная идея из первой версии на самом деле стоит того, чтобы сделать её нормально. Про неё и расскажу.

Странная идея

В первой версии не было экрана «неверный пароль». Вводишь пароль: если под ним есть хранилище, оно открывается, если нет — создаётся новое пустое. Мне просто показалось, что это прикольная идея; ни у кого я её не подсматривал.

Смысл в том, что если любой пароль что-то открывает, то со стороны нельзя понять, «настоящий» он или нет. Попросили открыть менеджер паролей — открываешь хранилище с парой безобидных записей. Есть ли там что-то ещё, проверить нечем. Это то же правдоподобное отрицание, что у скрытых томов VeraCrypt, только на обычных файлах в папке.

Звучит красиво, но в первой версии это не работало вообще.

Почему не работало

Имя файла хранилища было bcrypt-хэшем пароля. Мне хотелось по паролю сразу находить нужный файл, без перебора. А на деле это готовый оракул: берёшь словарь, считаешь bcrypt, сравниваешь с именами файлов в папке. Само шифрование можно даже не трогать.

Рядом лежали папки history/<имя>/ и sync/<имя>.json. У какого файла есть история, тот и живой. Сколько папок, столько и настоящих хранилищ. Ну и даты изменения: приманку никто не трогает, а настоящее хранилище меняется каждый день.

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

Что сделал теперь

Ключ получается из пароля через Argon2id (64 MiB, t=3, p=1). Соль одна на установку и лежит отдельно. Своя соль у каждого файла означала бы прогон Argon2 на каждый файл при входе, а это секунды.

Сам файл — 24 байта nonce и дальше шифротекст XChaCha20-Poly1305. Ни заголовка, ни магических байтов, ни версии. Версия формата, длина данных и JSON с записями лежат внутри шифротекста, а за ними нули до ближайшего размера-«корзины» (4 KiB, 8 KiB, 16 KiB…). Имя файла — просто 32 случайных hex-символа.

Раз сравнивать пароль не с чем, вход — это пробная расшифровка. Один раз считаем Argon2, потом пробуем ключ на каждом файле. AEAD-тег либо сходится, либо нет. «Не сошлось» выглядит одинаково и для чужого хранилища, и для мусора. Argon2 тут самое долгое, проверка тега на файлах в десятки килобайт почти ничего не стоит.

Вход: пробная расшифровка

История и состояние синхронизации стали такими же файлами в той же папке, с полем kind внутри шифротекста. Снаружи это просто N одинаковых файлов со случайными именами.

Папка раньше и сейчас

Метаданные оказались сложнее криптографии

Новое шифрование на Rust я сделал за день, но честно скажу: с ИИ-ассистентом, Rust для меня новый язык. Argon2id и XChaCha20-Poly1305 есть готовые в RustCrypto, так что сама криптография тут несложная. А вот с тем, что видно в проводнике, пришлось повозиться.

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

Размер прячут корзины. Хранилище с тремя записями и с тридцатью весят одинаково. С тремя тысячами, конечно, уже нет.

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

Какой из файлов настоящий?

Сколько это стоит

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

Где это не помогает

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

Если есть две копии диска, сделанные в разное время, видно, какой файл поменялся: байты приманок не меняются никогда. Туда же журнал NTFS (USN), теневые копии Windows, Time Machine — это всё те же «две копии». Пока хранилище открыто, ключ в памяти и может попасть в своп или файл гибернации. Rust затирает его при блокировке, но интерфейс на webview, и введённый пароль какое-то время живёт строкой в JavaScript.

С синхронизацией своя история. Сервер видит только шифротекст под непрозрачным id, но видит, сколько у аккаунта слотов. Если синхронизировать и приманку, и настоящее хранилище в одном аккаунте, хозяин сервера увидит два слота.

И главное. Если тот, кто спрашивает, знает, что программа так умеет, он может просто не поверить первому паролю. Тут криптография не поможет, только то, что лишние файлы есть у всех.

Что дальше

Думаю про две вещи. Первая — при каждом сохранении переписывать и приманки (новый nonce, те же случайные байты), чтобы между двумя снимками менялся не один файл, а все. С чужими настоящими хранилищами так не выйдет, ключа нет. Вторая — один размер для всех файлов вместо корзин, ценой места на диске.

Если видите, где ещё может торчать оракул, или считаете, что одинаковые даты — самообман, напишите в комментариях. Ради этого и пишу.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.