From a898347005112dd6288c6bad95c182080c68f626 Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Thu, 20 Aug 2026 22:42:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D0=B5=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D1=8B:=20ADR-015=E2=80=A6022,=20=D0=B1?= =?UTF-8?q?=D1=80=D0=B8=D1=84=20=D0=B0=D0=B9=D0=B4=D0=B5=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Регистрация открытая (015), контакты и приглашения по нику с закреплением ключа и блокировкой (016), лимиты (017), идентификация устройства через случайный deviceId в IndexedDB и серверный fan-out (018), отказ от серверного перца (019), смена пароля не в v1 (020), интерфейс ru/en (021), айдентика «Скобы» по turn 2 канваса (022). Архитектура, модель угроз и README обновлены. В открытых вопросах остались два: моногарнитура с кириллицей (Fragment Mono её не содержит) и удаление аккаунта. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01R2pCJkWyG2aYu48u1Vp1A1 --- README.md | 1 + docs/architecture.md | 36 +++++++++++- docs/decisions/005-accounts.md | 2 +- docs/decisions/008-server-relay.md | 2 +- docs/decisions/015-open-registration.md | 17 ++++++ docs/decisions/016-contacts-by-nick.md | 24 ++++++++ docs/decisions/017-limits.md | 34 +++++++++++ docs/decisions/018-device-identity.md | 22 +++++++ docs/decisions/019-no-server-pepper.md | 23 ++++++++ docs/decisions/020-password-change-later.md | 18 ++++++ docs/decisions/021-ui-language.md | 21 +++++++ docs/decisions/022-visual-identity.md | 21 +++++++ docs/identity/brief.md | 65 +++++++++++++++++++++ docs/open-questions.md | 10 +--- docs/threat-model.md | 8 ++- 15 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 docs/decisions/015-open-registration.md create mode 100644 docs/decisions/016-contacts-by-nick.md create mode 100644 docs/decisions/017-limits.md create mode 100644 docs/decisions/018-device-identity.md create mode 100644 docs/decisions/019-no-server-pepper.md create mode 100644 docs/decisions/020-password-change-later.md create mode 100644 docs/decisions/021-ui-language.md create mode 100644 docs/decisions/022-visual-identity.md create mode 100644 docs/identity/brief.md diff --git a/README.md b/README.md index 08deb9f..5b0a376 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Bare — маленький независимый инструмент, а не - [Архитектура](docs/architecture.md) — обзор системы - [Модель угроз](docs/threat-model.md) — от чего защищаемся и от чего нет - [Решения](docs/decisions/) — ADR по ключевым решениям +- [Айдентика](docs/identity/brief.md) — знак, цвет, типографика, экран - [Открытые вопросы](docs/open-questions.md) ## Статус diff --git a/docs/architecture.md b/docs/architecture.md index 0b068a8..a018844 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,9 +10,9 @@ Bare — это PWA-клиент на ванильных веб-технолог ## Аккаунты -Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и интеграций. Восстановления пароля нет. +Регистрация открытая — ник и пароль, инвайтов нет. Ник уникален и является идентификатором пользователя: 3–32 символа, латиница в нижнем регистре, цифры, `_`, `-`. Без email, телефона, OAuth и интеграций. Восстановления пароля нет. Смены пароля в v1 тоже нет — появится позже: формат блоба это уже позволяет. -Серверная аутентификация: Argon2id, сессия в httpOnly cookie. +Серверная аутентификация: Argon2id, сессия в httpOnly cookie, привязанная к устройству. Пароль — не короче 12 символов; правил про регистры и спецсимволы нет: длина важнее состава. UI рекомендует парольную фразу из нескольких слов и при регистрации прямо говорит: пароль — это ключ шифрования, а не запись в базе; восстановления нет. @@ -28,9 +28,27 @@ Bare — это PWA-клиент на ванильных веб-технолог Forward secrecy — осознанный non-goal v1. +## Контакты и комнаты + +Контакт добавляется по точному нику: сервер отдаёт публичный ключ и создаёт запрос, адресат принимает или отклоняет. Сообщения 1:1 ходят только между взаимными контактами. Поиска и каталога нет; ссылок и QR — тоже. + +Публичный ключ контакта закрепляется на устройстве при первом получении. Если сервер вернёт для ника другой ключ — предупреждение, шифрование на новый ключ только после явного подтверждения. Отпечаток виден в карточке контакта. + +В комнату приглашают только своих контактов, добавлением в состав без отдельного согласия. Создатель комнаты исключает участников; выйти может любой. Каждое изменение состава — rekey. + +Отклонить запрос контакта можно с блокировкой: сервер не доставляет от заблокированного ни сообщений, ни новых запросов. + +## Устройства + +Устройство — 128-битный случайный `deviceId`, сгенерированный клиентом при первом запуске и хранящийся в IndexedDB рядом с историей. Стёрли данные сайта — новое устройство. `deviceId` — адрес очереди, не секрет; аутентифицирует сессия. + +Доставка — серверный fan-out: сообщение раскладывается в очереди всех устройств всех участников, кроме отправившего. Все устройства аккаунта владеют одним приватным ключом, шифротекст один для всех. Отправитель устройств адресата не знает. + +Устройство без связи 30 дней удаляется вместе с очередью и push-подпиской. Список устройств с метками из user-agent — в настройках; любое можно удалить. + ## Хранение -Сервер хранит только три вещи: аккаунты (ник, argon2-хеш, зашифрованный ключевой блоб), метаданные комнат и контактов, транзитную очередь зашифрованных недоставленных сообщений. Очередь per-device: доставлено и подтверждено ACK — удалено с сервера; не забрано за 30 дней — удалено. +Сервер хранит только три вещи: аккаунты (ник, argon2-хеш, зашифрованный ключевой блоб, устройства), метаданные комнат, контактов и блокировок, транзитную очередь зашифрованных недоставленных сообщений. Очередь per-device: доставлено и подтверждено ACK — удалено с сервера; не забрано за 30 дней — удалено; больше 10 000 сообщений — старейшие удаляются. Серверного «перца» для ключевого блоба нет: развёртывание — бинарь плюс один файл базы, ничего, что нельзя потерять, кроме самой базы. Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. @@ -54,6 +72,18 @@ iOS: пуши работают только у PWA, установленного Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки. +## Лимиты + +Сообщение — до 4 096 символов плейнтекста, проверяет клиент. Тело запроса — до 32 КиБ, проверяет сервер. Частота — token bucket в памяти сервера, превышение — `429` с `Retry-After`: 60 сообщений в минуту, 20 запросов контакта в сутки, 5 регистраций в час с IP, 10 попыток входа за 15 минут на ник. Контактов — до 500, участников комнаты — до 100, устройств — до 10. Все числа — константы в одном файле сервера, конфигурации нет. + +Антиспам структурный: писать можно только взаимным контактам, блокировка ника, лимиты. Модерации нет — серверу нечего модерировать. + +## Интерфейс + +Два языка, русский и английский: два плоских словаря в одном ES-модуле, выбор по `navigator.language` с переключателем в настройках. Склонения — `Intl.PluralRules`, даты — `Intl.DateTimeFormat`. + +Айдентика — «Скобы»: знак из четырёх углов, моноширинная гарнитура для всего, сообщения строками без баблов, палитра bone/ink/stone с одним красным акцентом. Бриф — `docs/identity/brief.md`. Шрифт не грузится с внешних хостов. + ## Scope v1 Чаты 1:1 и комнаты. Только текст и эмодзи (эмодзи — юникод, отдельной фичи нет). Экспорт/импорт истории. Пуши на всех платформах. Всё остальное — за пределами v1. diff --git a/docs/decisions/005-accounts.md b/docs/decisions/005-accounts.md index a6d2767..fba8afc 100644 --- a/docs/decisions/005-accounts.md +++ b/docs/decisions/005-accounts.md @@ -13,4 +13,4 @@ Email, телефон и OAuth тянут за собой внешние сер - Ноль внешних сервисов в цикле регистрации и входа. - Сервер не знает о пользователе ничего, кроме ника. - Восстановления доступа нет — пароль ещё и материал ключа (ADR-006). -- Открытость регистрации (инвайты или нет) — открытый вопрос. +- Регистрация открытая — ADR-015. diff --git a/docs/decisions/008-server-relay.md b/docs/decisions/008-server-relay.md index ee28355..2ae4cf0 100644 --- a/docs/decisions/008-server-relay.md +++ b/docs/decisions/008-server-relay.md @@ -12,4 +12,4 @@ - На сервере нет истории — компрометация сервера не раскрывает переписку. - Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно. -- Нужна идентификация устройства для очередей — открытый вопрос. +- Идентификация устройства для очередей — ADR-018. diff --git a/docs/decisions/015-open-registration.md b/docs/decisions/015-open-registration.md new file mode 100644 index 0000000..82ce7fc --- /dev/null +++ b/docs/decisions/015-open-registration.md @@ -0,0 +1,17 @@ +# ADR-015: Регистрация открытая + +Закрывает открытый вопрос из [ADR-005](005-accounts.md). + +## Контекст + +ADR-005 оставил открытым, нужны ли инвайты. Инвайты — это сущность (код, выдача, срок, учёт), отдельный UI и барьер для первого контакта с продуктом. Bare — публичный инструмент, а не закрытый клуб. + +## Решение + +Регистрация открытая: ник и пароль, больше ничего. Инвайтов нет. + +## Следствия + +- Любой может завести аккаунт, значит, любой может злоупотреблять. Защита — не на входе, а в механике общения: сообщения ходят только между взаимными контактами (ADR-016) и в пределах лимитов (ADR-017). +- Ники занимаются по принципу «кто первый». Споров о никах сервер не разбирает. +- Закрытая регистрация для приватных инсталляций — отдельным ADR, если понадобится. В v1 её нет. diff --git a/docs/decisions/016-contacts-by-nick.md b/docs/decisions/016-contacts-by-nick.md new file mode 100644 index 0000000..32c2229 --- /dev/null +++ b/docs/decisions/016-contacts-by-nick.md @@ -0,0 +1,24 @@ +# ADR-016: Контакты и приглашения — по нику + +Закрывает открытый вопрос о механике добавления контакта. + +## Контекст + +Ник — единственный идентификатор пользователя (ADR-005). Ссылки-приглашения, QR-коды, поиск и каталог пользователей — отдельные сущности со своим жизненным циклом. При открытой регистрации (ADR-015) нужен ещё и механизм согласия: иначе любой, кто знает ник, может писать кому угодно. + +## Решение + +**Контакт.** Пользователь вводит точный ник. Сервер отдаёт публичный ключ и создаёт запрос контакта. Адресат видит запрос и принимает или отклоняет. Сообщения 1:1 ходят только между взаимными контактами. Поиска по части ника и каталога нет. + +**Ключи.** Публичный ключ контакта сохраняется на устройстве при первом получении и больше с сервера не перечитывается. Если сервер когда-либо вернёт для этого ника другой ключ — клиент показывает предупреждение и не шифрует на новый ключ без явного подтверждения. Отпечаток ключа виден в карточке контакта; сверка вне канала — по желанию пользователя, автоматики нет. + +**Комната.** Участник приглашает в комнату только своих контактов — добавлением в состав, без отдельного согласия: согласие уже дано на уровне контакта. Не хочешь — выходишь. Создатель комнаты может исключать участников. Любое изменение состава — rekey (ADR-007). + +**Блокировка.** Отклонить запрос можно с блокировкой ника: сервер не доставляет от заблокированного ни сообщений, ни новых запросов. + +## Следствия + +- Серверу достаточно таблиц контактов (пара ников, статус) и блокировок. Новых сущностей — инвайтов, ссылок, токенов — нет. +- Ник надо знать заранее и передать вне Bare. Это ограничение осознанное: Bare — инструмент для людей, которые уже знакомы. +- Закрепление ключа защищает от подмены ключа сервером после первого контакта. Первый контакт — доверие на слово (TOFU): активно-злонамеренный оператор в модели угроз и так за пределами защиты. +- Спам ограничен структурно: чужому нельзя написать, пока он не принял запрос; сами запросы — под лимитами (ADR-017). diff --git a/docs/decisions/017-limits.md b/docs/decisions/017-limits.md new file mode 100644 index 0000000..ae3e7f2 --- /dev/null +++ b/docs/decisions/017-limits.md @@ -0,0 +1,34 @@ +# ADR-017: Лимиты + +Закрывает открытый вопрос о лимитах и антиспаме. + +## Контекст + +Открытая регистрация (ADR-015) и реле с очередями (ADR-008) требуют потолков: на размер сообщения, на частоту действий, на объём хранимого. Сервер не видит содержимого, поэтому антиспам может быть только структурным и количественным — фильтрации по тексту не существует. + +## Решение + +**Размеры.** +- Сообщение — до 4 096 символов плейнтекста; проверяет клиент до шифрования. +- Тело любого запроса к серверу — до 32 КиБ. Это единственная проверка размера на сервере. +- Ник — 3–32 символа: латиница в нижнем регистре, цифры, `_`, `-`. Регистр не различается. + +**Частота.** Token bucket в памяти сервера, без внешних хранилищ. Превышение — `429` с `Retry-After`. +- Отправка сообщений: 60 в минуту на аккаунт. +- Запросы контакта: 20 в сутки на аккаунт. +- Регистрация: 5 в час с одного IP. +- Вход: 10 попыток за 15 минут на ник, 30 — с одного IP. Argon2 дорогой; это защита процессора сервера в той же мере, что и аккаунтов. +- Прочие запросы: 120 в минуту на аккаунт. + +**Объёмы.** +- Очередь устройства — до 10 000 сообщений; при переполнении старейшие удаляются. Плюс 30-дневный TTL из ADR-008. +- Контактов у аккаунта — до 500. Участников в комнате — до 100. Устройств у аккаунта — до 10. + +**Антиспам.** Сводится к трём вещам: писать можно только взаимным контактам (ADR-016), блокировка ника (ADR-016), лимиты выше. Жалоб и модерации нет: серверу нечего модерировать. + +## Следствия + +- Все числа — константы в одном файле сервера. Клиент дублирует только длину сообщения и формат ника — для подсказок до отправки. Конфигурации нет: поменять лимит — поменять константу и пересобрать. +- Перезапуск сервера сбрасывает счётчики. Для маленького инструмента это приемлемо. +- Лимит по IP не различает людей за одним NAT. Осознанная грубость: цифры взяты с запасом. +- Переполнение очереди теряет сообщения, как и TTL. Устройство, не выходившее на связь, получает не всё — это уже зафиксировано в ADR-008. diff --git a/docs/decisions/018-device-identity.md b/docs/decisions/018-device-identity.md new file mode 100644 index 0000000..0e91b9e --- /dev/null +++ b/docs/decisions/018-device-identity.md @@ -0,0 +1,22 @@ +# ADR-018: Устройство — случайный идентификатор в IndexedDB + +Закрывает открытый вопрос из [ADR-008](008-server-relay.md). + +## Контекст + +Очередь недоставленных сообщений — per-device (ADR-008), подписка на пуши — тоже per-device (ADR-011). Серверу нужно отличать устройства одного аккаунта. Стабильного идентификатора браузера не существует, и это правильно; его надо завести самим. + +## Решение + +- При первом запуске клиент генерирует 128-битный случайный `deviceId` (`crypto.getRandomValues`) и хранит его в IndexedDB рядом с историей. Одно хранилище — один жизненный цикл: стёрли данные сайта — исчезли и история, и идентичность устройства. Новый запуск — новое устройство. +- При входе клиент передаёт `deviceId`. Сервер создаёт запись устройства с ключом (аккаунт, `deviceId`) и очередь к нему. Сессионная cookie привязана к устройству. +- `deviceId` — не секрет, а адрес очереди. Аутентифицирует сессия. Коллизии между аккаунтами невозможны: ключ составной. +- Доставка — серверный fan-out: сообщение раскладывается в очереди всех устройств всех участников, кроме отправившего. Отправитель не знает и не должен знать устройств адресата: все устройства одного аккаунта владеют одним приватным ключом (ADR-006), шифротекст один для всех. +- Устройство без связи 30 дней удаляется вместе с очередью и push-подпиской — тот же срок, что TTL очереди (ADR-008). На следующем запуске клиент регистрируется как новое устройство; локальная история при этом цела. +- Клиент при регистрации устройства передаёт короткую метку из user-agent («Firefox · Linux»). Список устройств с метками и датой последней связи виден в настройках; любое можно удалить — очередь и сессия уничтожаются. + +## Следствия + +- Сообщение, отправленное с телефона, появляется и на ноутбуке — как новое, через его очередь. Это не синхронизация истории: то, что было до появления устройства, на него не приедет. +- Один браузер в двух профилях или в режиме инкогнито — разные устройства. Инкогнито плодит устройства при каждом запуске; их чистит 30-дневный срок и лимит в 10 устройств (ADR-017). +- Потеря `deviceId` (чистка storage) эквивалентна потере устройства: недоставленное в старую очередь пропадает по TTL. `navigator.storage.persist()` (ADR-009) снижает риск. diff --git a/docs/decisions/019-no-server-pepper.md b/docs/decisions/019-no-server-pepper.md new file mode 100644 index 0000000..2665355 --- /dev/null +++ b/docs/decisions/019-no-server-pepper.md @@ -0,0 +1,23 @@ +# ADR-019: Серверного «перца» для ключевого блоба нет + +Закрывает открытый вопрос о дополнительном шифровании блоба серверным ключом. + +## Контекст + +Идея: шифровать ключевой блоб ещё раз серверным ключом, хранящимся вне базы. Тогда украденный дамп или бэкап сам по себе не даёт материала для оффлайн-перебора паролей. Против оператора это не помогает — у него есть оба. + +## Решение + +Перца нет. Блоб хранится так, как описано в ADR-006 и ADR-013: один слой шифрования, ключ выведен из пароля. + +Аргументы: + +- Появляется второй критичный артефакт рядом с базой. ADR-003 обещает «бэкап — копия одного файла»; перец это обещание ломает. Потеря файла с перцем — невозможность входа с новых устройств для всех пользователей сразу, а восстановления паролей в Bare нет by design. Для маленького самохостного инструмента риск потерять ключ при переезде выше риска, который он закрывает. +- Сценарий «утёк бэкап, но не сервер» уже закрыт ADR-013: PBKDF2 с миллионом итераций и пароль от 12 символов делают перебор дорогим. Словарный пароль перец тоже не спасёт при компрометации сервера. +- Каждый дополнительный слой — код, который нужно аудировать, и оперативная процедура, которую нужно помнить. Простота важнее. + +## Следствия + +- Стойкость блоба в любом сценарии равна стойкости пароля, как и записано в модели угроз. Новых оговорок в ней не появляется. +- Развёртывание остаётся «бинарь плюс файл базы». Ничего, что нельзя потерять, кроме самой базы, у оператора нет. +- Решение обратимо: перец можно добавить позже отдельным ADR — он не меняет формат блоба, только оборачивает его. diff --git a/docs/decisions/020-password-change-later.md b/docs/decisions/020-password-change-later.md new file mode 100644 index 0000000..0733bfc --- /dev/null +++ b/docs/decisions/020-password-change-later.md @@ -0,0 +1,18 @@ +# ADR-020: Смена пароля — не в v1 + +Закрывает открытый вопрос о сроках смены пароля. + +## Контекст + +Пароль — материал ключа, которым зашифрован ключевой блоб (ADR-006). Смена пароля — это расшифровка блоба старым ключом и перешифровка новым, целиком на клиенте; ECDH-пара и секрет аккаунта не меняются. Технически просто, но это отдельный сценарий с гонками между устройствами и отдельный UI. + +## Решение + +В v1 смены пароля нет. Появится позже отдельным ADR. + +Формат блоба уже готов к этому: параметры KDF лежат рядом с блобом (ADR-013), тот же механизм перешифровки нужен для повышения итераций. Ключи и секрет аккаунта при смене пароля сохраняются, поэтому контакты, комнаты и экспортированные архивы останутся в силе. + +## Следствия + +- В v1 скомпрометированный пароль — скомпрометированный аккаунт, и путь один: новый аккаунт. Об этом надо прямо сказать при регистрации, рядом с «восстановления нет». +- Серверная часть смены пароля — замена argon2-хеша и блоба одним запросом — тривиальна; откладывается целиком, чтобы не делать половину. diff --git a/docs/decisions/021-ui-language.md b/docs/decisions/021-ui-language.md new file mode 100644 index 0000000..0d4a52c --- /dev/null +++ b/docs/decisions/021-ui-language.md @@ -0,0 +1,21 @@ +# ADR-021: Интерфейс на русском и английском, i18n своими руками + +Закрывает открытый вопрос о языке интерфейса. + +## Контекст + +Аудитория Bare двуязычна. Библиотеки i18n на клиенте невозможны (ADR-001), и они не нужны: интерфейс чата — несколько десятков строк. + +## Решение + +- Два языка: русский и английский. Других в v1 нет. +- Все строки интерфейса — в одном ES-модуле, два плоских словаря с одинаковыми ключами. Ничего сверх: никаких форматов файлов переводов, никаких внешних инструментов. +- Язык при первом запуске — из `navigator.language` (ru → русский, всё остальное → английский). Переключатель в настройках, выбор хранится в `localStorage`. +- Склонения и множественные формы — руками, через `Intl.PluralRules`. Даты и время — через `Intl.DateTimeFormat` с выбранной локалью. +- Документация остаётся на русском. + +## Следствия + +- Добавить язык — добавить словарь; удалить — удалить. Ключи проверяются на полноту простым скриптом при желании, не сборкой. +- Ники и содержимое сообщений языком интерфейса не затрагиваются. +- Текст в UI короткий и строчный — по голосу бренда (ADR-022). Переводить надо в том же регистре и тоне. diff --git a/docs/decisions/022-visual-identity.md b/docs/decisions/022-visual-identity.md new file mode 100644 index 0000000..376627f --- /dev/null +++ b/docs/decisions/022-visual-identity.md @@ -0,0 +1,21 @@ +# ADR-022: Визуальная айдентика — «Скобы», моно-интерфейс без баблов + +Закрывает открытый вопрос о визуальной айдентике. + +## Контекст + +Айдентика исследовалась в Claude Design: шесть направлений знака, два развиты до мини-айдентик, затем интерфейс чата на базе одной из них. Исследование — [канвас «Исследование айдентики Bare»](https://claude.ai/design/p/0126e4da-40e4-449c-bfbb-7aefc7b64659?file=Bare+Identity.dc.html). + +## Решение + +Работаем с turn 2 канваса: интерфейс на базе мини-айдентики 1h «Скобы», одна гарнитура — моноширинная, сообщения без баблов. Артборды 2a (десктоп, 1120) и 2b (мобильный, 390) — референс для вёрстки клиента. + +Подробности — палитра, типографика, правила знака, структура экрана — в [docs/identity/brief.md](../identity/brief.md). Бриф — источник истины для клиента; канвас — история исследования. + +## Следствия + +- Знак — четыре угла рамки, внутри пусто. Без градиентов, теней, маскотов и пузырей. +- Цвет акцента — только для скоб в акцентных случаях и статусов. Не для кнопок и заливок. +- Сообщения — строки: автор, время, текст. Эстетика лога, а не мессенджера. +- Шрифт в продукте не загружается с внешних хостов: либо файл в репозитории, либо системный моно-стек. Внешний запрос за шрифтом — утечка метаданных и зависимость, несовместимые с философией. +- Выбор конкретной моногарнитуры для кириллицы — открытый вопрос (см. бриф). diff --git a/docs/identity/brief.md b/docs/identity/brief.md new file mode 100644 index 0000000..4ef792b --- /dev/null +++ b/docs/identity/brief.md @@ -0,0 +1,65 @@ +# Айдентика Bare — бриф + +Зафиксировано в [ADR-022](../decisions/022-visual-identity.md). Источник — turn 2 [канваса](https://claude.ai/design/p/0126e4da-40e4-449c-bfbb-7aefc7b64659?file=Bare+Identity.dc.html), артборды 2a и 2b. Этот документ — источник истины для клиента. + +## Идея + +Bare не добавляет — он кадрирует. Четыре угла держат внимание на содержимом, которого сам бренд не касается. Система живёт на разметочной эстетике: тонкие линии, моно-подписи, много воздуха. + +## Знак + +Четыре уголка рамки, из которой вынули содержимое. Знак — пустота внутри. + +- Внутрь рамки ничего не помещать. +- Знак всегда одноцветный: ink, bone или mark. Без градиентов, теней, обводок. +- Скобы можно раздвигать, «кадрируя» заголовок — пропорции углов неизменны. +- Охранное поле — длина одного уголка. +- Favicon и иконка приложения: на 16 px штрих — 9 единиц сетки, зазоры между углами в пропорции 3:2. Углы масштабируются без потерь. На тёмном и акцентном фоне знак всегда светлый. +- Минимальный размер знака — 12 px. + +Wordmark — `bare`, строчными, той же моногарнитурой, что и интерфейс. + +## Цвет + +| Токен | Значение | Роль | +|---|---|---| +| bone | `#F7F5F0` | фон | +| ink | `#1B1917` | текст, знак | +| stone | `#A9A59D` | вторичный текст, метаданные | +| mark | `oklch(55% 0.19 20)` | акцент: скобы в акцентных случаях, статусы, непрочитанное | + +Служебные серые из мока: `#3C3B38`, `#6E6D68` — промежуточные тона текста; `#DEDCD6`, `#E7E3DA` — линии и разделители. + +Акцент — красный регистрационных меток. Не для кнопок и заливок. Интерактивное обозначается не цветом, а положением и подчёркиванием. + +Тёмной темы в моке нет. Появится — отдельным артбордом и правкой этого брифа. + +## Типографика + +Одна гарнитура — моноширинная, для всего: wordmark, интерфейс, сообщения, метаданные. Вторая гарнитура (Familjen Grotesk из мини-айдентики 1h) в turn 2 снята. + +Размеры из мока: 10, 11, 12, 13, 14, 15 px для интерфейса; 32 px — wordmark. Текст сообщений — 14–15 px. + +Регистр — строчный везде, включая заголовки и кнопки. Имена людей — строчными, `@ник`; комнаты — `#имя`. + +**Открытый вопрос — гарнитура.** В моке — Fragment Mono, но она не содержит базовой кириллицы (U+0400–045F): русский текст в моке рендерится системным fallback. Для ru-интерфейса (ADR-021) нужен выбор: + +- моногарнитура с кириллицей под OFL, один файл `.woff2` в репозитории — JetBrains Mono или IBM Plex Mono; +- или системный стек `ui-monospace, monospace` — ноль загрузок, вид зависит от ОС. + +В любом случае шрифт не грузится с внешних хостов (ADR-022). + +## Экран + +Сообщения без баблов. Каждое — строка: автор, время, текст. Последовательные сообщения одного автора группируются под одним именем. + +**Десктоп (2a, 1120).** Две колонки. + +- Слева — сайдбар: wordmark; раздел «каналы» со списком `#комнат`, счётчик непрочитанных у имени; раздел «личные» со списком `@контактов`; внизу — «ты: @ник». +- Справа — чат: шапка с именем комнаты и строкой «N онлайн · тема: …»; лента с разделителями дат и разделителем «новые» перед непрочитанным; внизу поле ввода с префиксом `>` и плейсхолдером «сообщение в #имя», под ним подсказка «enter — отправить». + +**Мобильный (2b, 390).** Одна колонка: шапка с именем и «N онлайн», лента, поле ввода. Список чатов — отдельный экран. + +## Голос + +Короткие фразы, никакого маркетингового глянца. Строчные буквы. Пример из мока: «браузер и есть приложение». diff --git a/docs/open-questions.md b/docs/open-questions.md index 50c8ad5..045aa92 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -2,11 +2,5 @@ Решения по этим пунктам ещё не приняты. Каждое принятое решение уходит в ADR и вычёркивается отсюда. -- Регистрация: открытая или по инвайтам? -- Механика добавления контакта и приглашения в комнату: по нику? по ссылке? -- Лимиты: длина сообщения, rate limiting, антиспам. -- Смена пароля (= перешифровка ключевого блоба): в v1 или позже? -- Идентификация устройства для per-device очередей. -- Серверный «перец» для ключевого блоба: дополнительное шифрование блоба серверным ключом, хранящимся вне базы. Плюс: дамп базы сам по себе перестаёт быть материалом для оффлайн-перебора. Минус: не защищает от оператора; потеря серверного ключа — невозможность входа с новых устройств для всех. Решение отложено. -- Язык интерфейса (ru/en); нужна ли i18n. -- Визуальная айдентика: отдельный бриф будет добавлен в `docs/identity/`. +- Моногарнитура для кириллицы. Fragment Mono из мока не содержит базовой кириллицы (U+0400–045F): русский интерфейс в моке рендерится системным fallback. Варианты: JetBrains Mono или IBM Plex Mono одним `.woff2` в репозитории; системный стек `ui-monospace, monospace`. Внешних загрузок шрифта не будет в любом случае (ADR-022). Подробнее — `docs/identity/brief.md`. +- Удаление аккаунта. ADR-014 упоминает перерегистрацию под тем же ником — она невозможна без удаления. Решить: есть ли удаление в v1, что происходит с комнатами (rekey без участника) и очередями. diff --git a/docs/threat-model.md b/docs/threat-model.md index 09bd0b5..7f9e2e7 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -12,7 +12,9 @@ **Сетевой наблюдатель.** HTTPS обязателен. Наблюдатель видит факт и объём трафика к серверу, не содержимое. -**Кража базы или бэкапа.** В базе нет ничего сверх того, что видит оператор: те же шифротексты и метаданные. +**Кража базы или бэкапа.** В базе нет ничего сверх того, что видит оператор: те же шифротексты и метаданные. Ключевые блобы в дампе — материал для оффлайн-перебора паролей; цена перебора задана ADR-013. Серверного «перца» нет (ADR-019): второй критичный артефакт рядом с базой опаснее для маленькой инсталляции, чем сценарий, который он закрывает. + +**Спам и незнакомцы.** Регистрация открытая. Написать можно только взаимному контакту: запрос надо принять. Отклонение с блокировкой — и сервер больше не доставляет от этого ника ничего. Частота запросов и сообщений ограничена (ADR-017). **Push-инфраструктура.** Пуш не несёт содержимого: сервер не знает плейнтекста, поэтому его нет и в пуше. @@ -20,10 +22,14 @@ **Активно-злонамеренный оператор.** Оператор, способный подменить клиентский код, может украсть ключи и плейнтекст. Это фундаментальный предел web-E2EE: клиент каждый раз загружается с сервера. Смягчение — открытый код и клиент из нескольких читаемых файлов без сборки: подмену можно заметить глазами. Гарантии нет. +Второй путь того же оператора — подмена публичного ключа при добавлении контакта. Первый контакт — доверие на слово (TOFU). Дальше ключ закреплён на устройстве: подмена после первого контакта видна как предупреждение (ADR-016). Отпечаток ключа доступен для сверки вне канала; автоматической защиты первого контакта нет. + **Метаданные.** Кто, с кем, когда и сообщениями какого размера обменивается — серверу видно. Скрытие метаданных — не задача Bare. **Компрометация устройства.** История лежит на устройстве в открытом виде (IndexedDB). Доступ к устройству — доступ к истории. Защита устройства — зона ответственности пользователя и ОС. +**Утечка пароля.** В v1 пароль сменить нельзя (ADR-020). Узнавший пароль владеет аккаунтом — ключами, контактами, входящими сообщениями — до тех пор, пока владелец не заведёт новый аккаунт. + **Слабый пароль.** Пароль — материал ключа. Ключевой блоб хранится на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля. Гарантия «оператор не читает сообщения» действует в пределах стойкости пароля пользователя: слабый пароль — слабое E2EE. Это осознанная цена парольного мультидевайса. Контрмеры (ADR-013): PBKDF2-HMAC-SHA256 с не менее чем 600 000 итераций, пароль от 12 символов, рекомендация парольной фразы в UI. **Собеседник.** E2EE не защищает от участника чата: получатель может сохранить, переслать, сфотографировать. «Удалить у всех» после доставки не существует.