ADR-013: PBKDF2-HMAC-SHA256 ≥600k итераций (цель 1M), параметры KDF версионируются рядом с блобом; пароль от 12 символов, длина важнее состава. ADR-014: парольная фраза экспорта отменена, ключ экспорта — HKDF от 32-байтового секрета аккаунта из ключевого блоба; архив .bare криптографически бесполезен вне аккаунта-владельца. Перекрёстно обновлены ADR-006, ADR-010, architecture, threat-model; в открытые вопросы добавлен серверный «перец» для блоба. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW4ZXtKn5NjX8NH3Lhm2ND
60 lines
7.2 KiB
Markdown
60 lines
7.2 KiB
Markdown
# Архитектура
|
|
|
|
Bare — это PWA-клиент на ванильных веб-технологиях и Go-сервер одним бинарём. Между ними — голый HTTPS: приём сообщений через SSE, отправка обычным `fetch POST`. Чат поверх голого HTTP.
|
|
|
|
## Стек
|
|
|
|
Клиент: HTML + CSS + vanilla JS, ES-модули без сборки, один service worker, manifest.json. PWA. Ноль npm-зависимостей.
|
|
|
|
Сервер: Go, стандартная библиотека плюс ровно три внешних пакета — webpush-go, драйвер SQLite, argon2. База — SQLite. HTTPS обязателен: без него не работают service worker и пуши.
|
|
|
|
## Аккаунты
|
|
|
|
Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и интеграций. Восстановления пароля нет.
|
|
|
|
Серверная аутентификация: Argon2id, сессия в httpOnly cookie.
|
|
|
|
Пароль — не короче 12 символов; правил про регистры и спецсимволы нет: длина важнее состава. UI рекомендует парольную фразу из нескольких слов и при регистрации прямо говорит: пароль — это ключ шифрования, а не запись в базе; восстановления нет.
|
|
|
|
## E2EE
|
|
|
|
Вся клиентская криптография — WebCrypto, без крипто-библиотек.
|
|
|
|
Идентичность пользователя — ECDH-пара (P-256). Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2-HMAC-SHA256, не менее 600 000 итераций, целевое значение — 1 000 000), и хранится на сервере как блоб — сервер видит только шифротекст. Параметры KDF лежат рядом с блобом и читаются клиентом при входе: их можно повышать без миграции всех аккаунтов разом. Рядом с приватным ключом в блобе живёт случайный 32-байтовый секрет аккаунта — из него выводятся ключи экспорта истории. Отсюда два следствия. Сброс пароля невозможен by design. Мультидевайс-вход прост: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ.
|
|
|
|
Чаты 1:1: ECDH shared secret → AES-GCM.
|
|
|
|
Комнаты: у комнаты симметричный ключ, он раздаётся участникам зашифрованным на их публичные ключи. При изменении состава — rekey. Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников.
|
|
|
|
Forward secrecy — осознанный non-goal v1.
|
|
|
|
## Хранение
|
|
|
|
Сервер хранит только три вещи: аккаунты (ник, argon2-хеш, зашифрованный ключевой блоб), метаданные комнат и контактов, транзитную очередь зашифрованных недоставленных сообщений. Очередь per-device: доставлено и подтверждено ACK — удалено с сервера; не забрано за 30 дней — удалено.
|
|
|
|
Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM.
|
|
|
|
При старте клиент запрашивает `navigator.storage.persist()` и показывает занятое место через `storage.estimate()`.
|
|
|
|
## Экспорт и импорт истории
|
|
|
|
Экспорт: вся локальная история сериализуется, шифруется и сохраняется одним файлом `.bare`. Ключ экспорта выводится из секрета аккаунта: HKDF(секрет, случайный salt, info="bare-export-v1") → AES-GCM, всё на WebCrypto. Отдельной парольной фразы нет: архив криптографически привязан к аккаунту и вне его бесполезен — у чужого клиента нет секрета аккаунта, расшифровка невозможна в принципе.
|
|
|
|
Заголовок файла открытый: magic, версия формата, salt, отпечаток публичного ключа владельца. Ника в заголовке нет — лишняя утечка. Импорт: клиент сверяет отпечаток со своим (при несовпадении — «архив создан другим аккаунтом», сверка — UX-вежливость, не защита) и делает идемпотентный merge в IndexedDB по messageId — повторный импорт и склейка истории с двух устройств не создают дублей.
|
|
|
|
Это единственный механизм переноса истории между устройствами. Осознанно.
|
|
|
|
## Пуши
|
|
|
|
Web Push + VAPID. Одна пара ключей, никаких регистраций и оплат у вендоров, никакого Firebase SDK.
|
|
|
|
Пуш — сигнал, не транспорт: содержимое всегда догоняется через очередь при открытии. Текст пуша generic («имя: новое сообщение») — сервер не знает плейнтекста. Declarative Web Push не используем: несовместим с E2EE.
|
|
|
|
iOS: пуши работают только у PWA, установленного на экран «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение на уведомления запрашивается после осмысленного действия (первое отправленное сообщение), не при входе.
|
|
|
|
Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки.
|
|
|
|
## Scope v1
|
|
|
|
Чаты 1:1 и комнаты. Только текст и эмодзи (эмодзи — юникод, отдельной фичи нет). Экспорт/импорт истории. Пуши на всех платформах. Всё остальное — за пределами v1.
|