Этап 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
This commit is contained in:
@@ -151,6 +151,36 @@ func TestForeignDevice(t *testing.T) {
|
||||
// Со своим устройством — обычная отправка.
|
||||
expect(t, e.do(http.MethodPost, "/api/messages", message(ulid(nowMillis(), 4), "marta"),
|
||||
with(petya), withDevice(petyaDevice)), http.StatusAccepted, "")
|
||||
|
||||
// Комнаты: заголовок здесь необязателен — он всего лишь просит не слать
|
||||
// событие отправившему устройству, — но принадлежность проверяется
|
||||
// та же (docs/protocol.md, «Общие правила», «Комнаты»).
|
||||
room := map[string]any{
|
||||
"id": roomIDOf(40),
|
||||
"name": "общая",
|
||||
"keyId": keyID(40),
|
||||
"keys": keysFor([]string{"petya"}, 40),
|
||||
}
|
||||
expect(t, e.do(http.MethodPost, "/api/rooms", room, with(petya), withDevice(martaDevice)),
|
||||
http.StatusForbidden, "unknown_device")
|
||||
expect(t, e.do(http.MethodPost, "/api/rooms", room, with(petya), withDevice("мусор")),
|
||||
http.StatusForbidden, "unknown_device")
|
||||
expect(t, e.do(http.MethodPost, "/api/rooms", room, with(petya), withDevice(deviceOf(9))),
|
||||
http.StatusForbidden, "unknown_device")
|
||||
// Отказ ничего не создал: идентификатор комнаты свободен.
|
||||
expect(t, e.do(http.MethodPost, "/api/rooms", room, with(petya)), http.StatusCreated, "")
|
||||
|
||||
leave := "/api/rooms/" + roomIDOf(40) + "/leave"
|
||||
expect(t, e.do(http.MethodPost, leave, nil, with(petya), withDevice(martaDevice)),
|
||||
http.StatusForbidden, "unknown_device")
|
||||
expect(t, e.do(http.MethodPost, leave, nil, with(petya), withDevice("мусор")),
|
||||
http.StatusForbidden, "unknown_device")
|
||||
// Отказ ничего не изменил: из комнаты никто не вышел.
|
||||
if got := e.room(petya, roomIDOf(40)); got == nil {
|
||||
t.Fatal("комната пропала после отказа по устройству")
|
||||
}
|
||||
expect(t, e.do(http.MethodPost, leave, nil, with(petya), withDevice(petyaDevice)),
|
||||
http.StatusNoContent, "")
|
||||
}
|
||||
|
||||
// Удаление устройства уносит очередь и сессии устройства.
|
||||
|
||||
Reference in New Issue
Block a user