Files
bare/docs/decisions/042-current-room-key-order.md
T
mayatnikovandClaude Opus 5 05586218e1 Этап 3: TOFU и комнаты — ключ комнаты, атомарный rekey, участники
Сервер: комнаты, состав и завёрнутые ключи; смена состава и rekey одним
атомарным запросом с проверкой keys[].to против итогового состава; обрезка
до двух последних ключей; передача владения по joined_at; события room
каждому со своим ключом и room_left удалённым; членство и keyId в проверках
сообщения; миграция 002.

Клиент: TOFU на всех путях, по которым публичный ключ доходит до клиента;
предупреждение о смене ключа с блокировкой отправки и повторной расшифровкой
сохранённого raw; заворачивание и разворачивание ключей комнаты, включая
себе — тем же кодом, без ветвления; расшифровка любым известным keyId;
экраны участников, создание комнаты, карточка контакта с двумя отпечатками.

ADR-037: roomId генерирует клиент. crypto.md вплетает roomId в заворачивание,
а оно происходит до запроса — создатель обязан привязать ключ к идентификатору,
которого по прежнему протоколу ещё не существовало.
ADR-039: завёрнутый ключ принимается только от участника. Иначе сервер
подставляет ключ, завёрнутый посторонним аккаунтом, TOFU молчит — ник
незнакомый, первый ключ запоминается молча, — и комната уезжает на ключ
сервера. Одно подменённое поле в ответе, без сговора и подмены кода.
ADR-041: долг по rekey — состояние комнаты, а не свойство события. Владелец,
офлайн в момент выхода участника, не узнавал о долге никогда, и комната
навсегда оставалась на ключе, который вышедший знает.
ADR-038, 040, 042, 043, 044: тексты экранов комнат, снятие pending при
возврате прежнего ключа, порядок ключей, форма раньше прав, экран покинутой
комнаты.

Приёмка на боевом: комната на троих, добавленный четвёртый читает только
новое, вышедший после rekey новых не получает и писать не может,
keys_mismatch, key_exists, not_owner, owner, room_conflict, чужой X-Device.
Подмена public_key в базе даёт у собеседника предупреждение и блокирует
отправку — проверено в Chrome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-22 22:18:13 +03:00

3.6 KiB
Raw Blame History

ADR-042: Порядок ключей комнаты и «текущий ключ»

Уточняет ADR-018: «текущий ключ — последний полученный в порядке сервера».

Контекст

На однозначности «последнего» держится обрезка: сервер хранит два последних keyId комнаты (ADR-018) и обязан не выбросить ничей действующий ключ. docs/storage.md определял его одной строкой — «строка room_keys с максимальным created_at», — а два rekey подряд укладываются в одну миллисекунду, и максимум становится неоднозначным.

Код этапа 3 это починил: сервер поднимает время нового ключа до последний + 1, а при равенстве доопределяет порядок по key_id; клиент делает то же со своим receivedAt. Инвариант несущий, а записан был только комментариями в коде.

Второе: порядка сервера клиент не знает и знать не может. В Room.key приходит один текущий ключ без номера и без времени, так что клиент считает текущим тот, который получил последним. В гонке двух rekey с разных устройств владельца эти порядки расходятся: устройство, чей ответ пришёл раньше события соседнего, считает текущим свой ключ, а сервер — чужой.

Решение

  • Инвариант записывается в docs/storage.md: время записи room_keys строго больше времени всех прежних ключей той же комнаты; при равенстве порядок доопределяется по key_id. То же — про клиентский roomKeys.receivedAt.
  • Клиент держит порядок получения, а не порядок сервера. Это осознанный предел: номера ключа в протоколе нет и не заводится.
  • Расхождение безвредно, пока оба ключа живы, а живы они, пока комната держит два последних keyId. Сообщение, отправленное ключом, который сервер уже обрезал, получает 400 unknown_key и хоронится как failed (ADR-033).

Следствия

  • Обрезка до двух keyId не выбрасывает ничей текущий ключ: самый свежий key_id есть у каждого участника (иначе keys_mismatch), и он остаётся всегда.
  • created_at в room_keys перестаёт быть в точности «миллисекундами Unix»: у двух rekey в одну миллисекунду второе время сдвинуто вперёд. Это записано рядом с колонкой.
  • Порядковый номер ключа в Room.key — возможное расширение отдельным ADR, если расхождение порядков когда-нибудь окажется дорогим.