diff --git a/docs/architecture.md b/docs/architecture.md index 9c311b4..0b068a8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -14,11 +14,13 @@ Bare — это PWA-клиент на ванильных веб-технолог Серверная аутентификация: Argon2id, сессия в httpOnly cookie. +Пароль — не короче 12 символов; правил про регистры и спецсимволы нет: длина важнее состава. UI рекомендует парольную фразу из нескольких слов и при регистрации прямо говорит: пароль — это ключ шифрования, а не запись в базе; восстановления нет. + ## E2EE Вся клиентская криптография — WebCrypto, без крипто-библиотек. -Идентичность пользователя — ECDH-пара (P-256). Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2), и хранится на сервере как блоб — сервер видит только шифротекст. Отсюда два следствия. Сброс пароля невозможен by design. Мультидевайс-вход прост: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ. +Идентичность пользователя — ECDH-пара (P-256). Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2-HMAC-SHA256, не менее 600 000 итераций, целевое значение — 1 000 000), и хранится на сервере как блоб — сервер видит только шифротекст. Параметры KDF лежат рядом с блобом и читаются клиентом при входе: их можно повышать без миграции всех аккаунтов разом. Рядом с приватным ключом в блобе живёт случайный 32-байтовый секрет аккаунта — из него выводятся ключи экспорта истории. Отсюда два следствия. Сброс пароля невозможен by design. Мультидевайс-вход прост: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ. Чаты 1:1: ECDH shared secret → AES-GCM. @@ -36,7 +38,9 @@ Forward secrecy — осознанный non-goal v1. ## Экспорт и импорт истории -Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, merge в IndexedDB по messageId, идемпотентно — повторный импорт и склейка истории с двух устройств не создают дублей. +Экспорт: вся локальная история сериализуется, шифруется и сохраняется одним файлом `.bare`. Ключ экспорта выводится из секрета аккаунта: HKDF(секрет, случайный salt, info="bare-export-v1") → AES-GCM, всё на WebCrypto. Отдельной парольной фразы нет: архив криптографически привязан к аккаунту и вне его бесполезен — у чужого клиента нет секрета аккаунта, расшифровка невозможна в принципе. + +Заголовок файла открытый: magic, версия формата, salt, отпечаток публичного ключа владельца. Ника в заголовке нет — лишняя утечка. Импорт: клиент сверяет отпечаток со своим (при несовпадении — «архив создан другим аккаунтом», сверка — UX-вежливость, не защита) и делает идемпотентный merge в IndexedDB по messageId — повторный импорт и склейка истории с двух устройств не создают дублей. Это единственный механизм переноса истории между устройствами. Осознанно. diff --git a/docs/decisions/006-e2ee-webcrypto.md b/docs/decisions/006-e2ee-webcrypto.md index 3e76daa..9b005e2 100644 --- a/docs/decisions/006-e2ee-webcrypto.md +++ b/docs/decisions/006-e2ee-webcrypto.md @@ -6,11 +6,11 @@ ## Решение -Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto), и хранится на сервере как блоб. Чаты 1:1: ECDH shared secret → AES-GCM. +Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto; параметры и парольная политика — ADR-013), и хранится на сервере как блоб. Рядом с приватным ключом в блобе — секрет аккаунта для экспорта истории (ADR-014). Чаты 1:1: ECDH shared secret → AES-GCM. ## Следствия - Сервер видит только шифротекст — и блоба, и сообщений. - Сброс пароля невозможен by design: пароль — материал ключа. Забыл — значит забыл. - Мультидевайс-вход: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ. -- Стойкость блоба к оффлайн-перебору равна стойкости пароля. +- Стойкость блоба к оффлайн-перебору равна стойкости пароля. Контрмеры — ADR-013. diff --git a/docs/decisions/010-export-import.md b/docs/decisions/010-export-import.md index 284bc78..3eb9c94 100644 --- a/docs/decisions/010-export-import.md +++ b/docs/decisions/010-export-import.md @@ -1,5 +1,7 @@ # ADR-010: Перенос истории — только ручной экспорт/импорт +Частично заменён [ADR-014](014-export-account-binding.md): парольная фраза экспорта отменена, ключ экспорта выводится из секрета аккаунта. Принцип «перенос — только ручной экспорт/импорт» в силе. + ## Контекст История живёт на устройстве (ADR-009), но людям нужны перенос на новое устройство и склейка истории с нескольких. Облачная синхронизация сделала бы сервер архивом — non-goal. diff --git a/docs/decisions/013-password-policy-kdf.md b/docs/decisions/013-password-policy-kdf.md new file mode 100644 index 0000000..79a340c --- /dev/null +++ b/docs/decisions/013-password-policy-kdf.md @@ -0,0 +1,18 @@ +# ADR-013: Парольная политика и параметры KDF + +## Контекст + +Ключевой блоб лежит на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля (ADR-006). Раз пароль — материал ключа, его качество и цена перебора — не UX-детали, а параметры криптосистемы. + +## Решение + +- KDF: PBKDF2-HMAC-SHA256, не менее 600 000 итераций, целевое значение — 1 000 000. +- Число итераций (поле версии KDF) хранится рядом с блобом; при входе клиент читает параметры из блоба. Так итерации можно повышать в будущем без миграции всех аккаунтов разом. +- Минимальная длина пароля — 12 символов. Правил про регистры и спецсимволы нет: длина важнее состава. UI рекомендует парольную фразу из нескольких слов. +- UI при регистрации явно проговаривает: пароль — это ключ шифрования, а не запись в базе; восстановления нет. + +## Следствия + +- Гарантия «оператор не читает сообщения» действует в пределах стойкости пароля пользователя; слабый пароль — слабое E2EE. Это осознанная цена парольного мультидевайса, зафиксировано в threat-model. +- Перебор дорожает на порядки, но не отменяется: словарный пароль умирает при любых итерациях. +- Параметры повышаются со временем: новые аккаунты получают их сразу, существующие — перешифровкой блоба при очередном входе. diff --git a/docs/decisions/014-export-account-binding.md b/docs/decisions/014-export-account-binding.md new file mode 100644 index 0000000..bf21290 --- /dev/null +++ b/docs/decisions/014-export-account-binding.md @@ -0,0 +1,23 @@ +# ADR-014: Экспорт привязан к аккаунту криптографически + +Заменяет часть [ADR-010](010-export-import.md): парольной фразы экспорта больше нет. Принцип «перенос истории — только ручной экспорт/импорт» в силе. + +## Контекст + +Экспортный файл, зашифрованный отдельной парольной фразой, переносим кем угодно: файл плюс фраза — и чужой клиент импортирует чужую историю. Требование: архив должен быть бесполезен вне аккаунта-владельца. Проверка на уровне «клиент сверяет ник» неприемлема — клиент открытый, такая проверка тривиально выпиливается. Запрет должен быть криптографическим. + +## Решение + +- В ключевом блобе аккаунта, рядом с приватным ECDH-ключом, хранится случайный 32-байтовый секрет аккаунта. Он генерируется при регистрации, доступен любому устройству, вошедшему в аккаунт, и никогда не существует в открытом виде вне клиента. +- Ключ конкретного экспорта: HKDF(секрет аккаунта, salt, info="bare-export-v1"), salt случайный на каждый экспорт. Шифрование — AES-GCM. Всё на WebCrypto: HKDF там есть нативно. +- Отдельной парольной фразы для экспорта больше нет — сущность удалена. +- Формат `.bare`: заголовок открытым текстом — magic, версия формата, salt, отпечаток публичного ключа владельца; далее шифротекст. Ник владельца в заголовок не пишется — лишняя утечка. +- Импорт: клиент сверяет отпечаток со своим; при несовпадении показывает «архив создан другим аккаунтом» и не пытается расшифровать. Сверка — только UX-вежливость: реальная защита в том, что у чужого клиента нет секрета аккаунта и расшифровка невозможна в принципе. +- Импорт по-прежнему идемпотентный merge по messageId. + +## Следствия + +- Архив открывается только устройствами, вошедшими в аккаунт-владелец. Потеря пароля — потеря ключей — архив нечитаем навсегда. Это последовательное продолжение принципа «забыл — значит забыл», а не новое ограничение. +- Смена ключевой пары (например, перерегистрация под тем же ником) делает старые архивы нечитаемыми: секрет аккаунта другой. +- Привязка защищает файл, а не содержимое от самого владельца: скриншоты и модифицированный клиент никуда не деваются, а у собеседников есть собственные копии тех же сообщений. «Аналоговая дыра» криптографией не закрывается — зафиксировано в threat-model. +- Побочный плюс: исключено загрязнение чужого клиента историей чатов, в которых пользователь не участник. diff --git a/docs/open-questions.md b/docs/open-questions.md index 558f8fd..50c8ad5 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -7,5 +7,6 @@ - Лимиты: длина сообщения, rate limiting, антиспам. - Смена пароля (= перешифровка ключевого блоба): в v1 или позже? - Идентификация устройства для per-device очередей. +- Серверный «перец» для ключевого блоба: дополнительное шифрование блоба серверным ключом, хранящимся вне базы. Плюс: дамп базы сам по себе перестаёт быть материалом для оффлайн-перебора. Минус: не защищает от оператора; потеря серверного ключа — невозможность входа с новых устройств для всех. Решение отложено. - Язык интерфейса (ru/en); нужна ли i18n. - Визуальная айдентика: отдельный бриф будет добавлен в `docs/identity/`. diff --git a/docs/threat-model.md b/docs/threat-model.md index c75b0c4..09bd0b5 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -24,10 +24,12 @@ **Компрометация устройства.** История лежит на устройстве в открытом виде (IndexedDB). Доступ к устройству — доступ к истории. Защита устройства — зона ответственности пользователя и ОС. -**Слабый пароль.** Пароль — материал ключа. Ключевой блоб хранится на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля. Слабый пароль ослабляет и вход, и шифрование. +**Слабый пароль.** Пароль — материал ключа. Ключевой блоб хранится на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля. Гарантия «оператор не читает сообщения» действует в пределах стойкости пароля пользователя: слабый пароль — слабое E2EE. Это осознанная цена парольного мультидевайса. Контрмеры (ADR-013): PBKDF2-HMAC-SHA256 с не менее чем 600 000 итераций, пароль от 12 символов, рекомендация парольной фразы в UI. **Собеседник.** E2EE не защищает от участника чата: получатель может сохранить, переслать, сфотографировать. «Удалить у всех» после доставки не существует. +**Сам владелец истории.** Привязка экспорта к аккаунту (ADR-014) защищает файл, а не содержимое от владельца. Пользователь, желающий отдать переписку третьему лицу, сделает это скриншотами или модифицированным клиентом — «аналоговая дыра» криптографией не закрывается. К тому же у собеседников есть собственные копии тех же сообщений. Смысл привязки: перемещённый — случайно или злонамеренно — файл `.bare` бесполезен для посторонних. + ## Осознанные пределы v1 **Forward secrecy отсутствует.** Компрометация приватного ключа пользователя раскрывает ранее записанные атакующим шифротексты его чатов 1:1. Осознанный non-goal v1.