Telegram 安全方案解析 - 文件及音视频加密

客户端到服务端的文件发送

客户端到服务端的文件发送流程遵循通用的 客户端到服务端的加密,文件数据按照 Telegram 规定的 二进制数据序列化 规则即可。

客户端到客户端的文件发送

所有发送到 Secret Chat 的文件均使用一次性密钥加密,该密钥与 secret_key 没有任何关系。

  1. 发送端获取文件明文数据 file_data;
  2. 发送端生成两个 256 bits 的数字,分别作为 keyiv;
  3. 发送端计算 payload = encrypted_data = AES256_ige_encrypt(file_data, key, iv);
  4. 发送端计算 digest = md5(key + iv);
  5. 发送端计算 fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4);
  6. 发送端将 payload, fingerprint 作为一个文件整体上传至服务端,并获取文件对应的地址 path;
  7. 发送端将 path, key, iv 作为消息明文发送至接收端;
  8. 接收端收到消息后,通过 path 下载文件;
  9. 接收端重新计算 fingerprint,校验与下载的值是否相等;
  10. 接收端使用 aes, iv 解密文件得到原始文件。

客户端到客户端的音视频通话

在呼叫准备好之前,必须进行一些初始化的操作。

  • 主叫方需要联系被叫方,并检查它是否准备好接受呼叫。
  • 双方还要协商好要使用的协议
  • 双方需要了解对方或要使用的 Telegram 中继服务器的IP地址
  • 双方借助 Diffie–Hellman 密钥交换为这次呼叫生成一次性加密密钥

密钥交换

相比于客户端到服务端的 auth_key 交换,及客户端到客户端的 secret_key 交换,在音视频通话中,Telegram 优化了密钥交换流程,以实现更强的安全性。

  1. A->B : 生成 a, 计算 g_a_hash := hash(g^a), 发送 g_a_hash;
  2. B->A : 存储 g_a_hash, 生成 b, 计算 g_b := g^b, 发送 g_b;
  3. A->B : 计算 key := (g_b)^a, 计算 g_a = g^a, 计算 key_fingerprint = substr(SHA1(key), 0, 8), 发送 g_a, key_fingerprint;
  4. B : 校验 hash(g_a) == g_a_hash, 校验 substr(SHA1(key), 0, 8) == key_fingerprint, 计算 key := (g_a)^b;

A 确定一个特定的 a 值(和 g_a),但直到最后一步才向 B 透露 g_a。B 必须在不知道 g_a 的真实值的情况下选择他的 bg_b 的值。如果进行中间人攻击,则中间人不能根据 B 的 g_b 值来改变 a,也不能根据 g_a 来调整 b 的值。因此,中间人只有一次注入参数的机会,并且此时中间人没有任何中间交换的特征值。

借助上述优化,仅用超过 33 bits 的熵,就可以防止超过 0.9999999999 的中间人攻击。这些 bits 以四个表情符号的形式呈现给用户。Telegram 选择了一个由 333 个表情符号组成的表情池,这些表情符号看起来都有很大的不同,并且可以很容易地用任何语言的简单词汇来描述。

加密方案

音视频加密方案是一个基于 MTProto 2.0 的优化版本,数据包由一个或多个各种类型的端到端加密消息组成。

加密开始工作时,以下内容已就绪

  • 双方共享的加密密钥 key,如上文中生成的
  • 呼叫是呼出还是呼入的信息
  • 两个数据传输通道:由Telegram API 提供的信令通道 signaling,和基于 WebRTC 的传输通道 transport

这两种数据传输通道都是不可靠的(消息可能会丢失),但信令传输速度较慢,可靠性较高。

通话数据加密

一个数据包的主体由多个消息和它们各自的 seq 序号连在一起组成。

1
decrypted_body = message_seq1 + message_body1 + message_seq2 + message_body2

每个 decrypted_body 都是唯一的,因为第一个消息的 seq 序号不能相同。如果只有旧的消息需要重新发送,则先在数据包中添加一个具有新的唯一 seq 的空消息。

加密密钥 key 用于计算 128 bits 的 msg_key,然后计算 256 bits 的 aes_key 和 128 bits 的 aes_iv

  1. msg_key_large = SHA256(substr(key, 88+x, 32) + decrypted_body);
  2. msg_key = substr(msg_key_large, 8, 16);
  3. sha256_a = SHA256(msg_key + substr(key, x, 36));
  4. sha256_b = SHA256(substr(key, 40+x, 36) + msg_key);
  5. aes_key = substr(sha256_a, 0, 8) + substr(sha256_b, 8, 16) + substr(sha256_a, 24, 8);
  6. aes_iv = substr(sha256_b, 0, 4) + substr(sha256_a, 8, 8) + substr(sha256_b, 24, 4);

x 取决于呼叫是呼出还是呼入以及连接类型。

传输通道 信令通道
呼出 0 128
呼入 8 136

这允许应用程序决定哪些数据包类型将被发送到哪些连接,并在这些连接中独立工作(每个连接有各自的 seq 计数器)。

aes_keyaes_iv 将被用于加密 decrypted_body

1
encrypted_body = AES_CTR(decrypted_body, aes_key, aes_iv)

被发送的数据包由 msg_keyencrypted_body 组成。

1
packet_bytes = msg_key + encrypted_body

当接收到数据包时,使用 keymsg_key 对数据包进行解密,然后重新计算 msg_key,校验与收到的是否相等。如果不相等,则必须丢弃该数据包。

防止重放攻击

每个端都有自己的 32 bits 出站报文计数器 seq,从 1 开始单调递增。这个 seq 计数器被预置到每个发送的报文中,并为每个新报文增加 1。一个报文包中第一个报文的 seq 号不能相同。如果只有旧的消息需要重新发送,则先在数据包中添加一个具有新的唯一 seq 的空消息。当seq计数器达到 2^30 时,必须中止呼叫。每个端都会存储它收到(和处理)的所有大于 max_received_seq - 64 的消息的 seq 值,其中 max_received_seq 是目前收到的最大 seq 数。

如果收到一个报文,其中第一个报文的 seq <= max_received_seq - 64,或者它的 seq 值已经被收到,那么这个报文将被丢弃。否则,所有接收到的报文的 seq 值都会被记住,并调整 max_received_seq。这样可以保证没有两个数据包会被处理两次。

密钥校验

为了验证加密秘钥,并确保没有发生中间人攻击,双方做如下计算

1
hash = SHA256(key + g_a)

hash 按位等分为 4 个 64 bits 的整数,每个整数怼表情符号总数(目前为 333)取模,用于选择特定的表情符号,从而得到四个表情符号。双方对比 4 个表情符号是否相同,从而确保没有中间人攻击。

协议的具体内容保证了在 333 个表情符号中比较 4 个表情符号足以防止 0.9999999999 的中间人攻击。

群音视频通话

根据官方说明,群音视频通话仍在研发中,预计在 2020 年末推出,但至今(2021年2月15日)仍未推出。

群语音聊天

与音视频通话不同,群语音聊天只是在群内创建一个独立的空间,允许群成员一起发送语音聊天,但每一条语音仍是作为普通的语音消息,使用客户端到服务端的加密

参考