Этап 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:
@@ -22,11 +22,12 @@ func TestMigrateAndRestart(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bare.db")
|
||||
|
||||
first := open(t, path)
|
||||
if got := first.Applied(); len(got) != 1 || got[0] != "001_init.sql" {
|
||||
t.Fatalf("применённые миграции: получено %v, ожидалось [001_init.sql]", got)
|
||||
want := []string{"001_init.sql", "002_room_needs_rekey.sql"}
|
||||
if got := first.Applied(); !equal(got, want) {
|
||||
t.Fatalf("применённые миграции: получено %v, ожидалось %v", got, want)
|
||||
}
|
||||
if got := version(t, first); got != 1 {
|
||||
t.Errorf("user_version: получено %d, ожидалась 1", got)
|
||||
if got := version(t, first); got != len(want) {
|
||||
t.Errorf("user_version: получено %d, ожидалась %d", got, len(want))
|
||||
}
|
||||
// Все восемь таблиц из docs/storage.md на месте.
|
||||
for _, table := range []string{"users", "devices", "sessions", "contacts", "rooms", "room_members", "room_keys", "queue"} {
|
||||
@@ -52,8 +53,8 @@ func TestMigrateAndRestart(t *testing.T) {
|
||||
if got := second.Applied(); len(got) != 0 {
|
||||
t.Errorf("повторный старт применил %v, ожидалось ничего", got)
|
||||
}
|
||||
if got := version(t, second); got != 1 {
|
||||
t.Errorf("user_version после перезапуска: получено %d, ожидалась 1", got)
|
||||
if got := version(t, second); got != len(want) {
|
||||
t.Errorf("user_version после перезапуска: получено %d, ожидалась %d", got, len(want))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +125,7 @@ func TestUsersAndSessions(t *testing.T) {
|
||||
}
|
||||
|
||||
// Удаление пользователя уносит сессии каскадом.
|
||||
if err := s.DeleteUser(ctx, "marta"); err != nil {
|
||||
if _, err := s.DeleteUser(ctx, "marta"); err != nil {
|
||||
t.Fatalf("DeleteUser: %v", err)
|
||||
}
|
||||
if _, err := s.Session(ctx, live, now); !errors.Is(err, ErrNotFound) {
|
||||
|
||||
Reference in New Issue
Block a user