ADR-015…024 и спецификации v1: криптография, протокол, хранение, UI, деплой, план

Закрыты все открытые вопросы проектирования. Пароль не покидает клиент
(два ключа из мастера), TOFU для публичных ключей, устройства и конверт,
атомарный rekey комнат, регистрация и контакты, схема SQLite и драйвер
без cgo, сессии и CSRF, деплой через nginx+systemd, правила пушей,
айдентика «Скобы» на системном mono. Иконки PWA в web/icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
2026-08-22 10:53:24 +03:00
co-authored by Claude Fable 5
parent b9bf4ea04e
commit ae55c846aa
36 changed files with 1187 additions and 20 deletions
@@ -0,0 +1,26 @@
# ADR-017: Устройства, конверт сообщения и доставка
## Контекст
Очередь per-device (ADR-008) требует идентификации устройства. Формат конверта определяет, какие метаданные видит сервер, — это часть модели угроз, а не деталь реализации. Время сообщения: клиентский ULID (ADR-009) несёт часы клиента, которые врут.
## Решение
**Устройство.** Клиент при первом входе на устройстве генерирует `deviceId` — 16 случайных байт, base64url — и хранит его в IndexedDB. Регистрирует через `POST /api/devices`; идентификатор принадлежит аккаунту. Заголовок `X-Device` обязателен на запросах, где важно устройство: ACK, отправка (чтобы не возвращать эхо отправившему устройству), push-подписка; поток событий получает устройство в query — `EventSource` не умеет заголовки. Устройство, не появлявшееся 90 дней, удаляется вместе с очередью и подпиской.
**Конверт.** JSON, открытые поля: `id` (ULID, генерирует клиент), `to` (`{dm: nick}` или `{room: id}`), `from` (ставит сервер из сессии, клиентское значение игнорируется), `keyId` (`"dm"` для 1:1, идентификатор ключа для комнаты), `iv`, `ct`, `ts` (миллисекунды сервера). Внутри шифротекста — JSON `{t: текст}`. Открытые поля привязаны к шифротексту через AAD: `bare-msg-v1|id|chat|from|keyId`, где `chat``dm:a:b` (ники по возрастанию) или `room:id`.
**Часы.** Сервер принимает сообщение, только если метка времени в ULID отличается от серверных часов не больше чем на 5 минут; иначе `400 clock_skew` и клиент просит проверить часы. ULID присваивается в момент попытки отправки, не в момент набора: отложенное офлайном сообщение получает свежий идентификатор при повторе. Сортировка — по `id`, отображение времени — по `ts`.
**Доставка.** `POST /api/messages` в одной транзакции кладёт конверт в очередь каждого устройства каждого получателя (в 1:1 получатели — оба ника, в комнате — все участники), кроме устройства-отправителя. Подключённым по SSE устройствам конверт отправляется сразу. `POST /api/ack {ids}` удаляет конверты из очереди устройства. При подключении SSE сервер сначала отдаёт всю очередь устройства, затем `event: ready`, затем живые события. Каждые 20 секунд — комментарий-пинг.
**Идемпотентность.** Повторный `POST` с тем же `id` после ACK получателей породит повторную доставку; клиент сливает по `id` и дублей не показывает. Сервер не хранит историю идентификаторов — это противоречило бы ADR-008.
**Лимиты.** Текст — до 4000 символов, тело запроса — до 32 КиБ. Rate limiting — ADR-021.
## Следствия
- Серверу видны: кто, кому или в какую комнату, когда и какого размера. Ровно то, что модель угроз и так относит к метаданным.
- Отправитель получает своё сообщение на другие устройства тем же путём, что и получатели: мультидевайс без отдельной логики.
- Устройство определяется браузерным профилем: два браузера на одном телефоне — два устройства.
- Чистка IndexedDB браузером стирает `deviceId`; следующий вход создаёт новое устройство, старое отомрёт по сроку.