电报 MTProto 移动协议:详细描述

从 4.6 版本开始,主流 Telegram 客户端均已使用MTProto 2.0。MTProto v.1.0 已被弃用,目前正在逐步淘汰。

本文介绍了MTProto协议2.0版本(云聊天、服务器-客户端加密)的基础层。与MTProto协议 1.0版本(此处仅供参考)的主要区别如下:

另请参阅:MTProto 2.0:秘密聊天、端到端加密

协议描述

在通过传输协议在网络上传输消息(或多部分消息)之前,会以某种方式对其进行加密,并在消息顶部添加一个外部标头,该标头由一个 64 位密钥标识符auth_key_id(唯一标识服务器和用户的授权密钥)和一个 128 位消息密钥msg_key组成。

授权密钥auth_key与消息密钥msg_key结合,定义了一个实际的 256 位密钥aes_key和一个 256 位初始化向量aes_iv,它们用于以无限混叠扩展 (IGE) 模式使用 AES-256 加密对消息进行加密。需要注意的是,待加密消息的初始部分包含可变数据(会话、消息 ID、序列号、服务器盐值),这些数据显然会影响消息密钥(进而影响 AES 密钥和初始化向量)。在MTProto 2.0中,消息密钥定义为消息体(包括会话、消息 ID、填充等)SHA-256 的中间 128 位,并加上从授权密钥中提取的 32 个字节作为前缀。在较早的MTProto 1.0中,消息密钥计算为消息体 SHA-1 的低 128 位,不包括填充字节。

多部分消息会被加密成一条消息。

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

对这个设置有疑问?——请查看高级常见问题解答

注1

MTProto 中待加密的每条明文消息始终包含以下数据,以便在解密时进行检查,从而使系统能够抵御组件的已知问题:

注2

Telegram 的端到端加密秘密聊天功能在上述加密方式的基础上增加了一层加密。详情请参阅“秘密聊天,端到端加密” 。

MTProto在云聊天秘密聊天中均支持完美前向保密

术语

授权密钥(auth_key)

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

服务器密钥

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

密钥标识符(auth_key_id)

授权密钥的 SHA1 哈希值的低 64 位用于指示哪个特定密钥用于加密消息。密钥必须由其 SHA1 哈希值的低 64 位唯一确定,如果发生冲突,则会重新生成授权密钥。密钥标识符为零表示未使用加密,这在 Diffie-Hellman 密钥交换中用于生成授权密钥的注册期间,仅允许使用有限的消息类型。对于 MTProto 2.0,此处仍然使用 SHA1,因为 auth_key_id 应独立于协议版本来标识所使用的授权密钥。

会议

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

服务器盐

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

消息标识符 (msg_id)

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

重要提示:为了防止重放攻击,客户端传递的msg_id的低 32 位不能为空,并且必须表示消息创建时间点的分数部分。

消息序列号 (msg_seqno)

一个 32 位数字,等于发送方在此消息之前创建的内容相关消息数量的两倍,如果当前消息是内容相关消息,则该数字随后递增 1。

因此,内容相关消息的序列号为msg.seqNo = (current_seqno*2)+1(生成后,本地current_seqno计数器必须加 1),非内容相关消息的序列号为msg.seqNo = (current_seqno*2)(生成后current_seqno不得加1)。

因此,可以通过检查消息的 seqno 的最低有效位的值来简单地确定传入的 MTProto 消息的内容相关性(message.isContentRelated = (message.seqNo & 1) == 1)。

容器总是在其所有内容之后生成;因此,它的序列号大于或等于其中包含的消息的序列号。

内容相关消息

当收到一条 MTProto 消息,该消息被标记为与内容相关(通过设置seqno的最低有效位),接收方必须以某种方式确认该消息。

当接收方是客户端时,必须通过msgs_ack构造函数来实现。

当接收方是服务器时,这通常是通过msgs_ack构造函数完成的,但也可以通过方法的回复、错误或其他方式完成,具体方式由每个方法或构造函数的文档规定。

当使用 TCP 传输时,构造函数的内容相关性会影响服务器的行为:如果当前连接关闭然后重新打开,服务器会将尚未确认的内容相关消息重新发送到新连接。

客户端必须始终将所有 API 级别的 RPC 查询标记为内容相关,否则将发出bad_msg_notification 。error_code=35

客户端绝不能将msgs_ack构造函数msg_container(即容器msg_copy和确认)标记为内容相关,否则将发出bad_msg_notification 。gzip_packederror_code=34

客户端可以将除上述四个构造函数之外的任何其他构造函数标记为内容相关,以便在出现网络问题时通过向服务器请求确认来提高可靠性。

消息键(msg_key)

在MTProto 2.0中,要加密的消息的 SHA-256 哈希的中间 128 位(包括内部标头和MTProto 2.0 的填充字节),前面加上 32 字节的授权密钥片段。

在MTProto 1.0中,消息密钥的定义有所不同,它是待加密消息的 SHA-1 哈希值的低 128 位,计算哈希值时排除了填充字节。授权密钥不参与此计算。

内部(加密)头部

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

外部(加密)标头

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

有效载荷

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

定义 AES 密钥和初始化向量

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

对于 MTProto 2.0,从 auth_key 和 msg_key 计算 aes_key 和 aes_iv 的算法如下。

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

对于已过时的 MTProto 1.0,msg_key、aes_key 和 aes_iv 的计算方式有所不同(请参阅此文档以了解详情)。

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

在 MTProto 1.0 中,当使用 AES 加密长度不能被 16 字节整除的数据块时,加密前会用 0 到 15 个随机填充字节random_padding将数据填充到 16 字节的整除长度。在 MTProto 2.0 中,计算加密长度时会考虑此填充msg_key。请注意,MTProto 2.0 需要 12 到 1024 字节的填充,但仍然需要满足最终消息长度能被 16 字节整除的条件。

使用 MTProto 2.0 而不是 MTProto 1.0

客户端在同一 TCP 连接中只能使用 MTProto 2.0 或 MTProto 1.0。服务器会检测从客户端收到的第一条消息所使用的协议,然后对其消息使用相同的加密方式,并期望客户端此后也使用相同的加密方式。我们建议使用 MTProto 2.0;MTProto 1.0 已被弃用,仅为向后兼容而保留。

重要检查

当收到加密消息时,必须检查msg_key是否确实等于解密数据的 SHA-256 中间 128 位,并在其前面加上 32 字节的auth_key片段,并且 msg_id 对于从客户端到服务器的消息具有偶校验,对于从服务器到客户端的消息具有奇校验。

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

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

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

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

对于注重安全性的用户,建议他们以类似于 SSH 的方式对授权密钥进行密码保护。具体做法是,在密钥前面加上一个加密哈希函数(例如 SHA-256)的值,然后使用 CBC 模式的 AES 算法和与用户(文本)密码相同的密钥对整个字符串进行加密。当用户输入密码时,系统会解密并验证存储的受保护密码,方法是检查 SHA-256 值。从用户的角度来看,这与使用应用程序或网站密码几乎相同。

未加密消息

可以使用特殊的纯文本消息来创建授权密钥以及执行时间同步。这些消息以 `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
消息数据
字节
填充12..1024
字节

未加密消息

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

MTProto 2.0 使用 12 到 1024 个填充字节,而不是 MTProto 1.0 中使用的 0 到 15 个填充字节。

创建授权密钥

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

请参阅“创建授权密钥”

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