Этап 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:
@@ -181,6 +181,40 @@ export function removeContact(nick) {
|
||||
return request("DELETE", `/api/contacts/${encodeURIComponent(nick)}`);
|
||||
}
|
||||
|
||||
// --- комнаты -----------------------------------------------------------
|
||||
|
||||
// rooms — комнаты, где мы участники, каждая с нашим текущим завёрнутым
|
||||
// ключом (docs/protocol.md, «Комнаты»).
|
||||
export function rooms() {
|
||||
return request("GET", "/api/rooms");
|
||||
}
|
||||
|
||||
// createRoom заводит комнату. Идентификатор генерирует клиент: ключ
|
||||
// заворачивается до запроса и привязан к roomId (ADR-037). Занятый
|
||||
// идентификатор — 409 room_conflict, берётся новый.
|
||||
//
|
||||
// X-Device передаётся, чтобы это же устройство не получило комнату ещё
|
||||
// и событием: она приходит ответом (docs/protocol.md, «Комнаты»).
|
||||
export function createRoom(device, body) {
|
||||
return request("POST", "/api/rooms", body, { device });
|
||||
}
|
||||
|
||||
// changeMembers — смена состава и rekey одним запросом (ADR-018).
|
||||
export function changeMembers(id, body) {
|
||||
return request("POST", `/api/rooms/${encodeURIComponent(id)}/members`, body);
|
||||
}
|
||||
|
||||
// leaveRoom — выход из комнаты. X-Device передаётся по той же причине,
|
||||
// что и при создании: комната уходит из списка здесь же, а другим
|
||||
// устройствам вышедшего сервер шлёт room_left (ADR-041).
|
||||
export function leaveRoom(device, id) {
|
||||
return request("POST", `/api/rooms/${encodeURIComponent(id)}/leave`, undefined, { device });
|
||||
}
|
||||
|
||||
export function removeRoom(id) {
|
||||
return request("DELETE", `/api/rooms/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
// --- сообщения ---------------------------------------------------------
|
||||
|
||||
// sendMessage отдаёт конверт серверу; from и ts он поставит сам (ADR-017).
|
||||
@@ -208,6 +242,10 @@ export function ack(device, ids) {
|
||||
export function stream(device, handlers) {
|
||||
const source = new EventSource(`/api/events?device=${encodeURIComponent(device)}`);
|
||||
source.addEventListener("msg", (event) => handlers.msg(parse(event.data)));
|
||||
// room и room_left в очередь не кладутся: пропуск во время офлайна
|
||||
// чинится перечитыванием GET /api/rooms после ready (docs/protocol.md).
|
||||
source.addEventListener("room", (event) => handlers.room(parse(event.data)));
|
||||
source.addEventListener("room_left", (event) => handlers.roomLeft(parse(event.data)));
|
||||
source.addEventListener("ready", () => handlers.ready());
|
||||
source.addEventListener("error", () => handlers.error(source.readyState === EventSource.CLOSED));
|
||||
return () => source.close();
|
||||
|
||||
Reference in New Issue
Block a user