端到端加密,电报秘密聊天
关于电报 MTProto 的端到端加密功能,面向高级用户。如果您想从更易于理解的方式了解更多关于秘密聊天的信息,请参阅我们的常见问题解答。
请注意,从 4.6 版本开始,主流 Telegram 客户端均已使用MTProto 2.0。MTProtov.1.0
已被弃用,目前正在逐步淘汰。
秘密聊天是一对一的聊天,消息使用只有聊天参与者才持有的密钥进行加密。请注意,这种端到端加密的秘密聊天的方案与云聊天所使用的方案不同:
关于 MTProto 2.0 的说明
本文介绍了MTProto协议2.0版本中的端到端加密层。与1.0版本(此处仅供
参考)的主要区别如下:
- 使用 SHA-256 代替 SHA-1;
- msg_key 的计算中涉及到填充字节;
- msg_key 不仅取决于要加密的消息,还取决于部分秘密聊天密钥;
- v.1.0 版本使用 12..1024 个填充字节代替 0..15 个填充字节。
另请参阅:MTProto 2.0:云聊天、服务器-客户端加密
密钥生成
密钥是使用Diffie-Hellman 协议生成的。
让我们考虑以下场景:用户A希望与用户B发起端到端加密通信。
发送请求
用户A执行messages.getDhConfig以获取 Diffie-Hellman 参数:素数p和高阶元素g。
在每次生成新密钥之前执行此方法至关重要。将参数值及其版本缓存起来是合理的,这样可以避免每次都接收所有值。如果客户端存储的版本仍然是最新的,服务器将返回构造函数消息messages.dhConfigNotModified。
客户端需要检查p是否为安全的 2048 位素数(即p和(p-1)/2均为素数,且2²2047 < p <2²2048),以及g是否生成一个素数阶为(p-1)/2的循环子群,即 g 是否模 p 为二次剩余。由于g始终等于 2、3、4、5、6 或 7,因此可以使用二次互反律轻松完成此操作,从而得到p mod 4g的一个简单条件——即:当 g = 2 时,p mod 8 = 7;当g = 3 时,p mod 3 = 2;当g = 4时,无需额外条件;当g = 5时,p mod 5 = 1 或 4;当g =6时,p mod 24 = 19 或 23;当g= 7时,p mod 7 = 3、5 或 6。客户端检查完g和p之后,将结果缓存起来是合理的,这样可以避免将来重复进行耗时的计算。这个缓存可以与用于生成授权密钥的缓存共享。
如果客户端需要为随机数生成器增加熵,可以传递`random_length`参数(`random_length > 0`),以便服务器生成一个合适长度的随机序列。重要提示:直接使用服务器生成的随机序列可能不安全,必须将其与客户端生成的序列结合使用。
客户端A计算一个 2048 位数字a(使用足够的熵或服务器的随机数;见上文),并在传入后执行messages.requestEncryptiong_a := pow(g, a) mod dh_prime。
用户B会收到包含聊天构造函数encryptedChatRequested的所有关联授权密钥(所有已授权设备)的更新updateEncryption。系统必须向用户 B 显示有关用户A的基本信息,并提示用户接受或拒绝此请求。
两个客户端都需要检查g、g_a和g_b是否大于 1 且小于p-1。我们还建议检查g_a和g_b是否在2^{2048-64}和p - 2^{2048-64}之间。
接受请求
用户B在客户端界面确认与A创建秘密聊天后,客户端B还会收到 Diffie-Hellman 方法的最新配置参数。之后,它使用与a类似的规则生成一个随机的 2048 位数字b。
收到通过encryptedChatRequested更新的g_a后,即可立即生成最终共享密钥。如果密钥长度小于 256 字节,则添加几个前导零字节作为填充,使密钥长度正好为 256 字节。其指纹key_fingerprint等于 SHA1(密钥) 的最后 64 位。key = (pow(g_a, b) mod dh_prime)
注 1:在本例中,即使对于 MTProto 2.0 秘密聊天,也使用 SHA1。
注2:此指纹用于密钥交换过程的健全性检查,以在开发客户端软件时检测错误——它与客户端上用作秘密聊天中外部身份验证手段的密钥可视化无关。客户端上的密钥可视化是使用 SHA1(初始密钥)的前 128 位和 SHA256(秘密聊天更新到第 46 层时使用的密钥)的前 160 位生成的。
客户端B在传递消息和密钥指纹后执行messages.acceptEncryption。g_b := pow(g, b) mod dh_prime
对于客户端B 的所有已授权设备(当前设备除外),updateEncryption更新会通过构造函数encryptedChatDiscarded发送。此后,唯一能够访问秘密聊天的设备是调用messages.acceptEncryption的设备B。
用户A将收到一个updateEncryption更新,其中包含构造函数encryptedChat,用于发起聊天的授权密钥。
利用更新得到的g_b ,客户端A还可以计算共享密钥key = (pow(g_b, a) mod dh_prime)。如果密钥长度小于 256 字节,则添加几个前导零字节作为填充,使密钥长度正好为 256 字节。如果接收到的密钥的指纹与传递给encryptedChat 的指纹相同,则可以发送和处理传入的消息。否则,必须执行messages.discardEncryption并通知用户。
完美前向保密
为了保障过往通信安全,官方 Telegram 客户端会在密钥用于解密和加密超过 100 条消息,或使用超过一周(前提是该密钥至少用于加密过一条消息)后,启动密钥重置程序。旧密钥将被安全丢弃,即使拥有当前使用的新密钥也无法重建。
本文进一步描述了重新密钥协议:秘密聊天中的完美前向保密。
请注意,您的客户端必须支持秘密聊天中的前向保密功能,才能与官方 Telegram 客户端兼容。
在秘密聊天中发送和接收消息
外发消息的序列化和加密
创建一个类型为DecryptedMessage的 TL 对象,其中包含纯文本消息。为了向后兼容,该对象必须包装在decryptedMessageLayer构造函数中,并指定支持的层(从 46 开始)。
端到端加密消息内容的 TL-Schema 可在此处获取 »
使用通用的 TL 规则将生成的结构序列化为字节数组。生成的数组前面会添加 4 个字节,表示数组长度(不包括这 4 个字节)。
为了使字节数组的长度能被 16 字节整除,会在其中填充 12 到 1024 个随机填充字节。(在较早的 MTProto 1.0 加密中,只使用了 0 到 15 个填充字节。)
消息密钥msg_key的计算方法是:取上一步获得的数据的 SHA256 中间 128 位,并加上来自共享密钥key的 32 字节作为前缀。(对于较早的 MTProto 1.0 加密,msg_key 的计算方法不同,它是取上一步获得的数据的 SHA1 低 128 位,不包括填充字节。)
对于 MTProto 2.0,AES 密钥aes_key和初始化向量aes_iv的计算方式如下(key是在密钥生成期间获得的共享密钥):
- msg_key_large = SHA256 (substr (key, 88+x, 32) + plaintext + random_padding);
- msg_key = substr (msg_key_large, 8, 16);
- sha256_a = SHA256 (msg_key + substr (key, x, 36));
- sha256_b = SHA256 (substr (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);
对于 MTProto 2.0,x=0表示来自秘密聊天发起者的消息,x=8表示反向的消息。
对于已过时的 MTProto 1.0,msg_key、aes_key 和 aes_iv 的计算方式有所不同(请参阅此文档以了解详情)。
数据使用 256 位密钥aes_key和 256 位初始化向量aes-iv,采用带有无限混叠扩展 (IGE) 的 AES-256 加密算法进行加密。加密密钥指纹key_fingerprint和消息密钥msg_key被添加到生成的字节数组的顶部。
加密数据嵌入到messages.sendEncryptedAPI 调用中,并传递给 Telegram 服务器,以便发送给秘密聊天的另一方。
从 MTProto 1.0 升级到 MTProto 2.0
一旦秘密聊天中的双方都使用至少 Layer 73,则所有外发消息都应使用 MTProto 2.0。如果在创建秘密聊天时未协商足够高的起始层,则最初收到的一些消息可能使用 MTProto 1.0。收到第一条使用 MTProto 2.0 加密的消息(或第一条使用 Layer 73 或更高层加密的消息)后,所有序列号更高的消息也必须使用 MTProto 2.0 加密。
只要当前层级低于 73,各方都应尝试使用 MTProto 1.0 解密接收到的消息,如果解密失败(msg_key 不匹配),则尝试使用 MTProto 2.0。一旦收到第一条使用 MTProto 2.0 加密的消息(或层级升级到 73),就不需要再尝试使用 MTProto 1.0 解密任何后续消息(除非客户端仍在等待某些间隙被填补)。
解密收到的消息
上述步骤按相反顺序执行。
收到加密消息后,必须检查 msg_key 是否确实等于解密消息的 SHA256 哈希值的中间 128 位,并加上从共享密钥中提取的 32 字节。
如果消息层级高于客户端支持的层级,则必须通知用户客户端版本已过期,并提示用户更新。
序列号
为防止篡改,必须按原始顺序解读所有消息。秘密聊天支持一种特殊的机制,可以独立于服务器处理 seq_no 计数器。
本文进一步描述了如何正确处理这些计数器:秘密聊天中的序列号。
请注意,您的客户端必须支持秘密聊天中的序列号,才能与官方 Telegram 客户端兼容。
发送加密文件
发送到秘密聊天的所有文件都使用一次性密钥进行加密,这些密钥与聊天的共享密钥没有任何关系。在发送加密文件之前,假定加密文件的地址将使用messages.sendEncryptedFile方法的file参数附加到加密消息的外部,并且用于直接解密的密钥将通过消息正文发送(构造函数decryptedMessageMediaPhoto、decryptedMessageMediaVideo和decryptedMessageMediaFile中的key参数) 。
在将文件发送到秘密聊天室之前,会生成两个随机的 256 位数字,它们将作为 AES 密钥和初始化向量用于加密文件。同样,也会使用带有无限乱码扩展 (IGE) 的 AES-256 加密。
密钥指纹的计算方法如下:
- 摘要 = md5(密钥 + iv)
- 指纹 = substr(digest, 0, 4) XOR substr(digest, 4, 4)
加密文件的内容以与云聊天中文件存储方式大致相同的方式存储在服务器上:通过调用`upload.saveFilePart`函数逐段上传。
随后调用`messages.sendEncryptedFile`函数会为存储的文件分配一个标识符,并将文件地址与消息一起发送。接收者将收到一条包含`encryptedMessage`的更新消息,其中的`file`参数将包含文件信息。
使用构造函数inputEncryptedFile可以将传入和传出的加密文件转发到其他秘密聊天,以避免在服务器上保存相同的内容两次。
使用更新框
秘密聊天与特定设备(或者更确切地说是授权密钥)关联,而非用户。传统的消息框使用pts来描述客户端状态,因此并不适用,因为它旨在长期存储消息并允许从不同设备访问消息。
为了解决这个问题,引入了一个额外的临时消息队列。当发送有关秘密聊天消息的更新时,会发送一个新的qts值,这有助于在连接长时间中断或更新丢失的情况下重建差异。
随着事件数量的增加,qts的值每增加一个新事件就增加 1。初始值可能不等于(也不会等于)0。
客户端会通过调用`messages.receivedQueue`方法显式确认已接收并存储临时队列中的事件,或者通过调用`updates.getDifference`方法隐式确认(传入的是`qts`的值,而非最终状态)。所有已被客户端确认已送达的消息,以及超过 7 天的消息,都可能(并且将会)从服务器中删除。
取消授权后,相应设备的事件队列将被强制清除,qts的值将变得无关紧要。
更新到新图层
您的客户端应始终存储秘密聊天另一端客户端已知支持的最高层数。首次创建秘密聊天时,此值应初始化为 46。收到任何包含上层信息的包后,必须立即更新此远程层数值,例如:
- 任何包含layer_no且layer>= 46 的decryptedMessageLayer秘密聊天消息,或
- 解密后的 MessageActionNotifyLayer服务消息,包装方式如同已过时的第 8 层(构造函数)的decryptedMessageService构造函数一样decryptedMessageService#aa48327d。
通知远程客户端有关您的本地层的信息
为了通知远程客户端你的本地层,你的客户端必须发送指定decryptedMessageActionNotifyLayer类型的消息。此通知必须封装在相应层的构造函数中。
在以下两种情况下,客户端必须将其本地层的信息通知远程客户端:
- 一旦新的秘密聊天创建完成,密钥交换成功后,就会立即开始聊天。
- 本地客户端更新以支持新的秘密聊天层后,必须立即向所有现有秘密聊天发送通知。请注意,只有在更新到包含秘密聊天实现更改的新层时才需要这样做(例如,当客户端从第 46 层更新到第 47 层时,则无需执行此操作)。