Files
bare/docs/decisions/040-trusted-key-returned-clears-pending.md
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.3 KiB

ADR-040: Возврат к доверенному ключу закрывает состояние pending

Уточняет ADR-016: у состояния «ключ изменился» появляется второй выход.

Контекст

ADR-016 знает одно состояние и один выход из него: ключ ника изменился, отправка блокируется до явного «доверять новому ключу». docs/ui.md даёт под это ровно одну кнопку.

Выход оказался не единственным возможным, а единственным записанным. Сервер, отдавший чужой ключ и вернувший обратно настоящий, оставляет клиент в тупике: в pending лежит ключ, которому доверять нельзя, а кнопка «доверять новому ключу» продвинула бы в основные именно его — то есть уже отозванную подмену. Отправка при этом заблокирована, и разблокировать её человеку нечем.

Клиент этапа 3 снимал pending сам, когда сервер снова отдавал доверенный ключ. Поведение верное, но в документах его не было, а CLAUDE.md и docs/plan.md запрещают дописывать спецификацию молча.

Решение

  • Публичный ключ, совпавший с доверенным, закрывает состояние pending: запись возвращается к прежнему ключу, полоса в чате и второй отпечаток в карточке контакта исчезают, отправка разблокируется.
  • Человеку об этом не сообщается: смены ключа не случилось, а состояние обещало ровно смену.
  • Кнопка «доверять новому ключу» остаётся единственным выходом там, где новый ключ никуда не делся.
  • Строка записана в docs/ui.md, «Карточка контакта».

Следствия

  • Тупика нет: из состояния выходит либо человек — подтверждением, либо сам сервер — возвратом к прежнему ключу.
  • Подмена, откатившаяся до того, как человек посмотрел на экран, следа в состоянии не оставляет. След остаётся в ленте: сообщение, зашифрованное подменным ключом, так и лежит нерасшифрованным с пометкой «ключ изменился» — расшифровать его нечем, ключа подменщика у нас нет и не будет.
  • Отдельного поля «здесь была подмена» не заводится: peers держит доверие, а не журнал. Журнал подмен — отдельное решение, если понадобится.