Лимиты: все четыре правила ADR-021 — регистрация 5/час на IP, вход 10/10 мин на IP и ник, сообщения 30/мин, прочие изменяющие 60/мин; 429 с Retry-After; X-Real-IP читается только с loopback, иначе адрес соединения — иначе заголовок отменял бы лимит на IP; карты вёдер ограничены поколениями. Аудит нашёл то, что пропустили пять раундов ревью: ADR-056: nginx вёл access_log с IP и полными путями вопреки обещанию deploy.md. Ники и социальный граф ложились в /var/log/nginx рядом с чистым журналом bare. ADR-058: «выйти на других устройствах» не обрывал уже открытый SSE — отозванная сессия продолжала получать сообщения. ADR-059: промежуточный ключ комнаты был невосстановим. Участник, пропустивший офлайн два rekey подряд, навсегда не расшифровал бы сообщения среднего ключа — вопреки обещанию storage.md о повторной попытке после получения keyId. ADR-063: ACK уходил по одному на конверт, а не пачкой. Получатель в оживлённой комнате выедал общее ведро подтверждениями и упирался в 429 на всех изменяющих запросах, включая выход из комнаты: 116 отказов за прогон стало нулём. ADR-055, 057, 060, 061, 062: ключи вёдер и границы, +dirty у bare version, 403 unknown_device не хоронит сообщение, усечение имени в подсказке ввода, kdf как оракул после повышения цели KDF. Модель угроз пополнена тем, что действительно видит оператор: push-подписки лежат в базе открытым текстом, и вместе с VAPID-ключом с той же машины это произвольное уведомление на экране блокировки. README приведён к v1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
27 lines
4.2 KiB
Markdown
27 lines
4.2 KiB
Markdown
# ADR-059: участник получает все удерживаемые ключи комнаты, а не только текущий
|
||
|
||
Уточняет [ADR-018](018-rooms-membership-rekey.md) и [ADR-042](042-current-room-key-order.md): сервер держит два последних `keyId` — значит, и раздаёт два.
|
||
|
||
## Контекст
|
||
|
||
`GET /api/rooms` и событие `room` отдавали участнику ровно один ключ — текущий. Других источников ключа у клиента нет: запросить конкретный `keyId` протокол не умеет.
|
||
|
||
Этого хватает на одну смену ключа и не хватает на две. Комната `{владелец, D, E}`, устройство D офлайн. Владелец добавляет участника — rekey `K1`; кто-то пишет сообщение ключом `K1`; владелец убирает участника — rekey `K2`. События `room` в очередь не кладутся (`docs/protocol.md`, «События»), поэтому `K1` до D не дошёл, а конверт с `keyId = K1` лежит в его очереди и дождётся подключения. D возвращается, забирает конверт и спрашивает `GET /api/rooms` — там `K2`. `K1` на сервере есть (обрезка держит два последних), но не отдаётся никому и никогда.
|
||
|
||
Сообщение остаётся `undecryptable: "unknown_key"` навсегда, а `docs/storage.md` обещает обратное: «Нерасшифрованное сообщение хранит `raw` для повторной попытки после … получения недостающего `keyId`». Получить его было нечем. Конфиденциальность цела — это потеря читаемости у законного участника.
|
||
|
||
## Решение
|
||
|
||
- Поле `key` типа `Room` заменяется на `keys` — список завёрнутых для запрашивающего ключей комнаты, от старого к новому. Порядок — время записи и `key_id` при равенстве, тот же, что у обрезки (ADR-042).
|
||
- `GET /api/rooms` отдаёт все ключи, которые сервер ещё держит (до двух, ADR-018). Событие `room` и ответы `POST /api/rooms` и `POST /api/rooms/{id}/members` несут один — только что розданный: подключённому устройству остальные уже приходили, а отключённое доберёт их из `GET /api/rooms` после `ready`.
|
||
- Клиент сохраняет ключи по порядку: текущим у него остаётся последний полученный (ADR-042), поэтому исходящее по-прежнему шифруется свежим ключом.
|
||
- Отдельного эндпоинта «ключ по (roomId, keyId)» не заводится: он был бы четвёртым способом получить то же самое.
|
||
- Правятся `docs/protocol.md` («Типы», «Комнаты», «События») и `docs/storage.md`.
|
||
|
||
## Следствия
|
||
|
||
- Сообщение, отправленное между двумя rekey, читается участником, который в это время был офлайн. Ради этого сервер и держал два ключа.
|
||
- Три смены ключа за время офлайна по-прежнему теряют средний: сервер держит два последних `keyId`, а не всю историю. Это прежняя цена ADR-018, и она записана.
|
||
- Сервер не узнаёт о ключах ничего нового: он и раньше хранил обе записи и раздавал одну из них.
|
||
- Ответ `GET /api/rooms` вырастает на один завёрнутый ключ на комнату. Это десятки байт.
|