
愿世界和平!霍尔木兹海峡太重要了,前几个月海峡通行受到影响,全世界都在为此买单,各行各业都收到了影响,普通人的日常支出都被迫升高。这世道不太平,只能独善其身,积极面对,每天健康饮食,自律健身学习,度过康波周期的低谷,后续有好几年的上升期等着我们去创造未来。
信息安全是各个IT系统的生命线,而E2EE加密是IM通讯里最硬核的技术。部分大厂app的高级研发有公开这个功能领域部分设计,基于大厂已有公开资料信息,梳理总结看他们是如何实现。不一定对,权当学习分享。
这几条不是一次到位的:朴素方案先满足"服务端不可解密",再被逐条逼着补上身份认证、前向安全、后向安全。
链路安全,就是一条"打穿—补洞—又打穿"的链:
朴素 DH → 服务端解不开,但有中间人 + 必须在线两个洞
加签名 DH → 挡住中间人,但仍要对方在线
公钥仓库 → 对方不在线也能发,但单密钥泄露=全历史暴露
KDF 棘轮 → 前向安全有了,但链密钥泄露=未来全暴露
DH 棘轮 → 后向安全有了,但离线/乱序两个工程坑冒出来
离线/乱序规约 → 坑补上,但还剩长期密钥隐患
X3DH 三类密钥 → 长期密钥不再单点
群聊 Sender Key→ N² 协商扛不住,退回单棘轮 + 签名
分层加密 → 正文密文,控制层明文供服务端路由具体每一步的设计实现,按公开资料搜索了解到的信息,我们看大厂是如何一步步去迭代实现升级的。
第一道坎:怎么让通信双方在一条不可信信道上,得到同一个对称密钥,而中间的服务端拿不到?
业界几乎都不用 RSA/ECC 那种"公钥加密、私钥解密",而是用 Diffie-Hellman 密钥交换(DH):双方各生成一对公私钥、只交换公钥,各自用"自己的私钥 + 对方的公钥"算出同一个共享密钥。
为什么是 DH 不是公钥加密?核心区别在密钥暴露面:公钥加密要一方生成对称密钥、再把这个密钥本身发到网上——即便被公钥裹了一层,密钥实体终究在信道里走了一趟;而 DH 协商出的共享密钥从不在网络上传输,两边各自独立算出同一个值,信道上只看得到两个公开的公钥(破解依赖离散对数难题)。从根本上减少了密钥暴露面。附带一提,DH 还能延展到多人协商出同一个密钥,为群聊留了口子。
但朴素 DH 有两个致命洞:
洞 1 · 中间人攻击:
DH 本身不带身份认证。攻击者站在中间,
分别和 Alice、Bob 各做一次 DH,做两条加密链路居中转译。
双方都以为在跟对端通信,实际密钥都在攻击者手里。
洞 2 · 双方必须在线:
一来一回交换公钥,接收方离线整个流程就卡住——
IM 里发消息时对方常常不在线,这条直接不可用。那如何解决洞1问题?防止中间人攻击?
中间人之所以能得手,根因是"收到的公钥到底是不是对方的"没法验证。大厂的补法很直接:给临时公钥加一层签名——用一把和身份绑定的长期密钥,签自己发出去的 DH 临时公钥,对方收到先验签确认来源再算共享密钥。完整性还会配 HMAC 保证没被改过一个 bit;算法上通常用 ECDH(椭圆曲线版),同等难度下密钥更短、计算更快。
加签名后洞 1 堵上了。但洞 2 还在——签名只解决"这把公钥是不是对方的",没解决"对方此刻不在线、没法一来一回交换公钥"。这正是 IM 区别于实时音视频握手的核心难点。
IM 是异步的:发"在吗"时对方大概率不在线,但 DH 协商要双方各拿到对方公钥。破局点很巧:DH 公钥本来就可以随便公开。既然能公开就不必当面交换——让每个人提前把公钥上传到一个公开的密钥仓库(一个第三方角色,专存"用户标识 → 公钥"映射),任何人想发消息先去仓库取对方公钥、配自己私钥算出共享密钥,完全不需要对方此刻在线。
注册时: 每个客户端生成 DH 公私钥对,公钥传密钥仓库,私钥留本地
发消息时: Alice 去仓库取 Bob 公钥 → 配自己私钥算共享密钥 → 加密发出
Bob 上线: 取 Alice 公钥 → 配自己私钥算出同一个共享密钥 → 解密密钥仓库只是个公钥货架,看不到任何私钥。洞 2 堵上了。但这个最朴素的异步方案有个更可怕的洞:双方自始至终只用这一把共享密钥加密所有消息——一旦它在任何时刻泄露(设备被取证、内存被 dump),过去全部历史和未来全部消息一次性全暴露。这就引出前向安全这条底线。
要做到"某次密钥泄露,之前的消息仍解不开",核心思路是让每条消息用不同密钥,且新密钥能往前推、旧密钥推不回来。实现靠 KDF(密钥派生函数)的单向性:KDF 本质是 Hash,喂给它当前密钥导出新密钥,新密钥切成两半——前半作为下一次 KDF 的输入(链密钥),后半作为本条消息密钥(用完即丢)。每发一条棘轮步进一格;Hash 单向不可逆,拿到当前密钥推不出上一条——前向安全成立。

KDF 棘轮。链密钥单向往前派生,每步产出一把一次性消息密钥;单向性 → 前向安全。
KDF 棘轮把"单密钥泄露全暴露"砍掉了一半——之前的安全了。但它还留着另一半窟窿:KDF 是确定性的,如果有人掌握了某一步的链密钥,他就能照着同样的算法一路往后算出之后所有的消息密钥。换句话说,KDF 棘轮有前向安全、没有后向安全。
KDF 棘轮的软肋是"链密钥泄露=之后全暴露"。要补上后向安全,思路是:每隔一段就往 KDF 链里掺一份谁也预测不了的新随机量,这样即便此刻链密钥泄露,掺入新随机量之后的密钥就重新不可推算了。
这份"新随机量"从哪来?还是 DH——它的特性是换掉一对密钥,协商出的新密钥就跟着变。于是每完成一个消息轮回,双方各自更新自己的临时 DH 密钥对,拿"对方最新临时公钥 + 自己最新临时私钥"协商出一个新值,作为盐喂进 KDF 棘轮。攻击者掌握了某一步链密钥,也算不出后续轮次掺进来的 DH 协商值,后续密钥重新安全。这就是第二个棘轮——DH 棘轮,两个合起来就是 双棘轮:

双棘轮分工。KDF 棘轮逐条派生密钥做前向安全;DH 棘轮按轮回掺入新盐做后向安全。两者咬合才同时守住前后两条底线。
到这里前向、后向两条底线都满足了。但 DH 棘轮"每轮更新临时公钥"这个动作,在 IM 这个异步、可能乱序的场景里立刻砸出两个工程坑。
DH 棘轮"每轮换临时公钥"这个动作,砸出两个异步系统特有的工程坑——也是 IM E2EE 比 TLS 这类一次性握手难落地的真正原因。
坑一是接收方离线、发送方却在步进棘轮:Alice 连发 5 条、Bob 全程离线,若 Alice 每发一条就换公钥,Bob 上线时对不齐、离线消息全解不开。解法是给步进定死规矩——只有发出消息并收到对方回复后才更新临时公钥,因为"能回复"就证明它已上线、已拿到当前公钥。坑二是乱序到达:KDF 棘轮严格按序派生,要解 N+2 得先派生 N、N+1,做法是收到 N+2 时把跳过的密钥提前派生缓存、它们到了直接取用,缓存必须设上限防恶意大跳号撑爆内存。
回头看还有一处隐患:双棘轮的初始密钥用到了那把注册时生成、终身不换的长期身份密钥,它一旦泄露,由它参与的协商就都有风险,又没法像临时密钥那样轮换。Signal 的补法是把首次密钥协商升级成 X3DH(DH 的三倍扩展版):每人不再只有一把长期密钥,而是三类密钥各司其职:
密钥 | 周期 | 作用 |
|---|---|---|
身份密钥 IPK | 长期,注册时生成 | 绑定用户身份 |
已签名预共享密钥 SPK | 中期,定期轮换,由身份密钥签名 | 隔离/保护长期身份密钥,可轮换 |
一次性预共享密钥 OPK | 一次性,用一把消一把,不足补充 | 给每次新会话一份独有的新鲜随机量 |
三类公钥都预先传到密钥仓库。Alice 给 Bob 发消息时再生成一把临时密钥 EPK,取 Bob 的 IPK / SPK / OPK,用"排列组合"算 4 把 DH 拼接、过一次 KDF,导出会话初始密钥:
DH1 = DH(IPK-A, SPK-B)
DH2 = DH(EPK-A, IPK-B)
DH3 = DH(EPK-A, SPK-B)
DH4 = DH(EPK-A, OPK-B)
初始密钥 S = KDF(DH1 || DH2 || DH3 || DH4)为什么要 4 把而不是 1 把?每把各自防一类风险,注意 Alice 长期身份密钥 IPK-A 只出现在 DH1,后三把全用临时密钥 EPK-A:
任何单一把密钥泄露,都不足以单独算出初始密钥,长期密钥不再是单点。
代价是密钥仓库要为每用户管三类密钥库,其中 OPK 用一把少一把:恶意客户端连续发起握手就能把某用户 OPK 库耗光,之后退化成 IPK + SPK 协商、新鲜度下降——所以 OPK 库要有限流和补充策略。这是 X3DH 留给运维的一个真实运营点。
单聊推完了。群聊能不能照搬双棘轮?不能——N 人两两都做一次 X3DH + 双棘轮是 N² 量级的协商和密钥维护,群一大就崩。Signal 群聊的取舍是砍掉 DH 棘轮、只保留 KDF 链棘轮 + 签名:
入群: 每个成员各生成 ① 一条 KDF 链密钥(发自己的消息用)
② 一对签名密钥
通过已建立的单聊安全通道,把链密钥和签名公钥分发给其他每个成员
发消息: 用自己的 KDF 链棘轮派生消息密钥加密 + 用签名私钥签名 → 发服务端扇出
收消息: 先用发送人签名公钥验签 → 再用对应链密钥派生密钥解密
退群: 所有剩余成员清掉自己的链密钥和签名密钥,重新生成,再各自分发一遍为什么群聊敢砍掉 DH 棘轮?因为后向安全在群里靠退群重密钥这个粗粒度动作近似——成员一变整组密钥重置,拿"重密钥的扇出代价"换"省掉双棘轮复杂度"。代价摆在明处:每个成员都要存其他所有成员的链密钥和签名公钥,群越大密钥库越大;更要命的是退群重密钥——严格做法是剩余每人都重新生成并向所有人分发,N 人群退一个,剩 N-1 人各自向其余 N-2 人重发,总分发量约 (N-1)² 量级,万人群即约 1 亿次,根本扛不住。所以业界共识是用 Signal 这套做 E2EE 群聊人数不能太多,真上 E2EE 的产品几乎都硬限群规模。
最后一步不是加密码学,而是画一条边界。整包都加密,服务端连"这条发给谁"都看不到、没法路由扇出。所以业界通行做法是只加密消息体、控制层保持明文:from / to / timestamp / messageId 这些路由元数据走服务端可读的链路加密,服务端据此投递、扇出,但看不到正文。代价是元数据暴露通信关系——要防流量分析得靠链路层加密兜底。"正文密文 + 控制层明文"是绝大多数 IM E2EE 落地的事实标准。

端到端加密的完整演进链——每个原语都不是凭空设计,而是被前一步的具体攻击逼出来的。这条链就是 Signal Protocol 的来历。
整条链最脆弱的从来不是算法本身——X25519、AES-GCM、HKDF 这些成熟原语能撑住数学攻击。真正的攻击面是工程接口的疏漏:OPK 库枯竭、棘轮步进时机错乱、乱序密钥缓存被打爆、群成员变更后的重密钥遗漏。
先说明选型:E2EE 协议级实现的公开标杆集中在国际开放协议,协议级 E2EE 没有可比的公开案例。所以这一节取三套有完整公开文档、可被审计的国际协议横向看:Signal 是事实标准;Matrix 的 Olm/Megolm 是开放联邦式的另一条落地;Telegram MTProto 的 Secret Chat 是被密码学界长期诟病的反面参照。
Signal Protocol 是 E2EE 的事实标准,What某App、Google某Messages、Facebook某Messenger 的秘密对话都建立在它之上。它正是第二节那条演进链的完整落地:X3DH 做首次异步密钥协商,双棘轮 逐条派生消息密钥做前向 + 后向安全,Sender Key 做群聊(链棘轮 + 签名、退群重密钥)。
落地细节里最见功力的是异步/乱序处理:每条消息头带"当前棘轮公钥 + 序号 N + 上一发送链长度 PN",接收方据此判断该不该步进、跳过了几条,跳过的密钥按"棘轮公钥 + 序号"索引存进跳过密钥表。需要说明的是,X3DH 和双棘轮 Signal 都发布了独立规范,但群聊 Sender Key 并没有单独的官方设计规范、公开记录较少,下面对它的描述基于开源实现与业界归纳。
维度 | 详情 |
|---|---|
优势 | 协议公开可审计、密码学社区背书最强;前向 + 后向安全完整;异步/乱序处理工程化最成熟 |
代价 | 多端 / 历史漫游要额外工程;群规模上限低(Sender Key 重密钥代价);服务端富功能几乎全放弃 |
Matrix 是开放联邦式协议——没有单一服务商,任何人可自建 homeserver 互联。它的 E2EE 用两套棘轮:Olm 做单聊,是 Signal 双棘轮的独立实现(同样的 KDF + DH 内核);Megolm 做群聊,机制和 Sender Key 高度相似——每个发送方维护一条只做 Hash 棘轮的 outbound 会话密钥,每发一条做一次 Hash 派生下一条,通过 Olm 通道分发给房间其他成员。
和 Sender Key 的异同值得玩味:相同点是都用单向 Hash 链棘轮换掉双棘轮、只保前向安全;差异在重密钥——Megolm 的设计意图是成员退出时作废当前 outbound 会话、重建新会话,新成员加入只分发当前棘轮状态、解不开入群前历史。这种"加入只给当前态"是拿"新人看不到历史"换"避免全量重密钥",和 Signal/WhatsApp 在群成员变更上是同一思路。
维度 | 详情 |
|---|---|
优势 | 协议与多个客户端实现全开源可审计;联邦式无单一信任方;Olm/Megolm 分工清晰、群聊可扩展性好于纯两两加密 |
代价 | Megolm 群聊前向安全粒度受会话轮换频率限制;联邦多 homeserver 下密钥/设备验证体验复杂;元数据在联邦节点间仍有暴露面 |
Tele某gram 用自研的 MTProto 协议,且 E2EE 只在 Secret Chat(密聊) 里启用——普通云聊天(Cloud Chat)默认不做端到端加密,只做客户端到服务端加密、服务端能读全部内容(这是多端漫游、云端历史的代价)。Secret Chat 里才用 DH + 自定义加密构造做端到端,且绑定单设备、不跨端同步。
但MTProto 长期受密码学界批评,其一是自研非标准协议:相比 Signal/Matrix 用经同行评审的标准构造,MTProto 的 ad hoc 设计缺乏充分审查,Matthew Green 等研究者多次质疑;电子前哨基金会 2015 年的安全通讯评测里,Telegram 默认聊天只拿到 4/7(密聊 7/7),评分明显偏低。其二是默认不开:日常对话走服务端可读的云聊天,"E2EE" 是个需主动开启、且单设备的小众功能。也有学术工作(如对 MTProto 2.0 的符号化形式验证)给出过有条件的正面结论,但"默认不开 + 自研协议"是它和 Signal/Matrix 路线最根本的分歧。
维度 | 详情 |
|---|---|
优势 | 云聊天多端漫游 / 云端历史体验极好;普通对话因服务端可读,多端同步与云端搜索实现成本低 |
代价 | E2EE 仅密聊可用且默认关闭、绑定单设备;自研非标协议受密码学界长期质疑;普通对话服务端可读 |
维度 | Signal Protocol | Matrix Olm/Megolm | Tele某gram MTProto |
|---|---|---|---|
密钥协商 | X3DH | Olm(双棘轮内核) | 自定义 DH(仅密聊) |
单聊棘轮 | KDF + DH 双棘轮 | Olm 双棘轮 | 无棘轮式前后向安全设计 |
群聊方案 | Sender Key(链棘轮 + 签名) | Megolm(Hash 链棘轮) | 群组不做 E2EE |
多端 | 受限,需额外工程 | 联邦 + 多设备验证(较复杂) | 密聊绑定单设备 |
是否默认开启 | 默认全程开启 | 房间可开(默认随客户端策略) | 默认关,仅密聊手动开 |
协议是否公开可审计 | 是,社区背书强 | 是,全开源联邦 | 自研非标,长期受质疑 |
横向看:Signal 与 Matrix 是同一条演进链的两个开源实现(X3DH/Olm 双棘轮 + Sender Key/Megolm 链棘轮),分歧主要在"中心化 vs 联邦";Telegram 是另一条路——把强加密做成可选小众功能、主体走服务端可读,多端漫游轻松很多,但也是密码学评价上长期失分的根由。三家共同印证了第二节的推导:群聊都退回单棘轮 + 签名,双棘轮的 N² 代价在群里谁都扛不住。
真上 E2EE 群聊,最容易翻车的就是 2.8 那个退群重密钥,严格做法在万人群是亿级分发、根本不可行。业界一致的折中是两条:硬限群规模,控制在几百人量级;接受成员变更的安全降级,参照 Megolm/Sender Key——新成员加入只下发当前棘轮状态(看不到入群前历史,本就合理),不触发现有成员全量重密钥。再加一个常被忽略的优化:重密钥与首次收发解耦——成员变更后不立刻全员重分发,等各自下次真要收发时再触发,避免大群里大量"潜水成员"白白产生重密钥流量。
2.6 那条"收到回复才步进 DH 棘轮"的规约,很多人实现时当成边界 case 草草处理,结果离线消息解不开的 bug 反复出现。更稳的做法是:把步进显式绑定到"确认对方已收齐"事件,而不是绑定到"我发了消息"——发送方维护一个"对方已确认到哪条"的水位,只有水位推进才允许步进。这比"发消息就步进"多记一个状态,但能从根上消除"离线期间狂换公钥导致全解不开"这类最难复现的偶发 bug。代价是后向安全的步进频率慢一点——拿一点点后向安全粒度换离线场景的解密确定性,在异步系统里通常值得。
E2EE 的可观测:棘轮协议有一批通用密文监控看不出来的专属故障维度,必须单独埋点:
这些指标全部基于客户端上报的匿名化错误码,不碰任何明文,却能定位绝大多数"对方消息我解不开"的根因。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。