Этап 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:
+20
-4
@@ -26,6 +26,7 @@ import { DESKTOP, clear, wide } from "./ui/dom.js";
|
||||
import { renderAuth } from "./ui/auth.js";
|
||||
import { renderChat } from "./ui/chat.js";
|
||||
import { renderContact } from "./ui/contact.js";
|
||||
import { renderMembers } from "./ui/members.js";
|
||||
import { renderNew } from "./ui/new.js";
|
||||
import { renderSettings } from "./ui/settings.js";
|
||||
import { frame } from "./ui/shell.js";
|
||||
@@ -69,9 +70,11 @@ const ctx = {
|
||||
|
||||
// --- роутинг -----------------------------------------------------------
|
||||
|
||||
// route разбирает hash. Маршруты — docs/ui.md, «Каркас»; комнаты придут
|
||||
// на этапе 3, до тех пор `#/room/…` — неизвестный путь и ведёт в список.
|
||||
// route разбирает hash. Маршруты — docs/ui.md, «Каркас». Ник — форма
|
||||
// ADR-019, идентификатор комнаты — 16 случайных байт base64url
|
||||
// (docs/crypto.md, «Идентификаторы»). Всё, что не разобралось, — список.
|
||||
const NICK_ROUTE = /^#\/(dm|contact)\/([a-z0-9_]{2,32})$/;
|
||||
const ROOM_ROUTE = /^#\/room\/([A-Za-z0-9_-]{22})(\/members)?$/;
|
||||
|
||||
function route() {
|
||||
const hash = location.hash || "#/";
|
||||
@@ -85,6 +88,10 @@ function route() {
|
||||
if (nick) {
|
||||
return { kind: nick[1], nick: nick[2] };
|
||||
}
|
||||
const room = ROOM_ROUTE.exec(hash);
|
||||
if (room) {
|
||||
return { kind: room[2] ? "members" : "room", roomId: room[1] };
|
||||
}
|
||||
return { kind: "root" };
|
||||
}
|
||||
|
||||
@@ -101,7 +108,14 @@ async function render() {
|
||||
}
|
||||
const where = route();
|
||||
// На десктопе `#/` показывает первый чат — тот, что вверху списка.
|
||||
let chatId = where.kind === "dm" ? sync.dmChatId(where.nick) : null;
|
||||
// У экранов «карточка контакта» и «участники» открытого чата нет:
|
||||
// в сайдбаре не выделен никто.
|
||||
let chatId = null;
|
||||
if (where.kind === "dm") {
|
||||
chatId = sync.dmChatId(where.nick);
|
||||
} else if (where.kind === "room") {
|
||||
chatId = sync.roomChatId(where.roomId);
|
||||
}
|
||||
if (where.kind === "root" && wide()) {
|
||||
const list = await sync.chats().catch(() => []);
|
||||
if (mine !== state.paint) {
|
||||
@@ -123,7 +137,9 @@ async function render() {
|
||||
} else if (where.kind === "new") {
|
||||
renderNew(main, ctx);
|
||||
} else if (where.kind === "contact") {
|
||||
renderContact(main, ctx, where.nick);
|
||||
parts.push(renderContact(main, ctx, where.nick));
|
||||
} else if (where.kind === "members") {
|
||||
parts.push(renderMembers(main, ctx, where.roomId));
|
||||
} else if (chatId !== null) {
|
||||
parts.push(renderChat(main, ctx, chatId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user