Files
bare/docs/decisions/015-password-never-leaves-client.md
T
mayatnikovandClaude Fable 5 ae55c846aa ADR-015…024 и спецификации v1: криптография, протокол, хранение, UI, деплой, план
Закрыты все открытые вопросы проектирования. Пароль не покидает клиент
(два ключа из мастера), TOFU для публичных ключей, устройства и конверт,
атомарный rekey комнат, регистрация и контакты, схема SQLite и драйвер
без cgo, сессии и CSRF, деплой через nginx+systemd, правила пушей,
айдентика «Скобы» на системном mono. Иконки PWA в web/icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-22 10:53:24 +03:00

3.7 KiB

ADR-015: Пароль не покидает клиент — два ключа из одного мастера

Контекст

ADR-005 и ADR-006 используют один пароль и для серверной аутентификации (Argon2id), и как материал ключа шифрования блоба. Если клиент отправляет пароль на сервер в открытом виде, оператор, логирующий тела запросов, получает материал ключа — и обещание «оператор не читает сообщения» рушится на первом же входе. Кроме того, ADR-013 требует повышать число итераций KDF без миграции всех аккаунтов разом, а смена пароля числится открытым вопросом.

Решение

  • Пароль никогда не отправляется на сервер. Клиент выводит мастер-ключ: master = PBKDF2-HMAC-SHA256(NFC(пароль), salt = SHA-256("bare-v1:" + nick), iterations, 256 бит). Соль детерминированная — известна до входа без запроса к серверу.
  • Из мастера через HKDF-SHA256 выводятся два независимых ключа: authKey = HKDF(master, info="bare-auth-v1") — 32 байта, уходит на сервер как «пароль»; kek = HKDF(master, info="bare-kek-v1") — AES-GCM-256, шифрует ключевой блоб и сервер его не видит.
  • Сервер хранит argon2id(authKey). Argon2id остаётся (ADR-002, ADR-005): он защищает дамп базы от использования authKey как готового пароля для входа.
  • Перед входом клиент спрашивает GET /api/kdf?nick= и получает число итераций. Для несуществующего ника сервер отвечает текущим целевым значением — ответ не раскрывает существование ника.
  • Повышение итераций и смена пароля — одна и та же операция POST /api/password: клиент, имея пароль в памяти, выводит новый authKey, перешифровывает блоб новым kek и отправляет оба вместе со старым authKey для подтверждения. Сервер заменяет хеш и блоб атомарно. Смена пароля входит в v1.
  • Смена пароля по желанию пользователя завершает остальные сессии (logoutOthers: true); автоматическое повышение итераций — нет.

Следствия

  • Пассивный оператор не получает материал ключа ни при регистрации, ни при входе. Модель угроз становится честной.
  • Соль из ника означает, что перерегистрация под тем же ником с тем же паролем даёт тот же мастер. Ключевая пара при этом новая — старые архивы нечитаемы (ADR-014), мастер это не спасает.
  • PBKDF2 выполняется один раз на вход; на слабом телефоне 1 000 000 итераций — до нескольких секунд. UI показывает «вычисляем ключ».
  • Вопрос «смена пароля в v1 или позже» закрыт: в v1.