CH1

多设备问题

Step 1 / 13
📱 Alice phone 本机 = 发起方 📱 Bob phone device_id=1 📲 Bob iPad device_id=2 💻 Bob laptop device_id=3 "发给 Bob" 该走哪一台? alice/1 bob/1 bob/2 bob/3 ProtocolAddress = (name, device_id) 每台设备独立 IK + SPK + OTPK ⚠️ Double Ratchet 的端点假设 DR 协议本身只在"两个端点"之间 ping-pong: (Alice 的一对 IK) ↔ (Bob 的一对 IK) 现实里 Bob 有 N 台设备 → N 对独立的 IK "发给 Bob"这句话在协议层面无法直接表达 ProtocolAddress · 寻址原子 struct ProtocolAddress { name: String, device_id: DeviceId } name 通常 = ServiceId 字符串(账号 UUID) DeviceId = NonZeroU8 → 单账号上限 255 台设备 规范来源:Sesame §2 Terms + libsignal address.rs DR 端点 = 一台设备;多设备时一个"Bob"对应多个端点 每台设备有独立的 (name, device_id),独立的 IK + SPK + OTPK 📱 Alice phone alice/1 📱 Bob phone bob/1 📲 Bob iPad bob/2 💻 Bob laptop bob/3 SessionRecord → bob/1 SessionRecord → bob/2 SessionRecord → bob/3 DR session 黑盒带 · 每对 (UserID, DeviceID) 一份 UserRecords · 会话簿 bob_uuid { 1, 2, 3 } alice_uuid { 1 } DeviceRecord 内含: • 1 个 active session • N 个 inactive sessions(保留收延迟消息) active inactive (2) active inactive (1) active inactive (0) active 用于加密;inactive 仅供试解延迟消息 SessionRecord · 黑盒接口 对外只暴露两个动词 message_encrypt(addr, ptext) message_decrypt_*(msg, addr) 黑盒里有什么 root key / sending CK / receiving CK DH 公私钥 / 消息序号 / skipped MK → 详见 beginner 教程 CH3-CH4 一对 (UserID, DeviceID) → 一份 SessionRecord(内含 current DR state) 两层嵌套 UserRecord { user_id: ServiceId, devices: HashMap<DeviceId, DeviceRecord>, } DeviceRecord { active + inactive sessions, registration_id Sesame 规范的两层映射:libsignal 不提供,应用层维护 active / inactive 三规则 ① 新 session 上位 对端重装 / 重新协商 → 新 session 成 active 旧 active 降为 inactive 摞顶 ② inactive 命中 收到旧链上的延迟消息 → 解密成功 该 inactive 立即 promote 为 active ③ inactive 上限 数量 / 时限 由 SessionRecord 内部封装 active 用于加密;inactive 仅用于 trial-decrypt 收延迟消息 📱 Alice phone alice/1 📱 Bob phone bob/1 📲 Bob iPad bob/2 💻 Bob laptop bob/3 Sesame 控制层 · fan-out 枚举 recipient 全部 device 每个 active session 跑一次 encrypt for d in book.devices_of(bob) message_encrypt(...) → 输出 N 个独立密文 plaintext ciphertext_1 ciphertext_2 ciphertext_3 ☁️ Server 只看信封,不看明文 OutEnvelope · 投递结构 (device_id, registration_id, ct) 📲 Alice iPad alice/2 self-copy recipient set {bob, self} − {self.cur} 为什么不能广播一份密文 每个 SessionRecord 对应一对独立的 (root key, sending CK) 同一明文喂三条链 → 三个 MK 各异 → 三份独立密文,互相之间无法解 DR 的端点单位是设备,不是用户 一明文 → N 份独立加密的密文,每个 session 独立 服务器看到 ✓ • device_id(路由用) • registration_id(检测重装) • 密文长度 / 时间戳 • 谁在跟谁聊 服务器看不到 ✕ • 任何明文 • N 份密文之间的关联 • Session 内部状态 • MK / RK / CK 服务器只看信封(device_id + registration_id),看不到明文 multi-device fan-out · 收件集合算法 recipients = ∪{ recipient, sender_user } for u in recipients: for d in book.devices_of(u): if (u, d) == (sender_user, sender_dev): skip encrypt(addr=(u, d), ptext) → envelope 扇出循环再套一层:收件集合 = {recipient, self},排除自己的当前 device_id 📱 Alice phone recipient active (旧) previous_session_states ↓ inactive · candidate A inactive · candidate B inactive · candidate C SessionRecord (sender) 📱 Bob phone sender · bob/1 ☁️ Server forward ciphertext (delayed) ✓ candidate B 解密成功 promote demote trial-decrypt 顺序 current → previous_session_states[..] 命中后 promote 命中的为 active;旧 active 降为 inactive head 不匹配 → 触发 stale 流程(CH5 Step 12) SessionState 转移 old active demote inactive candidate promote new active 每次成功解密都跑这一轮 trial-decrypt: current → previous;命中后 promote 为 active 📱 Alice phone alice/1 📱 Bob phone bob/1 📲 Bob iPad bob/2 💻 Bob laptop bob/3 UserRecords · Address book bob { 1, 2, 3 } alice { 1 } 💻 Bob laptop 2 bob/4 · 未知 device linking · provisioning 在 App 层 ☁️ Server 目录服务 fetch prekey bundle (bob/4) 💻 Bob laptop 2 bob/4 { 1, 2, 3, 4 } 📲 Bob iPad ⚠️ stale ⚠️ MAXLATENCY stale 不立即删 SessionNotFound / UntrustedIdentity 触发 保留至 MAXLATENCY 收延迟消息;超时后清 下一次发送时 fan-out 自动重建 📲 Bob iPad 已移除 { 1, 3, 4 } converged device linking · App 层 provisioning(libsignal 不提供) primary 显示 QR 码 new device 扫码 + 生成 IK ☁️ server 注册 device_id 上传 prekey bundle IK + SPK + OTPK 池 device linking 是 App 层 provisioning(QR 码 + 服务端配对),不在 libsignal PreKeyBundle 字段 registration_id: u32 device_id: DeviceId pre_key: Option<OTPK> signed_pre_key: SignedKey kyber_pre_key, identity_key, signature 首次发送前 process_prekey_bundle 建立初始 DR session SignalProtocolError 触发条件 SessionNotFound 本地无该 (user, device) session → schedule_prekey_fetch + mark_stale UntrustedIdentity 对端 IK 变了(重装 / 换设备) → 显示 safety number 变更提示 DuplicateMessage 收到已解过的旧密文 → 丢弃 stale 不立即删,保留 MAXLATENCY 接收延迟消息;超时后清 最终一致性 · convergence loop 收敛条件 = 本地 device set == 服务端 device set 且全部 active 可用 不收敛 → 应用 stale diff → 重跑 fan-out 扇出失败 → discard 当前批次防"半更新" → 整批重试 兜底 = 重试 + MAXLATENCY 超时清理 双保险 收敛条件 = 本地 set == 服务端 set 且 active 可用;不收敛就重跑扇出