BlindPost BlindPost ← 所有文章
English简体中文繁體中文

为什么你刚发出去的消息,连你自己都解不开

关于端到端加密,有一个常见的默认假设:发送方一定能解开自己发的消息。是你写的明文,那总该留着加密用的那把钥匙吧?

BlindPost 不是这样工作的。一旦你把消息发出去,那段在我们基础设施里流转的密文,只有目标接收方能解 —— 不是我们,不是发完之后的你,也不是日后有人拿到了你手机的情形。你自己的手机会本地留一份明文(让你能在聊天历史里看到你刚才发了什么),但流过服务器、躺在接收方那一侧的那段密文,对所有人都封死了,除了接收方本人。

下面用标准密码学的层面,讲清楚为什么。

你按下发送之后发生了什么

你点发送的时候,客户端大致做这几件事:

  1. 临时生成一对全新的、一次性的 X25519 密钥,只为这条消息用一次。我们叫它临时密钥。它只存活在这次发送操作之内,从不落盘、从不写进你的账号、从不以原始形式被任何人看到
  2. 算共享密钥:用这把临时私钥(你刚生成的)和接收方公开的公钥做椭圆曲线 Diffie–Hellman。
  3. 派生 session key:对共享密钥跑 HKDF,域分隔标签里同时绑定你的用户标识和接收方的标识。
  4. 加密明文:用这把 session key 配一个 authenticated cipher 加密,加密时把发送方 / 接收方 / 时间戳一起绑进 authentication data。
  5. 对密文签名,用你的长期身份私钥,让接收方能验证消息确实是你发的。
  6. 打包 密文 + 签名 + 临时公钥 + 最少必要的路由元数据,送出去。
  7. 函数返回。那个保存临时私钥的局部变量超出作用域,被 GC 回收。没了

发完之后,流过我们基础设施的东西:

想解开这段密文,需要的是:

宇宙里只有一方能完成这个运算:接收方本人。还得是在他们还持有对应那把短期私钥的窗口内 —— 我们会定期轮换 prekey,用户自己也可以主动废止旧密钥。

你 —— 发送方 —— 在发送函数返回的那一刻就把临时私钥扔出了作用域。我们的基础设施上的任何人都没碰到过它。接收方也从没拿到过它;接收方手上的是临时公钥那一半,加上他们自己的私钥 —— 这两个组合起来能算出同一个共享密钥。

为什么这件事重要:一个威胁模型 walkthrough

场景 1:今天你的手机被偷了。 攻击者把你的整个手机 dump 一遍 —— 安全存储里的每把密钥、本地数据库的每一个字节都拿到。他们能得到:

他们做不到的事:回头把那些消息对应的密文版本(在我们基础设施上、在备份里、在传票拿走的证据柜里 —— 凡是密文存在的地方)解开。每条都用了一把不同的临时密钥加密,而这些临时密钥没有一把还存在于你手机上的任何地方

场景 2:再叠加一次完整的服务器入侵。 加上一个把我们集群上每一份加密 envelope 都偷走的攻击者。把场景 1 里的密钥跟这堆密文交叉对照,他们还是解不开你发过的消息。这道数学闭不上。

这套组合能力确实能拿到的:

拿不到的:

群消息:同样的属性

群消息走的是同款架构,只是把"接收方公钥"换成"群的公钥"(群本身就是那把公钥)。每次发送都生成一把全新的临时私钥,跟群公钥做 Diffie–Hellman,加密,签名,扔掉临时私钥。即便明天某个大群的每个成员都被拿下,而我们存的每一字节密文也被偷走,你过去发到那个群的密文依然在数学上够不到 —— 除非有人手上还有(a)那些早已消失的临时私钥之一,或者(b)群的私钥 —— 而群私钥从头到尾只在成员的设备上存在过。

我们为什么这么搭

更简单的端到端协议可以让发送方握住一把稳定的、自己也知道的加密密钥 —— 用我的长期密钥加密,把同一把密钥用接收方公钥包一份发过去,两边都能解。这种做法更快、更简单。但有一个脆弱属性:发送方一旦被攻陷 = 自己发过的所有消息都被回溯解密,不管那些密文落到了哪里。备份、还没删的服务器副本、几年后躺在证据柜里被传票调走的副本,统统能开。

每条消息一把临时密钥跑 Diffie–Hellman 这套(有时叫 sender forward secrecy / fire-and-forget encryption),把这个攻击面整个铲掉。代价是每次发送多算几毫秒密码学。换来的是:未来的任何一个版本的你、任何一次入侵、任何一次配合法庭命令交出密钥的情形,都没办法从离开你设备之后的那些密文里把明文捞回来。

你的过去,对你自己也是封闭的。这就是我们想要的属性。

立即体验 BlindPost