Telegram 安全方案解析 - 客户端到服务端的加密
简介
Telegram(非正式简称 TG)是跨平台的即时通信软件,其客户端是自由及开放源代码软件,但服务器是专有软件。用户可以相互交换加密与自毁消息,发送照片、影片等所有类型文件。官方提供手机版(Android、iOS、Windows Phone)、桌面版(Windows、macOS、Linux)和网页版等多种平台客户端;同时官方开放应用程序接口(API),因此拥有许多第三方的客户端可供选择,其中多款内置中文。
Telegram 移动端网络通信协议称作 Mobile Protocol,其最新版本为 2.0(旧的 1.0 版本已废弃)。本文将仅对 Mobile Protocol 2.0 的原理与流程展开描述。
加密方案概述
Telegram 使用自研的 MTProto Mobile Protocol 协议进行网络通信,该协议旨在使移动设备上运行的应用程序访问服务器 API。
该协议被细分为以下三个部分:
- 高级组件(API查询语言):定义将 API 查询和响应转换为二进制消息的方法。
- 加密(授权)层:定义在通过传输协议传输之前,对消息进行加密的方法。
- 传输组件:定义客户端和服务器通过其他现有网络协议(例如 HTTP,HTTPS,WS,WSS,TCP,UDP)传输消息的方法。
Telegram 客户端自 4.6 版开始,均使用 MTProto 2.0 协议,本文阐述的也即 MTProto 2.0 协议。MTProto 1.0 正在逐步淘汰,不推荐使用。
客户端到服务端的加密
客户端到服务端的加密是 Telegram 默认使用的加密方式。
- 首先客户端与服务端通过 DH 算法协商一个对称密钥
auth_key
- 后续客户端与服务端的通信,均使用
auth_key
按照一定规则做对称加密
auth_key 的协商
auth_key
在客户端第一次启动时,用户注册前,在后台静默与服务器通信协商生成。用户填写注册信息时,auth_key
可能尚未生成,此时可以用用户按键的间隔作为auth_key
生成过程中随机数生成的熵源。
auth_key
只能由客户端主动与服务端进行协商,协商成功得到如下参数:
auth_key
: 唯一的对称密钥,用于后续客户端与服务端网络通信的加密auth_key_id
: 由auth_key
派生,用于在客户端与服务端网络通信中标识所使用的auth_key
server_salt
: 客户端与服务端网络通信加密所用参数,利用auth_key
协商过程中的参数派生出第一个server_salt
具体流程如下。
使用 auth_key 加密通信
客户端到服务端的消息加密方案流程如下图所示。
假设 A 与 B 进行通信,其主要流程如下所示。无论 A 与 B 谁是服务端或客户端,即服务端 A 与客户端 B 通信,或客户端 A 与服务端 B 通信,均适用如下流程。
- 发送端获取待发送的明文信息
message_data
; - 发送端计算
msg_id = timestamp x 2^32
,若同时发送多个消息,则msg_id
累加 1。长度 64 bits; - 发送端计算生成消息序号
seq_no
。长度 32 bits; - 发送端计算
payload = msg_id + seq_no + message_data_length + message_data
; - 发送端计算
data = server_salt + session_id + payload + padding
; - 发送端计算
msg_key_large = SHA256(substr(auth_key, 88+x, 32) + message_data + random_padding)
;- 对于客户端发送至服务端的消息,
x=0
;对于服务端发送至客户端的消息,x=8
;
- 对于客户端发送至服务端的消息,
- 发送端计算
msg_key = substr(msg_key_large, 8, 16)
; - 发送端计算
sha256_a = SHA256(msg_key + substr(auth_key, x, 36))
; - 发送端计算
sha256_b = SHA256(substr(auth_key, 40+x, 36) + msg_key)
; - 发送端计算
aes_key = substr(sha256_a, 0, 8) + substr(sha256_b, 8, 16) + substr(sha256_a, 24, 8)
; - 发送端计算
aes_iv = substr(sha256_b, 0, 8) + substr(sha256_a, 8, 16) + substr(sha256_b, 24, 8)
; - 发送端计算
encrypted_data = AES256_ige_encrypt(data, aes_key, aes_iv)
; - 发送端计算
payload_new = auth_key_id + msg_key + encrypted_data
; - 发送端将
payload_new
发送至接收端; - 接收端收到消息后,通过
auth_key_id
获取auth_key
,结合msg_key
,使用相同算法计算出aes_key
,aes_iv
, 解密消息; - 接收端做如下校验
- 校验
server_salt
是否正确; - 校验
session_id
是否正确; - 校验
seq_no
是否正确; - 校验
message_data_length
是否正确; - 校验
msg_id
是否大于最近收到的其他消息,小于最近消息msg_id
的消息将被忽略; - 根据
msg_id
计算发送端timestamp
,与接收端timestamp
比较,校验差值是否在 [-30s, 300s] 区间内; - 校验
msg_id
除 4 的余数是否满足既定规则; - 通过
auth_key
,message_data
重新计算msg_key
,校验与发送端发送的msg_key
是否相等。
- 校验
特别的,在 auth_key
交换阶段,消息不加密,使用如下方式通信:
- 发送端计算
msg_id = timestamp x 2^32
,若同时发送多个消息,则msg_id
累加 1。长度 64 bits; - 发送端计算
payload = auth_key_id(=0) + msg_id + message_data_length + message_data
; - 发送端将
payload
发送至接收端; - 接收端收到消息后,做如下校验
- 校验
message_data_length
是否正确; - 根据
msg_id
计算发送端timestamp
,与接收端timestamp
比较,校验差值是否在 [-30s, 300s] 区间内; - 校验
msg_id
除 4 的余数是否满足既定规则。
- 校验
名词解释
Authorization Key (auth_key)
- 在客户端启动时,与服务端通过 DH 算法协商生成,在客户端和服务端共享
auth_key
属于用户,但同一个用户在不同客户端可以有不同的auth_key
,auth_key
可以标识唯一的客户端- 永远不通过网络传输
- 2048 bit
Server Key
- Server 端 RSA 私钥,用于在用户注册,
auth_key
生成期间给 Server 端发送的消息签名 - Client 内置对应的公钥,用于验签
- 几乎不改变
- 2048 bit
Key Identifier (auth_key_id)
- 对
auth_key
做 SHA1,取低阶 64 bits - 如果与已有
auth_key_id
冲突,则auth_key
需要重新生成 - 如果
auth_key_id
为 0,则表示不使用auth_key
加密,主要用于auth_key
生成过程(DH 交换)中的消息 - 64 bit
Session
- 客户端生成随机数,用于区分不同 session
- auth_key_id + session 的组合确定一个与服务端通信的应用程序实例
- 服务端会维护 session 状态
- 任何情况下,都不可能将一个 session 的消息发送到另一个 session
- 服务器可能会单方面舍弃 session 信息,客户端需要自己维护
- 64 bit
Server Salt
- 服务端生成随机数,周期性变化 (每个 session 相互独立)
- 新的 salt 生成后,后续的所有消息都要使用新的 salt(旧的 salt 在 300 秒内仍有效)
- 防重放攻击,防客户端手动修改时间
- 64 bit
Message Identifier (msg_id)
- 唯一标识一个 session 中的某一条 message
- 客户端 msg_id 可以被 4 整除
- 服务端 msg_id 被 4 除余 3,如果是对客户端消息的响应,则被 4 除余 1
- 客户端与服务端 msg_id 在一个 session 中必须单向增加
- msg_id 等于 unixtime * 2 ^ 32。从而保证 msg_id 可以对应一个 message 被创建的大致时间
- 一条消息在创建的 300 秒后,或创建的 30 秒前将被拒收(以防重放攻击)
- 一个 message container 的 msg_id 必须比它包含的所有 msg_id 大
- 为了应对重放攻击,客户端传递的 msg_id 的低阶 32 bits 一定不能为空,并且必须是消息被创建的时间点的一部分
- 64 bit
Content-related Message
- 需要明确确认的消息
- 包括所有用户消息和许多服务消息,几乎是除了容器和确认之外的所有消息
Message Sequence Number (seq_no)
- 等于发送者在此 message 之前创建的 “Content-related Message” 数量的两倍
- 如果当前消息是 Content-related Message,则随后 seq_no 加 1
- 一个容器消息总是在其全部内容之后生成的,所以容器消息的 seq_no 总是大于或等于它所包含的消息的 seq_no
- 32 bit
Message Key (msg_key)
- 对待加密消息做 SHA-256,并前缀 32-byte 的
auth_key
片段,取中间的 128 bit 作为 msg_key - 128 bit
Internal (cryptographic) Header
- 在消息或容器内容加密前的 header
- 包含 server salt (64 bit) 和 session (64 bit)
- 128 bit
External (cryptographic) Header
- 加密后的消息或容器的 header
- 包含 auth_key_id (64 bit) 和 msg_key (128 bit)
- 192 bit
Payload
- [External header] + [加密消息或容器]
这里的 Payload 对应上方客户端与服务端加密通信过程中的 payload_new,也就是最终发送的内容
客户端缓存 auth_key
- 建议用类似 ssh 的方式对 auth_key 进行密码保护
- 对 auth_key 做 SHA-256,并拼接在 auth_key 前面。使用用户输入密码对前面拼接的内容做 AES-CBC 加密,并保存在本地。用户解密时只需要输入密码,本地与解密后的内容重新做一次 SHA-256 验证其正确性。
消息结构
加密消息格式
auth_key_id | msg_key | salt | session_id | message_id | seq_no | message_data_length | message_data | padding12..1024 |
---|---|---|---|---|---|---|---|---|
int64 | int128 | int64 | int64 | int64 | int32 | int32 | bytes | bytes |
明文消息格式
auth_key_id = 0 | message_id | message_data_length | message_data |
---|---|---|---|
int64 | int64 | int32 | bytes |