CH1
为什么要加密
Step 1 / 25
A
Alice
B
Bob
"Hi Bob! 转账 1000"
E
Eve · 偷听者
"Hi Bob! 转账 1000" 👀
🔑 K
🔑 K
密文(用 K 加密)
x9F2cA7e1b3...
???? 看不懂
网络上传的内容,谁都能监听
双方共享一把密钥 K,加密后 Eve 看到的是乱码
A
Alice
B
Bob
🔑 K
❓ 怎么把 K 传给 Bob?
直接发,Eve 也会截到
Alice 的密钥对
🔒 私钥 a
(本地)
📢 公钥 A = g^a
(可公开)
Bob 的密钥对
🔒 私钥 b
(本地)
📢 公钥 B = g^b
(可公开)
Alice 收到 B
Bob 收到 A
ECDH · 椭圆曲线 Diffie-Hellman
Alice: a × B = ab
Bob: b × A = ab
双方算出同一个 ab ✓ Eve 只看到 A 和 B,算不出
(在椭圆曲线上 × 是标量乘法,但你可以先当成普通乘法理解)
来自 ECDH 的共享秘密
ab
KDF
🔑 对称密钥 K
用于 AES-GCM / ChaCha20-Poly1305
用 K 加密第 1 条消息 M₁
Enc(K, M₁) = c₁
⚠️ 但还有问题
如果 Alice 和 Bob 每次都用同一对 (a, A) (b, B),那 ab 永远不变
私钥某天泄漏 → 攻击者算出 ab → 历史所有密文一夜沦陷
下一章引入 KDF 链解决"每条消息都用不同密钥"的问题
问题:钥匙怎么传给对方?
现代密码学:每人一对密钥,公钥发出去,私钥永不出门
双方在网络上只交换公钥,却算出同一个秘密
用 ECDH 出来的秘密当密钥加密 —— 但还有问题
🔑 K
同一把
💣
M₁ 密文 c₁
M₂ 密文 c₂
M₃ 密文 c₃
💀
K 一泄漏
→ M₁ M₂ M₃ 全暴露
想要:每条消息一把不同的密钥;并且即使现在的密钥泄漏,过去的消息仍安全。
输入 CK
KDF
密钥派生函数
通常是 HMAC-SHA256
单向 · 不可逆
输出无法反推输入
新 CK · 给下一轮用
MK · 这条消息的密钥
把『链密钥 CK』喂进 KDF,吐出『下一个 CK』和『这条消息的 MK』。
CK 用于继续往下推;MK 用一次就丢。
CK₁
CK₂
CK₃
CK₄
…
KDF
KDF
KDF
KDF
MK₁
加密 M₁
MK₂
加密 M₂
MK₃
加密 M₃
MK₄
加密 M₄
Symmetric Ratchet · 对称棘轮
每发一条消息推进一格,每条消息用全新的 MK
CK 一直往下推(绿色);每格岔出一个一次性的 MK(粉色)用完即丢
攻击者在此偷到 CK₃
💀
CK₃ ⚠️
MK₁ ✓
安全
MK₂ ✓
安全
✅ 前向保密 (Forward Secrecy)
由于 KDF 不可逆,攻击者无法从 CK₃ 倒推回 CK₁、CK₂,自然也算不出 MK₁、MK₂
"现在被入侵,过去仍安全" —— FS 是 Signal 的核心安全保证之一
用同一把 K 加密 N 条消息:一旦泄漏,全部暴露
KDF 是一个『单向搅拌机』:能往前推,不能往回算
把 KDF 串成链,每条消息有专属的 MK
即使现在的 CK 被偷,过去的 MK 仍安全 —— 这就是『前向保密 FS』
攻击者偷到 CK₃
💀
CK₁ ✓
CK₂ ✓
CK₃ ⚠️
CK₄ ✗
CK₅ ✗
❌ 缺少『后向恢复 PCS』
KDF 链是确定的:偷到 CK₃ 后,用 KDF 一直算下去就能得到所有未来的 CK
攻击者一次入侵后,未来所有消息也持续被破。需要新办法。
CK_old
🎲 新的 DH
引入新的随机性
root key 更新
RK = KDF(RK, DH)
新 CK ✨
全新出发
每隔一段时间做一次新 DH,注入新的随机性,让 root key 重新派生
攻击者即使偷到旧 CK,也无法预测注入的新 DH 输出
✅ 几次新 DH 后,攻击者再也跟不上 —— 这就是 PCS
Alice
Bob
M₁ + Alice 新公钥 A₁
M₂ + Bob 新公钥 B₁(基于 A₁ 派生 RK)
M₃ + Alice 新公钥 A₂(基于 B₁ 派生 RK)
…如此交替进行(ping-pong)
每次发消息时附上自己的新公钥;收到对方新公钥后再派生新 RK 和新 CK。
不断重新协商,是『DH ratchet · 棘轮』里『棘』的来源 —— 单向不可回滚。
DH 输出
新一轮交换的结果
当前 RK
root key(主时间线)
KDF
把新 DH 和旧 RK 搅在一起
新 RK
下次 DH 时再喂进去
新 CK
送进 KDF 链开始派生 MK(回到 Ch3)
RK 是主时间线,永远只往前走。每次新 DH 派生出新 RK 和一条全新的 CK 链。
root chain
RK₁
RK₂
RK₃
RK₄
RK₅
RK₆
sending chain
(Alice 发消息时推进)
receiving chain
(收到对方消息时推进)
三条链同时进行:RK 主线每次 DH 推进一格,并派生出一条新的 sending 或 receiving 链;
每条 CK 链上每发/收一条消息推进一格(symmetric ratchet)。这就是『Double Ratchet · 双棘轮』。
KDF 链是确定的:偷到一格,未来全暴露
解法:每隔一段注入一次新 DH,让 root key 重新派生
每条消息都带新公钥,双方交替更新(ping-pong)
DH 输出 + 当前 RK → KDF → 新 RK + 新 CK
完整 Double Ratchet:root + sending + receiving 三条链同时演化
Bob (离线)
B
Server
📦 公钥仓库
Alice
A
B
Bob
📴 离线(睡觉中)
A
Alice
想现在发消息
❓ Bob 不在线,怎么做 DH?
之前所有 DH 都假设双方同时在线交换公钥。
现实里 Bob 可能离线几天 —— Alice 不能等。
IK_B · 身份密钥(长期)
SPK_B · 签名预共享(中期)
上传(提前)
📦 IK_B
📦 SPK_B(带 Bob 签名)
Bob 提前把『身份密钥 IK』和『签名预共享密钥 SPK』传到服务器。
IK 几乎永久不变(代表身份);SPK 定期轮换(一般一周/月)并由 IK 签名。
IK_B
SPK_B
OPK 池(一次性)
OPK₁
OPK₂
OPK₃
OPK₄
OPK₅
…
批量上传
📦 IK_B
📦 SPK_B
📦 OPK 池
OPK₁
OPK₂
OPK₃
OPK₄
OPK₅
…
Bob 还会预生成一池『一次性密钥 OPK』批量上传。
每个 OPK 只用一次,用完即弃。池子快空了 Bob 再补充。
📦 IK_B
📦 SPK_B
📦 OPK₂(即将原子领走)
OPK₁ OPK₃ OPK₄ …
"取 Bob 的密钥包"
返回 (IK_B, SPK_B, OPK₂)
Alice 收到 Bob 的
IK_B
SPK_B(验证签名)
OPK₂(只此一份)
+自己有 IK_A、EK_A
Alice 一次性取走 Bob 的『三件套』。服务器原子地把 OPK₂ 从池里删掉,
下次别人来要 Bob 的包,会拿到 OPK₁ 或 OPK₃,永远不会重复。
Alice 拥有
自己: IK_A, EK_A (一次性短暂)
Bob: IK_B, SPK_B, OPK_B
→ 共 5 把公钥参与运算
DH₁ = IK_A × SPK_B
绑定 Alice 身份 ↔ Bob 中期密钥
DH₂ = EK_A × IK_B
绑定 Alice 临时密钥 ↔ Bob 身份
DH₃ = EK_A × SPK_B
提供新鲜性(每次新 EK)
DH₄ = EK_A × OPK_B
提供后向恢复(一次性 OPK)
为什么要 4 个 DH?让攻击者必须同时拿到 Alice 的 IK 和 EK,以及 Bob 的 IK/SPK/OPK 才能破。
任何一个密钥泄漏都不足以打破整个 X3DH —— 这叫"密钥分摊安全"。
DH₁
DH₂
DH₃
DH₄
‖ 拼接
KDF
HMAC-based
SharedSecret
X3DH 的产出
32 bytes 随机数
Double Ratchet
RK₀ · 初始 root key
交给 Ch4 接着跑
4 个 DH 拼接喂进 KDF,得到 SharedSecret —— 这就是 Double Ratchet 的初始 root key。
从这一刻起,Alice 和 Bob 进入 Ch4 描述的 ping-pong,每条消息都有专属 MK。
挑战:Bob 离线,Alice 想现在就发,怎么协商密钥?
Bob 提前把 IK + SPK 上传到 Server
再批量上传一池 OPK(一次性,用完即弃)
Alice 一次性取走『三件套』,Server 原子删除 OPK
Alice 用自己的 (IK_A, EK_A) 和 Bob 的三件套做 4 个 DH
4 个 DH 输出 → KDF → SharedSecret → 接进 Double Ratchet 当 RK₀
Bob
Server
Alice
① 上传 IK + SPK + OPK 池
② 请求 Bob 的密钥包
③ 返回 (IK_B, SPK_B, OPK_B) · OPK 出队
④ X3DH → SharedSec → RK₀
⑤ 第 1 条消息 + Alice 公钥(密文)
⑥ 暂存,等 Bob 上线后转发
📱 Bob 上线 · 解密第 1 条
⑦ 回复 + Bob 新公钥(Double Ratchet ping-pong 开始)
…此后每条消息都附带新公钥,root chain 持续推进,每条消息有专属 MK
数据 / 密钥
Alice 看到
Server 看到
Bob 看到
IK / SPK / OPK 公钥
✓
✓
✓ (自有)
私钥 IK_A / EK_A / IK_B…
部分 (自己的)
✗
部分 (自己的)
SharedSecret · 4 DH 输出
✓
✗
✓
RK / CK / MK
✓
✗
✓
每条消息的明文
✓
✗
✓
每条消息的密文
✓
✓ (中转)
✓
谁在跟谁通信(元数据)
✓
✓ (能看到)
✓
Server 能转发密文、保存公钥包、看到通信元数据;但永远看不到明文和任何会话密钥。
这是 E2E (端到端加密) 的边界。元数据隐藏需要其他协议(如 Sealed Sender)解决。
时间
→
💀 攻击者偷到密钥
FS 区域 · 过去仍安全
短暂受损
PCS 区域 · 几次新 DH 后重新安全
KDF 单向性保证:偷到现在算不回过去
未做新 DH 之前
每次新 DH 注入新随机性,把攻击者甩开
前向保密 Forward Secrecy
"现在被入侵,过去仍安全"
由 KDF 单向链保证(Ch3 step 10)
后向恢复 Post-Compromise Security
"现在被入侵,未来还能恢复安全"
由 DH ratchet 注入新随机性保证(Ch4 step 12)
🎉 关键密钥速查
IK · 身份密钥
长期 · 几乎不变
代表你这个人;用于 X3DH 绑定身份
SPK · 签名预共享
中期 · 周/月轮换
由 IK 签名;防止被攻击者替换
OPK · 一次性密钥
短期 · 用一次
提供 X3DH 的后向恢复保证
EK · 短暂密钥
一次性 · Alice 发起方
每次开新会话时新生成;提供新鲜性
RK · root key
每次 DH 推进一格
主时间线;从 X3DH 的 SharedSec 启动
CK · chain key
每条消息推进一格
sending / receiving 各一条
MK · message key
每条消息一把
真正用来 AEAD(认证加密)这条消息
DH-EK · 棘轮短暂密钥
每条/每几条消息换
附在消息头里;驱动 DH ratchet ping-pong
🎉 恭喜走完 25 步!
你已经理解了 Signal 协议的全部核心:对称加密 · ECDH · KDF · Symmetric ratchet · DH ratchet · X3DH
想要回顾任何一步?点左侧章节的圆点直接跳转。
想看更深入的细节图谱?回到 ../advanced/index.html 探索老手版 12 张图。
把所有步骤串起来:注册 → 取走 → X3DH → 首消息 → ping-pong
谁能看到什么:E2E 加密的边界一目了然
两个安全方向:FS (左侧绿区) + PCS (右侧绿区)
◀ 上一步
下一步 ▶
↻ 重头来
键盘 ← / → 也能切;URL 自动保存当前步