电报 MTProto 移动协议:详细描述(版本 1.0,已弃用)

本文档描述的是 MTProtov.1.0,其状态为已弃用。有关最新 Telegram 客户端中使用的加密信息,请参阅此文档

在通过网络使用传输协议传输消息(或多部分消息)之前,消息会以某种方式加密,并在消息顶部添加一个外部标头,该标头包含:一个 64 位密钥标识符(唯一标识服务器和用户的授权密钥)和一个 128 位消息密钥。

用户密钥与消息密钥共同构成一个实际的 256 位密钥和一个 256 位初始化向量,该密钥和初始化向量用于使用带有无限混叠扩展 (IGE) 的 AES-256 加密算法对消息进行加密。请注意,待加密消息的初始部分包含可变数据(会话、消息 ID、序列号、服务器盐值),这些数据显然会影响消息密钥(进而影响 AES 密钥和初始化向量)。消息密钥定义为消息体(包括会话、消息 ID 等)SHA1 校验和的低 128 位。多部分消息会被合并为一个消息进行加密。

MTProto 服务器-客户端加密、云聊天

术语

授权密钥

客户端设备和服务器共享一个 2048 位密钥,该密钥在用户注册时直接在客户端设备上通过交换 Diffie-Hellman 密钥生成,并且绝不会通过网络传输。每个授权密钥都与用户绑定。用户可以拥有多个密钥(对应于不同设备上的“永久会话”),并且如果设备丢失,其中一些密钥可能会被永久锁定。另请参阅“创建授权密钥”。

服务器密钥

服务器使用一个 2048 位 RSA 密钥对自身消息进行数字签名,该密钥用于在注册过程中生成授权密钥时进行签名。应用程序内置一个公钥,可用于验证签名,但不能用于对消息进行签名。服务器端存储一个私钥,该私钥很少更改。

密钥标识符

授权密钥的 SHA1 哈希值的低 64 位用于指示哪个特定密钥用于加密消息。密钥必须由其 SHA1 哈希值的低 64 位唯一确定,如果发生冲突,则会重新生成授权密钥。密钥标识符为零表示未使用加密,这在注册期间用于生成基于 Diffie-Hellman 密钥交换的授权密钥的有限消息类型中是允许的。

会议

客户端生成一个(随机的)64 位数字,用于区分不同的会话(例如,使用同一授权密钥创建的不同应用程序实例)。会话及其密钥标识符对应一个应用程序实例。服务器可以维护会话状态。任何情况下,都不能将原本发往某个会话的消息发送到其他会话。服务器可以单方面删除任何客户端会话;客户端应该能够处理这种情况。

服务器盐

一个随机的 64 位数字,会定期(例如每 24 小时)根据服务器的请求进行更改(每个会话单独更改)。所有后续消息都必须包含新的盐值(尽管包含旧盐值的消息仍会被接受 300 秒)。此机制旨在防止重放攻击以及某些将客户端时钟调整到遥远未来某个时刻的攻击手段。

消息标识符 (msg_id)

一个(随时间变化的)64 位数字,用于在会话中唯一标识一条消息。客户端消息标识符必须能被 4 整除,服务器消息标识符对 4 取模的结果为 1(如果消息是对客户端消息的响应),否则为 3。客户端消息标识符必须像服务器消息标识符一样单调递增(在单个会话内),并且必须近似等于 unixtime*2^32。这样,消息标识符就指向消息创建的大致时间点。消息在创建后超过 300 秒或创建前 30 秒将被拒绝(这是为了防止重放攻击)。在这种情况下,必须使用不同的标识符重新发送消息(或将其放入标识符更高的容器中)。消息容器的标识符必须严格大于其嵌套消息的标识符。

重要提示:为防止重放攻击,客户端传递的msg_id的低 32 位不能为空,且必须是消息创建时间点的整数部分。在不久的将来,服务器将开始忽略msg_id低 32 位包含过多零的消息。

内容相关消息

需要明确确认的消息。这些消息包括所有用户消息和许多服务消息,几乎涵盖除容器消息和确认消息之外的所有消息。

消息序列号 (msg_seqno)

一个 32 位数字,等于发送方在此消息之前创建的“内容相关”消息(需要确认的消息,特别是非容器消息)数量的两倍;如果当前消息是内容相关消息,则该数字加一。容器始终在其所有内容生成之后才创建;因此,其序列号大于或等于其中包含的消息的序列号。

消息键

要加密的消息部分的 SHA1 哈希值的低 128 位(包括内部标头,不包括对齐字节)。

内部(加密)头部

在消息或容器加密之前,会在消息或容器之前添加一个头部(16 字节)。头部由服务器盐值(64 位)和会话信息(64 位)组成。

外部(加密)标头

在加密消息或容器之前添加的头部(24 字节)。它由密钥标识符(64 位)和消息密钥(128 位)组成。

有效载荷

外部头部 + 加密消息或容器。

定义 AES 密钥和初始化向量

使用 2048 位授权密钥 (auth_key) 和 128 位消息密钥 (msg_key) 计算 256 位 AES 密钥 (aes_key) 和 256 位初始化向量 (aes_iv),随后使用 AES-256 的无限乱码扩展 (IGE) 模式对要加密的消息部分(即除稍后添加的外部标头之外的所有内容)进行加密。

根据 auth_key 和 msg_key 计算 aes_key 和 aes_iv 的算法如下:

其中 x = 0 表示客户端向服务器发送消息,x = 8 表示服务器向客户端发送消息。

auth_key 的低 1024 位不参与计算。它们可以(与其余位一起或单独)用于客户端设备,以加密从服务器接收的数据的本地副本。auth_key 的低 512 位不存储在服务器上;因此,如果客户端设备使用它们来加密本地数据,而用户丢失了密钥或密码,则无法解密本地数据(即使可以从服务器获取数据)。

当使用 AES 加密长度不能被 16 字节整除的数据块时,在加密之前,会用随机字节将数据填充到能被 16 字节整除的最小长度。

重要测试

当收到加密消息时,必须检查 msg_key 是否确实等于先前加密部分的 SHA1 哈希的低 128 位,并且 msg_id 对于从客户端到服务器的消息具有偶校验,对于从服务器到客户端的消息具有奇校验。

此外,必须存储从对端收到的最后 N 条消息的标识符 (msg_id)。如果收到消息,其 msg_id 小于或等于任何已存储的值,则忽略该消息。否则,将新消息的 msg_id 添加到集合中。如果已存储的 msg_id 值数量大于 N,则删除最早的(即最小的)msg_id。

此外,应忽略超过 30 秒后或 300 秒前的 msg_id 值。这一点对服务器尤为重要。客户端也会发现此功能很有用(可以防止重放攻击),但前提是客户端的时间是确定的(例如,其时间已与服务器时间同步)。

某些包含客户端发送给服务器的数据(例如,最近客户端查询的 msg_id)的客户端到服务器服务消息,即使时间看起来“不正确”,也可能在客户端上被处理。更改 server_salt 的消息和无效客户端时间的通知尤其如此。参见移动协议:服务消息。

在客户端设备上存储授权密钥

对于注重安全性的用户,建议他们以类似于 SSH 的方式对授权密钥进行密码保护。具体做法是,在密钥前面加上 SHA1 值,然后使用 CBC 模式的 AES 算法和与用户(文本)密码相同的密钥对整个字符串进行加密。当用户输入密码时,系统会解密已加密的密码,并通过与 SHA1 值进行比较来验证密码的有效性。从用户的角度来看,这与使用应用程序或网站密码几乎相同。

未加密消息

可以使用特殊的纯文本消息来创建授权密钥以及执行时间同步。这些消息以 `auth_key_id = 0`(64 位)开头,表示没有授权密钥。紧接着是序列化格式的消息体,不包含内部或外部头部。消息体之前会添加消息标识符(64 位)和消息体长度(32 字节)。

只有极少数特殊类型的消息可以以纯文本形式传输。

信息示意图

加密消息

auth_key_id
int64
msg_key
int128
加密数据
字节

加密消息:encrypted_data

包含以下数据的密文:


int64
session_id
int64
message_id
int64
序列号
int32
消息数据长度
int32
消息数据
字节
填充0..15
字节

未加密消息

auth_key_id=0
int64
message_id
int64
消息数据长度
int32
消息数据
字节

创建授权密钥

通常情况下,在应用程序安装过程中,注册之前会为每个用户生成一个授权密钥。实际上,注册操作是在授权密钥生成之后进行的。但是,在后台生成授权密钥的同时,系统可能会提示用户填写注册表单。用户按键之间的间隔可以作为生成高质量随机数的熵源,而高质量随机数是创建授权密钥所必需的。

请参阅“创建授权密钥”。

在创建授权密钥期间,客户端会获取其服务器盐值(该盐值将与新密钥一起用于近期内的所有通信)。然后,客户端使用新生成的密钥创建一个加密会话,后续通信(包括传输用户注册信息和验证电话号码)均在该会话内进行,除非客户端创建新会话。客户端可以随时通过选择新的随机 session_id 来创建新会话或添加其他会话。