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 默认使用的加密方式。

  1. 首先客户端与服务端通过 DH 算法协商一个对称密钥 auth_key
  2. 后续客户端与服务端的通信,均使用 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 通信,均适用如下流程。

  1. 发送端获取待发送的明文信息 message_data;
  2. 发送端计算 msg_id = timestamp x 2^32,若同时发送多个消息,则 msg_id 累加 1。长度 64 bits;
  3. 发送端计算生成消息序号 seq_no。长度 32 bits;
  4. 发送端计算 payload = msg_id + seq_no + message_data_length + message_data;
  5. 发送端计算 data = server_salt + session_id + payload + padding;
  6. 发送端计算 msg_key_large = SHA256(substr(auth_key, 88+x, 32) + message_data + random_padding);
    • 对于客户端发送至服务端的消息,x=0;对于服务端发送至客户端的消息,x=8;
  7. 发送端计算 msg_key = substr(msg_key_large, 8, 16);
  8. 发送端计算 sha256_a = SHA256(msg_key + substr(auth_key, x, 36));
  9. 发送端计算 sha256_b = SHA256(substr(auth_key, 40+x, 36) + msg_key);
  10. 发送端计算 aes_key = substr(sha256_a, 0, 8) + substr(sha256_b, 8, 16) + substr(sha256_a, 24, 8);
  11. 发送端计算 aes_iv = substr(sha256_b, 0, 8) + substr(sha256_a, 8, 16) + substr(sha256_b, 24, 8);
  12. 发送端计算 encrypted_data = AES256_ige_encrypt(data, aes_key, aes_iv);
  13. 发送端计算 payload_new = auth_key_id + msg_key + encrypted_data;
  14. 发送端将 payload_new 发送至接收端;
  15. 接收端收到消息后,通过 auth_key_id 获取 auth_key,结合 msg_key,使用相同算法计算出 aes_key, aes_iv, 解密消息;
  16. 接收端做如下校验
    1. 校验 server_salt 是否正确;
    2. 校验 session_id 是否正确;
    3. 校验 seq_no 是否正确;
    4. 校验 message_data_length 是否正确;
    5. 校验 msg_id 是否大于最近收到的其他消息,小于最近消息 msg_id 的消息将被忽略;
    6. 根据 msg_id 计算发送端 timestamp,与接收端 timestamp 比较,校验差值是否在 [-30s, 300s] 区间内;
    7. 校验 msg_id 除 4 的余数是否满足既定规则;
    8. 通过 auth_key, message_data 重新计算 msg_key,校验与发送端发送的 msg_key 是否相等。

特别的,在 auth_key 交换阶段,消息不加密,使用如下方式通信:

  1. 发送端计算 msg_id = timestamp x 2^32,若同时发送多个消息,则 msg_id 累加 1。长度 64 bits;
  2. 发送端计算 payload = auth_key_id(=0) + msg_id + message_data_length + message_data;
  3. 发送端将 payload 发送至接收端;
  4. 接收端收到消息后,做如下校验
    1. 校验 message_data_length 是否正确;
    2. 根据 msg_id 计算发送端 timestamp,与接收端 timestamp 比较,校验差值是否在 [-30s, 300s] 区间内;
    3. 校验 msg_id 除 4 的余数是否满足既定规则。

名词解释

Authorization Key (auth_key)

  • 在客户端启动时,与服务端通过 DH 算法协商生成,在客户端和服务端共享
  • auth_key 属于用户,但同一个用户在不同客户端可以有不同的 auth_keyauth_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
  • 需要明确确认的消息
  • 包括所有用户消息和许多服务消息,几乎是除了容器和确认之外的所有消息

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

参考