Закрыты все открытые вопросы проектирования. Пароль не покидает клиент (два ключа из мастера), 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
4.8 KiB
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; следующий вход создаёт новое устройство, старое отомрёт по сроку.