Заложить репозиторий: философия, архитектура, модель угроз, ADR
Документы фиксируют принятые проектные решения Bare: клиент без фреймворков и сборки, Go-сервер одним бинарём, E2EE на WebCrypto, сервер-реле без истории, локальная история с экспортом/импортом, Web Push + VAPID. Кода нет — сначала документы. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW4ZXtKn5NjX8NH3Lhm2ND
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# ADR-001: Клиент на ванильных веб-технологиях, без сборки
|
||||
|
||||
## Контекст
|
||||
|
||||
Bare обещает аудируемость: любой должен иметь возможность проверить глазами, что делает код, который исполняет его браузер. Фреймворки, npm-зависимости и сборка превращают исходники в артефакт, который никто не читает.
|
||||
|
||||
## Решение
|
||||
|
||||
Клиент — HTML, CSS и vanilla JS на нативных браузерных API. ES-модули отдаются как есть, без транспиляции и бандлинга. Ноль npm-зависимостей. Один service worker и manifest.json — полноценный PWA.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Код в продакшене байт в байт совпадает с кодом в репозитории.
|
||||
- Нет supply-chain-рисков npm и нет тулчейна, который надо поддерживать.
|
||||
- Состояние, рендеринг и роутинг пишем руками — отказ от удобств фреймворков осознанный.
|
||||
- Поддерживаем только современные браузеры с ES-модулями и WebCrypto.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-002: Сервер на Go, один бинарь
|
||||
|
||||
## Контекст
|
||||
|
||||
Серверу Bare нужно немного: HTTP, SSE, SQLite, хеширование паролей, Web Push. Простота развёртывания и аудита важнее богатства экосистемы.
|
||||
|
||||
## Решение
|
||||
|
||||
Сервер пишется на Go со стандартной библиотекой. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Результат сборки — один бинарь.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Развёртывание: скопировать бинарь, запустить. Без рантаймов и обязательных контейнеров.
|
||||
- Любая новая зависимость требует обсуждения и нового ADR.
|
||||
- Часть вещей (роутинг, миграции) пишем руками поверх stdlib.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-003: SQLite как единственная база
|
||||
|
||||
## Контекст
|
||||
|
||||
Сервер хранит мало: аккаунты, метаданные комнат и контактов, транзитную очередь. Отдельный сервер БД — лишняя движущаяся часть, противоречащая идее одного бинаря.
|
||||
|
||||
## Решение
|
||||
|
||||
Вся серверная persistence — SQLite, один файл рядом с бинарём.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Бэкап — копия одного файла.
|
||||
- Нет сетевой БД — нет её настройки, аутентификации и отказов.
|
||||
- Вертикальный потолок масштабирования принимаем осознанно: Bare — маленький инструмент, не платформа.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-004: Realtime через SSE и fetch POST
|
||||
|
||||
## Контекст
|
||||
|
||||
Чату нужна доставка сообщений в реальном времени. WebSocket — отдельный протокол со своим жизненным циклом, капризами прокси и кастомным фреймингом. Хочется остаться в рамках голого HTTP.
|
||||
|
||||
## Решение
|
||||
|
||||
Приём сообщений — Server-Sent Events. Отправка — обычный `fetch POST`. Чат поверх голого HTTP.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Никакого кастомного протокола: всё видно в DevTools как обычные HTTP-запросы, реконнект SSE встроен в браузер.
|
||||
- Работает везде, где работает HTTPS, без апгрейда соединения.
|
||||
- Двунаправленного канала нет — он и не нужен: у отправки и приёма разные пути.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-005: Аккаунт — ник и пароль
|
||||
|
||||
## Контекст
|
||||
|
||||
Email, телефон и OAuth тянут за собой внешние сервисы, интеграции и утечку идентичности. Bare — независимый инструмент без внешних завязок.
|
||||
|
||||
## Решение
|
||||
|
||||
Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и восстановления пароля. Серверная аутентификация: Argon2id, сессия в httpOnly cookie.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Ноль внешних сервисов в цикле регистрации и входа.
|
||||
- Сервер не знает о пользователе ничего, кроме ника.
|
||||
- Восстановления доступа нет — пароль ещё и материал ключа (ADR-006).
|
||||
- Открытость регистрации (инвайты или нет) — открытый вопрос.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-006: E2EE на WebCrypto, ключ за паролем
|
||||
|
||||
## Контекст
|
||||
|
||||
Оператор не должен уметь читать сообщения. Крипто-библиотеки на клиенте противоречат нулю зависимостей и аудируемости — вся криптография должна быть нативной.
|
||||
|
||||
## Решение
|
||||
|
||||
Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto), и хранится на сервере как блоб. Чаты 1:1: ECDH shared secret → AES-GCM.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Сервер видит только шифротекст — и блоба, и сообщений.
|
||||
- Сброс пароля невозможен by design: пароль — материал ключа. Забыл — значит забыл.
|
||||
- Мультидевайс-вход: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ.
|
||||
- Стойкость блоба к оффлайн-перебору равна стойкости пароля.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-007: Симметричный ключ комнаты и rekey
|
||||
|
||||
## Контекст
|
||||
|
||||
Сообщение в комнате должны читать все участники, но не сервер. Шифровать каждое сообщение отдельно каждому участнику — квадратичный объём работы и трафика.
|
||||
|
||||
## Решение
|
||||
|
||||
У комнаты один симметричный ключ. Он раздаётся участникам зашифрованным на их публичные ключи. При любом изменении состава — rekey: новый ключ, новая раздача.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников.
|
||||
- Вышедший участник не читает сообщения после rekey.
|
||||
- Rekey выполняют клиенты; серверу ключи недоступны.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-008: Сервер — реле с per-device очередью
|
||||
|
||||
## Контекст
|
||||
|
||||
Сервер никогда не является местом, где живёт история (философия, п. 2). Но получатель бывает офлайн — сообщение надо где-то подержать до доставки.
|
||||
|
||||
## Решение
|
||||
|
||||
Сервер хранит транзитную очередь зашифрованных недоставленных сообщений, per-device. Доставлено и подтверждено ACK — удалено с сервера. Не забрано за 30 дней — удалено. Кроме очереди сервер хранит только аккаунты и метаданные комнат и контактов.
|
||||
|
||||
## Следствия
|
||||
|
||||
- На сервере нет истории — компрометация сервера не раскрывает переписку.
|
||||
- Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно.
|
||||
- Нужна идентификация устройства для очередей — открытый вопрос.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-009: История — только на устройстве, в IndexedDB
|
||||
|
||||
## Контекст
|
||||
|
||||
История, живущая на сервере, делает сервер архивом и целью атак. У Bare история — собственность устройства.
|
||||
|
||||
## Решение
|
||||
|
||||
Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. При старте — `navigator.storage.persist()`; занятое место показывается через `storage.estimate()`.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Зашёл с другого устройства — там только новые сообщения.
|
||||
- Потеря устройства без экспорта — потеря истории.
|
||||
- Браузер может вычистить storage; `persist()` снижает риск, но не исключает его.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-010: Перенос истории — только ручной экспорт/импорт
|
||||
|
||||
## Контекст
|
||||
|
||||
История живёт на устройстве (ADR-009), но людям нужны перенос на новое устройство и склейка истории с нескольких. Облачная синхронизация сделала бы сервер архивом — non-goal.
|
||||
|
||||
## Решение
|
||||
|
||||
Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, идемпотентный merge в IndexedDB по messageId. Это единственный механизм переноса истории между устройствами.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Повторный импорт и склейка истории с двух устройств не создают дублей.
|
||||
- Файл `.bare` самодостаточен и не зависит от пароля аккаунта.
|
||||
- Перенос — явное действие пользователя. Автоматики не будет. Осознанно.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-011: Web Push + VAPID, пуш — сигнал
|
||||
|
||||
## Контекст
|
||||
|
||||
Без уведомлений чат бесполезен. Firebase SDK и вендорские кабинеты — зависимость и завязка, несовместимые с философией.
|
||||
|
||||
## Решение
|
||||
|
||||
Web Push + VAPID: одна пара ключей, никаких регистраций и оплат у вендоров, никакого Firebase SDK. Пуш — сигнал, не транспорт: содержимое догоняется через очередь при открытии. Текст пуша generic («имя: новое сообщение»). Declarative Web Push не используем — несовместим с E2EE. Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Сервер не знает плейнтекста — и в пуше его нет.
|
||||
- Доставка идёт через инфраструктуру вендоров браузеров (FCM/APNs/Mozilla) — свойство стандарта, не наша зависимость.
|
||||
- iOS: пуши только у PWA на экране «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение запрашиваем после первого отправленного сообщения, не при входе.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-012: Forward secrecy — non-goal v1
|
||||
|
||||
## Контекст
|
||||
|
||||
FS требует ratcheting в духе Signal Protocol: состояние на каждую пару устройств, синхронизация, сложность, несовместимая с клиентом из нескольких читаемых файлов.
|
||||
|
||||
## Решение
|
||||
|
||||
В v1 forward secrecy нет. 1:1 — статический ECDH shared secret, комнаты — симметричный ключ до rekey.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Компрометация приватного ключа раскрывает ранее записанные шифротексты.
|
||||
- Модель угроз говорит об этом прямо (`docs/threat-model.md`).
|
||||
- Возврат к FS позже возможен — отдельным ADR.
|
||||
Reference in New Issue
Block a user