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 可用;不收敛就重跑扇出
◀ 上一步
下一步 ▶
↻ 重头来
键盘 ← / → 也能切;URL 自动保存当前步