上一篇我们讲清了 Telegram 的秘密:私密聊天的服务器「只做看不懂的邮差」。
这一篇,我们用 Go 的标准库,亲手把这个「邮差」敲出来——让服务器眼睁睁看着密文进进出出,却永远解不开一个字。
引言:把原理变成代码
上一篇文章《Telegram 软件架构与端到端加密拆解》里,我们得到了一个核心结论:
端到端加密的本质,是「把密钥放在两端设备上,让服务器只转发密文」。
但「原理懂了」和「代码会写」之间,隔着一段距离。这篇实战,我们就用 Go 的标准库,零第三方依赖,实现一个「够演示原理即可」的仿 Telegram 端到端加密系统。它忠实还原 Telegram Secret Chats 的核心思想:
- DH-2048 密钥交换:两端协商共享密钥,服务器拿不到私钥,算不出密钥。
- SHA-256 派生 AES 密钥:把 DH 共享密钥哈希成定长 256 位密钥。
- AES-GCM 加解密:密文带认证标签,防篡改。
- 密钥指纹比对:两端展示指纹,肉眼防中间人。
- 服务端密文中转:服务端只注册/路由/转发,从不接触明文。
免责声明:本文代码仅为教学演示,未实现 MTProto 的 salt 防重放、会话密钥轮换、IGE 模式、auth_key 等生产级机制,不可用于真实保密通信。
一、整体架构:服务器是「看不懂的邮差」
我们设计一个最简模型:一个服务端 + 两个客户端(Alice 和 Bob)。
1 | Alice ──注册──▶ ◀──注册── Bob |
服务端手里永远只有三样东西:公钥 A、公钥 B、密文。没有私钥,它就算不出共享密钥,也就解不开任何消息——这正是 Telegram 服务器面对 Secret Chats 时的处境。
二、协议帧:两端共享的「信封」
先定义一个 JSON 协议帧,作为客户端与服务端通信的契约:
1 | // 协议帧:客户端 <-> 服务端(encoding/json 序列化) |
三种类型各司其职:
| Type | 含义 | 携带字段 |
|---|---|---|
register |
用户注册上线 | from |
pubkey |
交换 DH 公钥 | from / to / pubkey |
message |
发送加密消息 | from / to / ciphertext |
三、服务端:只做三件事,绝不碰明文
服务端的逻辑极其简单——注册、路由、转发。它把密文原封不动地传给目标用户,从头到尾不接触任何密钥:
1 | // 服务端:注册 / 路由 / 密文中转(只存密文,不接触明文与密钥) |
注意 case "pubkey", "message" 这两行的处理:服务端拿到帧后,只根据 f.To 找到目标连接,然后把整个帧原样转发。它不解析 pubkey 的含义,更不解 ciphertext——因为它根本没有解密所需的密钥。
这就是「看不懂的邮差」:邮差全程捧着你的信,但他既没有开信刀,也没有密码本。
四、客户端:DH 密钥交换的完整流程
客户端的核心是 Diffie-Hellman 密钥交换。它的精妙之处在于:双方在公开信道上交换公钥,却各自算出同一个共享密钥,而偷听者永远算不出。
1. 使用 RFC 3526 固定安全素数
DH 需要一个「够大」的素数 p 和生成元 g。我们不自己生成质数(有陷阱且慢),直接用 RFC 3526 MODP Group 14 的 2048 位安全素数:
1 | // RFC 3526 MODP Group 14 的 2048 位安全素数(十六进制,已省略中段) |
完整的素数 p 是 512 个十六进制字符(2048 位),可在 RFC 3526 第 3 节「2048-bit MODP Group with 256-bit Prime Order Subgroup」查到。这里为排版省略中段。
2. 生成密钥对
1 | // 生成私钥 a(随机 256 位大整数),计算公钥 A = g^a mod p |
rand.Int 从加密安全的随机源 crypto/rand 取一个小于 p-2 的大整数作为私钥 a,再用模幂运算算出公钥 A = g^a mod p。私钥永远留在本地,只把公钥发出去。
3. 交换公钥,计算共享密钥
1 | // 双方交换公钥后,各自计算共享密钥 |
数学上 (g^b)^a = (g^a)^b = g^(ab) mod p,所以 Alice 用「Bob 的公钥 + 自己的私钥」、Bob 用「Alice 的公钥 + 自己的私钥」,算出的 s 完全一致。而偷听者手里只有 g、p、A、B 四个公开值,从它们反推私钥 a 或 b 是离散对数难题(2048 位下在计算上不可行)。
五、密钥派生与加密:SHA-256 + AES-GCM
1. 派生 AES 密钥
DH 共享密钥 s 是一个 2048 位的大整数,直接当 AES 密钥太长且分布不均。我们用 SHA-256 把它哈希成定长 256 位:
1 | // AES 密钥 key = SHA-256(共享密钥 s),得到均匀分布的 256 位密钥 |
2. AES-GCM 加密
AES-GCM 是一种「认证加密」,密文里自带认证标签,任何篡改都会被检测到:
1 | // 加密:AES-GCM Seal(返回 nonce + 密文 + 认证标签) |
注意两点:
nonce用crypto/rand随机生成,同一明文加密两次会得到不同密文,防止流量分析。Open一旦发现认证标签不匹配,会直接返回错误——篡改密文的人无法无声地替换消息。
六、密钥指纹:肉眼防中间人
端到端加密最大的天敌是中间人攻击(MITM)。我们的防线是密钥指纹——两端各自计算公钥指纹,通过可信渠道(打电话/见面)肉眼比对:
1 | // 密钥指纹:SHA-256(公钥) 前 8 字节,格式化为 AA:BB:CC:...:HH |
运行后,Alice 和 Bob 的终端会各自打印出同一串指纹:
1 | [alice] 密钥指纹: 1A:2B:3C:4D:5E:6F:70:81 |
两边一致 → 没有中间人。若有人在中途冒充 Bob 和 Alice 分别完成 DH 交换,两端算出的公钥指纹必然不同——这正是 Telegram 私密聊天让你「比对图形指纹」的原理,我们用 8 字节十六进制串做了同样的实现。
七、客户端主流程串起来
把上面的函数组装成客户端的完整流程:
1 | // 客户端:注册 -> DH 交换 -> 加解密收发 |
完整跑起来,你需要三个终端:
1 | # 终端 1:启动服务端 |
最终效果:Alice 和 Bob 各自看到相同的密钥指纹,互发的密文只有对方能解开,而服务端的日志里,只会出现一行行它自己都读不懂的 ciphertext。
八、局限与边界:为什么它还不能当「真 Telegram」
演示归演示,我们必须诚实标注这套代码的边界:
| 我们实现了 | MTProto / Telegram 还做了什么 |
|---|---|
| DH-2048 密钥交换 | auth_key 长期复用 + 会话密钥轮换 |
| SHA-256 派生密钥 | salt + 会话号参与的 msg_key 派生 |
| AES-GCM 认证加密 | AES-256-IGE 模式 |
| 密钥指纹比对 | 图形化指纹 + 二维码 |
| 服务端密文中转 | 防重放(msg_id)、前向保密(Perfect Forward Secrecy) |
最后一句:
我们写出的,是一把「能演示原理的锁」,而不是「能守住的保险柜」。
真正的端到端加密产品,安全性不只在加密算法里,更在前向保密、防重放、密钥轮换这些「被演示代码省略掉的细节」里。
记住:你省掉的每一行「麻烦」,都可能成为攻击者的一扇后门。
参考
- Telegram MTProto 2.0 官方文档:https://core.telegram.org/mtproto
- Telegram Secret Chats(端到端加密):https://core.telegram.org/api/end-to-end
- RFC 3526 — MODP Groups for IKE(2048-bit Group 14)
相关文章
本文是「端到端加密与安全通信」系列的一员:
- 《Telegram 软件架构与端到端加密拆解:为什么它的「安全」是可选,而非默认》 —— 端到端加密的产品侧拆解
- 《手机密码、加密通讯与取证对抗》 —— 移动设备加密与取证对抗
- 《bitchat:连服务器都没有,拿什么监管你?——去中心化、免疫审查的蓝牙 Mesh 即时聊天软件》 —— 无服务器的去中心化通讯
- 《GrapheneOS 完全指南:把安卓的安全与隐私做到极致》 —— 安全安卓系统上的加密实践
评论