Несколько лет назад я написал себе маленький менеджер паролей: 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, те же случайные байты), чтобы между двумя снимками менялся не один файл, а все. С чужими настоящими хранилищами так не выйдет, ключа нет. Вторая — один размер для всех файлов вместо корзин, ценой места на диске.

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

Комментарии (7)


  1. BugM
    18.10.2026 12:27

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


    1. forsaken_zyxel Автор
      18.10.2026 12:27

      Изначально хотел просто удобный менеджер паролей под себя. Потом начал его развивать, и понеслось: одна фича, вторая — сейчас почти всеми сам и пользуюсь. Шифрование появилось уже поверх этого, чтобы своим же приложением было не стрёмно пользоваться и ему можно было доверять. А идея с правдоподобным отрицанием пришла в голову случайно — на аналоги я тогда не оглядывался.

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


      1. BugM
        18.10.2026 12:27

        Не пройдет она ничего. Требование паролей означает что уже есть очень серьезное уголовное дело и от чего там пароли должны быть уже известно. Отдать не те это прямое увеличение срока и повышения вероятности посадки лет на 10. Есть что скрывать и сознательно скрывает. Любой суд будет трактовать однозначно.

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


    1. cpud47
      18.10.2026 12:27

      Тут бывает по-разному. Есть всякие верные проверки, там бывает пустая, или другая приманка срабатывает. Хотя обычно в таких случаях всё же не пароли ищут, да.


  1. cpud47
    18.10.2026 12:27

    Затея в общем-то стандартная. Только обычно делают явно созданные приманки, а не создают новые на любой пароль. Это нужно как из соображений ux (чтобы можно было показать красивую ошибку "неверный пароль"), так и из соображений "безопасности" — пустая/дефолтная бд очевидно что является приманкой.

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

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

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

    Есть немного более продвинутые подходы, избегают проблему с размерами, но у них свои проблемы.

    Но в любом случае, главная сложность в создании правдоподобной приманки. Без неё, эта вся схема работает только для неуловимого Джо.


    1. forsaken_zyxel Автор
      18.10.2026 12:27

      Про размер — в статье схема на момент написания, с тех пор поменял. Корзины теперь начинаются с 256 KiB, обычное хранилище из первой почти никогда не вылезает. Плюс при сохранениях иногда добавляются пустышки из случайных байт случайного размера (от 256 KiB до 2 MiB), так что самый большой файл не обязательно настоящий. И у каждого хранилища есть два шумовых файла его размера, которые переписываются вместе с ним — на двух снимках диска меняется не один файл, а три.

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


      1. cpud47
        18.10.2026 12:27

        Если при сохранениях добавляются пустышки, то будет spaceleak, что не очень хорошо.

        Как выше правильно заметили, конкретно базу паролей вряд ли будут требовать. Будут требовать конкретные пароли. Потому смысла конкретно в этом случае нет.