Сервер: комнаты, состав и завёрнутые ключи; смена состава и 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
74 lines
2.8 KiB
SQL
74 lines
2.8 KiB
SQL
-- Полная схема v1 (docs/storage.md, ADR-020). Время — миллисекунды Unix.
|
|
-- Таблицы этапов 2–3 создаются сразу: схема одна, миграция одна.
|
|
|
|
CREATE TABLE users (
|
|
nick TEXT PRIMARY KEY,
|
|
auth_hash BLOB NOT NULL, -- argon2id(authKey), 32 байта
|
|
auth_salt BLOB NOT NULL, -- 16 байт
|
|
auth_params TEXT NOT NULL, -- "argon2id,m=19456,t=2,p=1"
|
|
public_key TEXT NOT NULL, -- JWK, JSON
|
|
key_blob TEXT NOT NULL, -- непрозрачный JSON клиента
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE devices (
|
|
id TEXT PRIMARY KEY, -- base64url 16 байт, выдаёт клиент
|
|
nick TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen INTEGER NOT NULL,
|
|
push_subscription TEXT, -- JSON PushSubscription или NULL
|
|
push_pending INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX devices_nick ON devices(nick);
|
|
|
|
CREATE TABLE sessions (
|
|
token_hash BLOB PRIMARY KEY, -- SHA-256(токен)
|
|
nick TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
device_id TEXT REFERENCES devices(id) ON DELETE CASCADE, -- NULL до POST /api/devices
|
|
created_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL
|
|
);
|
|
CREATE INDEX sessions_nick ON sessions(nick);
|
|
|
|
CREATE TABLE contacts (
|
|
nick TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
peer TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (nick, peer)
|
|
);
|
|
|
|
CREATE TABLE rooms (
|
|
id TEXT PRIMARY KEY, -- base64url 16 байт, выдаёт клиент (ADR-037)
|
|
name TEXT NOT NULL,
|
|
owner TEXT NOT NULL REFERENCES users(nick),
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE room_members (
|
|
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
nick TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
joined_at INTEGER NOT NULL,
|
|
PRIMARY KEY (room_id, nick)
|
|
);
|
|
CREATE INDEX room_members_nick ON room_members(nick);
|
|
|
|
CREATE TABLE room_keys (
|
|
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
nick TEXT NOT NULL REFERENCES users(nick) ON DELETE CASCADE,
|
|
key_id TEXT NOT NULL,
|
|
sender TEXT NOT NULL, -- кто завернул
|
|
iv TEXT NOT NULL,
|
|
ct TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (room_id, nick, key_id)
|
|
);
|
|
|
|
CREATE TABLE queue (
|
|
device_id TEXT NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
msg_id TEXT NOT NULL,
|
|
envelope TEXT NOT NULL, -- готовый JSON Envelope
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (device_id, msg_id)
|
|
);
|
|
CREATE INDEX queue_created ON queue(created_at);
|