Заложить репозиторий: философия, архитектура, модель угроз, 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:
Claude
2026-08-20 09:17:37 +00:00
co-authored by Claude Fable 5
commit 3d814112a9
20 changed files with 397 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# бинарь
/bare
# локальная база
*.db
*.db-*
+19
View File
@@ -0,0 +1,19 @@
# Правила работы с репозиторием Bare
Перед любой работой прочитай `docs/philosophy.md` и `docs/architecture.md`. Всё в них — принятые решения, а не предложения.
## Жёсткие ограничения
- Клиент: HTML, CSS, vanilla JS, нативные браузерные API. Никаких фреймворков, npm-зависимостей и сборки — ES-модули как есть.
- Сервер: Go, стандартная библиотека. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Ничего сверх — без обсуждения.
- Криптография на клиенте: только WebCrypto.
## Процесс
- Новые фичи и зависимости по умолчанию отклоняются. Порядок: обсуждение → ADR → код.
- Любое изменение архитектуры — через новый ADR в `docs/decisions/` (формат: контекст / решение / следствия).
- Простота важнее функций. Сомневаешься — не добавляй.
## Документация
Язык — русский. Стиль — сжатый и декларативный: короткие утвердительные абзацы, без маркетинговой воды и канцелярита.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bare contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
# Bare
Максимально простой веб-чат. Web-native PWA на ванильных технологиях: HTML, CSS, vanilla JS — и Go-сервер одним бинарём. Ноль npm-зависимостей, никакой сборки.
Bare — маленький независимый инструмент, а не конкурент Discord, Slack или Telegram. Название буквальное: голый, без лишнего, ничего не спрятано за слоями абстракций.
Сервер — реле, а не архив: он передаёт зашифрованные сообщения и забывает их. История живёт только на устройстве. E2EE по умолчанию — оператор видит только шифротекст. Забыл пароль — значит забыл: восстановления нет, потому что пароль — материал ключа.
## Документация
- [Философия](docs/philosophy.md) — манифест
- [Архитектура](docs/architecture.md) — обзор системы
- [Модель угроз](docs/threat-model.md) — от чего защищаемся и от чего нет
- [Решения](docs/decisions/) — ADR по ключевым решениям
- [Открытые вопросы](docs/open-questions.md)
## Статус
Проектирование. Кода ещё нет — сначала документы.
## Лицензия
[MIT](LICENSE)
+55
View File
@@ -0,0 +1,55 @@
# Архитектура
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.
## E2EE
Вся клиентская криптография — WebCrypto, без крипто-библиотек.
Идентичность пользователя — ECDH-пара (P-256). Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2), и хранится на сервере как блоб — сервер видит только шифротекст. Отсюда два следствия. Сброс пароля невозможен 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`. Импорт: файл плюс фраза, merge в IndexedDB по messageId, идемпотентно — повторный импорт и склейка истории с двух устройств не создают дублей.
Это единственный механизм переноса истории между устройствами. Осознанно.
## Пуши
Web Push + VAPID. Одна пара ключей, никаких регистраций и оплат у вендоров, никакого Firebase SDK.
Пуш — сигнал, не транспорт: содержимое всегда догоняется через очередь при открытии. Текст пуша generic («имя: новое сообщение») — сервер не знает плейнтекста. Declarative Web Push не используем: несовместим с E2EE.
iOS: пуши работают только у PWA, установленного на экран «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение на уведомления запрашивается после осмысленного действия (первое отправленное сообщение), не при входе.
Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки.
## Scope v1
Чаты 1:1 и комнаты. Только текст и эмодзи (эмодзи — юникод, отдельной фичи нет). Экспорт/импорт истории. Пуши на всех платформах. Всё остальное — за пределами v1.
+16
View File
@@ -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.
+15
View File
@@ -0,0 +1,15 @@
# ADR-002: Сервер на Go, один бинарь
## Контекст
Серверу Bare нужно немного: HTTP, SSE, SQLite, хеширование паролей, Web Push. Простота развёртывания и аудита важнее богатства экосистемы.
## Решение
Сервер пишется на Go со стандартной библиотекой. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Результат сборки — один бинарь.
## Следствия
- Развёртывание: скопировать бинарь, запустить. Без рантаймов и обязательных контейнеров.
- Любая новая зависимость требует обсуждения и нового ADR.
- Часть вещей (роутинг, миграции) пишем руками поверх stdlib.
+15
View File
@@ -0,0 +1,15 @@
# ADR-003: SQLite как единственная база
## Контекст
Сервер хранит мало: аккаунты, метаданные комнат и контактов, транзитную очередь. Отдельный сервер БД — лишняя движущаяся часть, противоречащая идее одного бинаря.
## Решение
Вся серверная persistence — SQLite, один файл рядом с бинарём.
## Следствия
- Бэкап — копия одного файла.
- Нет сетевой БД — нет её настройки, аутентификации и отказов.
- Вертикальный потолок масштабирования принимаем осознанно: Bare — маленький инструмент, не платформа.
+15
View File
@@ -0,0 +1,15 @@
# ADR-004: Realtime через SSE и fetch POST
## Контекст
Чату нужна доставка сообщений в реальном времени. WebSocket — отдельный протокол со своим жизненным циклом, капризами прокси и кастомным фреймингом. Хочется остаться в рамках голого HTTP.
## Решение
Приём сообщений — Server-Sent Events. Отправка — обычный `fetch POST`. Чат поверх голого HTTP.
## Следствия
- Никакого кастомного протокола: всё видно в DevTools как обычные HTTP-запросы, реконнект SSE встроен в браузер.
- Работает везде, где работает HTTPS, без апгрейда соединения.
- Двунаправленного канала нет — он и не нужен: у отправки и приёма разные пути.
+16
View File
@@ -0,0 +1,16 @@
# ADR-005: Аккаунт — ник и пароль
## Контекст
Email, телефон и OAuth тянут за собой внешние сервисы, интеграции и утечку идентичности. Bare — независимый инструмент без внешних завязок.
## Решение
Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и восстановления пароля. Серверная аутентификация: Argon2id, сессия в httpOnly cookie.
## Следствия
- Ноль внешних сервисов в цикле регистрации и входа.
- Сервер не знает о пользователе ничего, кроме ника.
- Восстановления доступа нет — пароль ещё и материал ключа (ADR-006).
- Открытость регистрации (инвайты или нет) — открытый вопрос.
+16
View File
@@ -0,0 +1,16 @@
# ADR-006: E2EE на WebCrypto, ключ за паролем
## Контекст
Оператор не должен уметь читать сообщения. Крипто-библиотеки на клиенте противоречат нулю зависимостей и аудируемости — вся криптография должна быть нативной.
## Решение
Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto), и хранится на сервере как блоб. Чаты 1:1: ECDH shared secret → AES-GCM.
## Следствия
- Сервер видит только шифротекст — и блоба, и сообщений.
- Сброс пароля невозможен by design: пароль — материал ключа. Забыл — значит забыл.
- Мультидевайс-вход: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ.
- Стойкость блоба к оффлайн-перебору равна стойкости пароля.
+15
View File
@@ -0,0 +1,15 @@
# ADR-007: Симметричный ключ комнаты и rekey
## Контекст
Сообщение в комнате должны читать все участники, но не сервер. Шифровать каждое сообщение отдельно каждому участнику — квадратичный объём работы и трафика.
## Решение
У комнаты один симметричный ключ. Он раздаётся участникам зашифрованным на их публичные ключи. При любом изменении состава — rekey: новый ключ, новая раздача.
## Следствия
- Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников.
- Вышедший участник не читает сообщения после rekey.
- Rekey выполняют клиенты; серверу ключи недоступны.
+15
View File
@@ -0,0 +1,15 @@
# ADR-008: Сервер — реле с per-device очередью
## Контекст
Сервер никогда не является местом, где живёт история (философия, п. 2). Но получатель бывает офлайн — сообщение надо где-то подержать до доставки.
## Решение
Сервер хранит транзитную очередь зашифрованных недоставленных сообщений, per-device. Доставлено и подтверждено ACK — удалено с сервера. Не забрано за 30 дней — удалено. Кроме очереди сервер хранит только аккаунты и метаданные комнат и контактов.
## Следствия
- На сервере нет истории — компрометация сервера не раскрывает переписку.
- Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно.
- Нужна идентификация устройства для очередей — открытый вопрос.
+15
View File
@@ -0,0 +1,15 @@
# ADR-009: История — только на устройстве, в IndexedDB
## Контекст
История, живущая на сервере, делает сервер архивом и целью атак. У Bare история — собственность устройства.
## Решение
Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. При старте — `navigator.storage.persist()`; занятое место показывается через `storage.estimate()`.
## Следствия
- Зашёл с другого устройства — там только новые сообщения.
- Потеря устройства без экспорта — потеря истории.
- Браузер может вычистить storage; `persist()` снижает риск, но не исключает его.
+15
View File
@@ -0,0 +1,15 @@
# ADR-010: Перенос истории — только ручной экспорт/импорт
## Контекст
История живёт на устройстве (ADR-009), но людям нужны перенос на новое устройство и склейка истории с нескольких. Облачная синхронизация сделала бы сервер архивом — non-goal.
## Решение
Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, идемпотентный merge в IndexedDB по messageId. Это единственный механизм переноса истории между устройствами.
## Следствия
- Повторный импорт и склейка истории с двух устройств не создают дублей.
- Файл `.bare` самодостаточен и не зависит от пароля аккаунта.
- Перенос — явное действие пользователя. Автоматики не будет. Осознанно.
+15
View File
@@ -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 на экране «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение запрашиваем после первого отправленного сообщения, не при входе.
+15
View File
@@ -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.
+11
View File
@@ -0,0 +1,11 @@
# Открытые вопросы
Решения по этим пунктам ещё не приняты. Каждое принятое решение уходит в ADR и вычёркивается отсюда.
- Регистрация: открытая или по инвайтам?
- Механика добавления контакта и приглашения в комнату: по нику? по ссылке?
- Лимиты: длина сообщения, rate limiting, антиспам.
- Смена пароля (= перешифровка ключевого блоба): в v1 или позже?
- Идентификация устройства для per-device очередей.
- Язык интерфейса (ru/en); нужна ли i18n.
- Визуальная айдентика: отдельный бриф будет добавлен в `docs/identity/`.
+44
View File
@@ -0,0 +1,44 @@
# Философия
Bare — максимально простой веб-чат. Utility, а не платформа. Название буквальное: голый, без лишнего, ничего не спрятано за слоями абстракций.
## Принципы
### 1. Bare = голый web
HTML, CSS, vanilla JS, нативные браузерные API. Ноль npm-зависимостей на клиенте. Никакой сборки — ES-модули как есть.
### 2. Сервер — реле, а не архив
Сервер никогда не является местом, где живёт история. Он передаёт и забывает.
### 3. История живёт только на устройстве
Зашёл с другого места — там только новые сообщения. Перенос истории — явный ручной экспорт/импорт.
### 4. Забыл пароль — значит забыл
Восстановления нет, и это не политика, а криптография: пароль — материал ключа.
### 5. Оператор не может читать сообщения
E2EE по умолчанию. У админа с полным доступом к серверу — только шифротекст.
### 6. Аудируемость через простоту
Клиент — несколько читаемых файлов без сборки. Любой может проверить глазами, что код делает.
### 7. Простота важнее функций
Каждая фича должна оправдать своё существование. По умолчанию — нет.
## Non-goals
Это не «пока не успели», а осознанные решения не делать:
- Облачная синхронизация истории между устройствами.
- «Удалить у всех» после доставки.
- Серверный поиск — поиск только локальный.
- Файлы, медиа, голос, реакции, треды (v1).
- Forward secrecy (v1).
- Федерация.
+35
View File
@@ -0,0 +1,35 @@
# Модель угроз
Честная. Здесь написано не только что Bare защищает, но и чего он защитить не может.
## Что защищаем
Содержимое сообщений. Оно шифруется на устройстве отправителя и расшифровывается на устройствах получателей. Сервер, канал и оператор видят только шифротекст.
## От кого защищаем
**Пассивный оператор сервера.** Админ с полным доступом к базе и диску видит: ники, argon2-хеши, зашифрованные ключевые блобы, метаданные комнат и контактов, транзитную очередь шифротекстов. Плейнтекста у него нет.
**Сетевой наблюдатель.** HTTPS обязателен. Наблюдатель видит факт и объём трафика к серверу, не содержимое.
**Кража базы или бэкапа.** В базе нет ничего сверх того, что видит оператор: те же шифротексты и метаданные.
**Push-инфраструктура.** Пуш не несёт содержимого: сервер не знает плейнтекста, поэтому его нет и в пуше.
## От кого не защищаем
**Активно-злонамеренный оператор.** Оператор, способный подменить клиентский код, может украсть ключи и плейнтекст. Это фундаментальный предел web-E2EE: клиент каждый раз загружается с сервера. Смягчение — открытый код и клиент из нескольких читаемых файлов без сборки: подмену можно заметить глазами. Гарантии нет.
**Метаданные.** Кто, с кем, когда и сообщениями какого размера обменивается — серверу видно. Скрытие метаданных — не задача Bare.
**Компрометация устройства.** История лежит на устройстве в открытом виде (IndexedDB). Доступ к устройству — доступ к истории. Защита устройства — зона ответственности пользователя и ОС.
**Слабый пароль.** Пароль — материал ключа. Ключевой блоб хранится на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля. Слабый пароль ослабляет и вход, и шифрование.
**Собеседник.** E2EE не защищает от участника чата: получатель может сохранить, переслать, сфотографировать. «Удалить у всех» после доставки не существует.
## Осознанные пределы v1
**Forward secrecy отсутствует.** Компрометация приватного ключа пользователя раскрывает ранее записанные атакующим шифротексты его чатов 1:1. Осознанный non-goal v1.
**Push-транспорт идёт через инфраструктуру вендоров браузеров** (FCM, APNs, Mozilla). Это свойство стандарта Web Push, а не наша зависимость. Вендоры видят факт и время доставки пуша.