Этап 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:
+26
-10
@@ -74,14 +74,23 @@ func (s *server) sendMessage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if in.To.Room != "" {
|
||||
// Комнаты — этап 3. Участников нет ни у одной комнаты, потому что
|
||||
// нет и самих комнат: единственный возможный ответ — not_member.
|
||||
Error(w, http.StatusForbidden, "not_member", "вы не участник комнаты")
|
||||
return
|
||||
}
|
||||
sess, _ := auth.From(r)
|
||||
if _, ok := s.peer(w, r, in.To.DM, sess.Nick); !ok {
|
||||
room := in.To.Room != ""
|
||||
if room {
|
||||
member, knownKey, err := s.st.RoomAccess(r.Context(), in.To.Room, sess.Nick, in.KeyID)
|
||||
if err != nil {
|
||||
s.internal(w, r, err)
|
||||
return
|
||||
}
|
||||
if !member {
|
||||
Error(w, http.StatusForbidden, "not_member", "вы не участник комнаты")
|
||||
return
|
||||
}
|
||||
if !knownKey {
|
||||
Error(w, http.StatusBadRequest, "unknown_key", "у комнаты нет такого ключа")
|
||||
return
|
||||
}
|
||||
} else if _, ok := s.peer(w, r, in.To.DM, sess.Nick); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,7 +101,7 @@ func (s *server) sendMessage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
env := envelope{
|
||||
ID: in.ID,
|
||||
To: target{DM: in.To.DM},
|
||||
To: target{DM: in.To.DM, Room: in.To.Room},
|
||||
From: sess.Nick,
|
||||
KeyID: in.KeyID,
|
||||
IV: in.IV,
|
||||
@@ -104,14 +113,21 @@ func (s *server) sendMessage(w http.ResponseWriter, r *http.Request) {
|
||||
s.internal(w, r, err)
|
||||
return
|
||||
}
|
||||
devices, err := s.st.DeliverDM(r.Context(), store.Delivery{
|
||||
delivery := store.Delivery{
|
||||
From: env.From,
|
||||
To: env.To.DM,
|
||||
Room: env.To.Room,
|
||||
Exclude: device,
|
||||
MsgID: env.ID,
|
||||
Envelope: string(raw),
|
||||
Now: env.TS,
|
||||
})
|
||||
}
|
||||
var devices []string
|
||||
if room {
|
||||
devices, err = s.st.DeliverRoom(r.Context(), delivery)
|
||||
} else {
|
||||
devices, err = s.st.DeliverDM(r.Context(), delivery)
|
||||
}
|
||||
if err != nil {
|
||||
s.internal(w, r, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user