https://core.telegram.org/mtp...
云聊天(服务器-客户端加密)
图例分析
共享秘钥(
auth_key
), 持久化的, 通过DH生成实际的数据总是包含时间, 长度, 序列, 接收端应该在解密后检查这几个字段.
msg_key
为 Server Salt, Session ID, 和实际的数据Payload
的SHA1值的低128位.整个协议包嵌入到传输层(TCP,HTTP)
解密后
msg_key
必须等于获取到的数据的SHA-1 ?
嵌入到TCP, 或HTTP中, 解密后 msg_key
必须等于接受到的数据的 SHA1
值.
服务器和客户端的二进制通信包的基本结构为
+------------+---------+----------------+
|auth_key_id | msg_key | encrypted_data |
+------------+---------+----------------+
其中
应为
Authorization Key
并不会在网络中传输, 我们需要一个方式来的到Authorization Key
,auth_key_id
为Authorization Key
的低64
位, 用于唯一的标识一个Authorization Key
用于加密消息
在 MTProto
协议中, 每一个被加密的明文消息总是包含下面的组件, 在解密的时候应当检查的, 为了保证系统的足够的健壮.
注: 1
component | description |
---|---|
server salt |
服务器盐 |
session id |
会话ID |
message sequence number |
消息序列号 |
message length |
消息长度 |
time |
时间 |
注: 2
定义AES秘钥和初始化向量
2048位的授权秘钥(Authorization Key, auth_key
)和128位的消息秘钥(Message Key, msg_key
)用于计算一个256为的AES秘钥(aes_key
)以及一个256为的初始化向量(aes_iv
), 他们随后用于加密协议包的消息正文部分.(例如, 除了外部头的所有其他部分), 加密模式为IGE(Infinite Garble Extension, 无限混淆扩展)模式.
从 auth_key
和 msg_key
, 计算aes_key
, aes_iv
, 计算方法如下:
sha1_a = SHA1 (msg_key + substr (auth_key, x, 32));
sha1_b = SHA1 (substr (auth_key, 32+x, 16) + msg_key + substr (auth_key, 48+x, 16));
sha1_с = SHA1 (substr (auth_key, 64+x, 32) + msg_key);
sha1_d = SHA1 (msg_key + substr (auth_key, 96+x, 32));
aes_key = substr (sha1_a, 0, 8) + substr (sha1_b, 8, 12) + substr (sha1_c, 4, 12);
aes_iv = substr (sha1_a, 8, 12) + substr (sha1_b, 0, 8) + substr (sha1_c, 16, 4) + substr (sha1_d, 0, 8);
x
为字节偏移量, 消息的方向不同, x
的值不同.
其中 x =0
时, 消息为客户端到服务器的消息(Client->Server
), x = 8
是为从服务器到客户端的消息(Server->Client
). auth_key
的低1024位在计算中不涉及加密, 他们可以用于客户端设备加密从服务器端接收到的本地数据. auth_key
的低512位不存储在服务器; 因此, 如果客户端设备使用他们来加密本地数据, 并且用户丢失了秘钥或密码, 数据是不可能解密的(即使可以从服务器获取数据)
当AES用于加密一个不能被16整除的数据块, 在数据被加密之前, 整个数据块的长度必须被补齐到16的整数倍.
下面是Elixir的实现
def prepare_aes(:recv, auth_key, msg_key) do
<<
key1::binary-size(32),
key2::binary-size(16),
key3::binary-size(16),
key4::binary-size(32),
key5::binary-size(32),
_::binary
>> = auth_key
sha1_a = <> |> sha1()
sha1_b = <> |> sha1()
sha1_c = <> |> sha1()
sha1_d = <> |> sha1()
<> = sha1_a
<> = sha1_b
<<_sha1_c_0::binary-size(4), sha1_c_1::binary-size(12), sha1_c_2::binary-size(4)>> = sha1_c
<> = sha1_d
aes_key = <>
aes_iv = <>
{:ok, aes_key, aes_iv}
end
@spec prepare_aes(atom, binary, binary) :: {:ok, term, term}
def prepare_aes(:send, auth_key, msg_key) do
<<
_::binary-size(8),
key1::binary-size(32),
key2::binary-size(16),
key3::binary-size(16),
key4::binary-size(32),
key5::binary-size(32),
_::binary
>> = auth_key
sha1_a = <> |> sha1()
sha1_b = <> |> sha1()
sha1_c = <> |> sha1()
sha1_d = <> |> sha1()
<> = sha1_a
<> = sha1_b
<<_sha1_c_0::binary-size(4), sha1_c_1::binary-size(12), sha1_c_2::binary-size(4)>> = sha1_c
<> = sha1_d
aes_key = <>
aes_iv = <>
{:ok, aes_key, aes_iv}
end
重要的测试
当接收到一个加密的消息时, 必须坚持 msg_key
实际上等于之前加密部分的SHA1散列的低128位, 并从客户端到服务器的消息的msg_id
为奇数校验
, 从服务器到客户的消息为偶数校验
...
在客户端设备上存储授权秘钥(Authorization Key)
可以建议用户关注安全问题, 几乎以SSH相同的方对秘钥进行密码保护. 这是通过添加秘钥的SHA1到Key的前面, 随后整个字符串使用AES的CBC模式加密, 然后是一个等于用户密码的Key. 当用户输入密码时, 存储的保护密码被解密并且通过和SHA1对比进行校验. 从用户的角度, 这实际上和使用一个应用程序或一个Web站点密码是一样的.
非加密消息
特殊的纯文本消息可以用于创建授权秘钥以及执行时间同步. 当 auth_key_id = 0 (64 位)
是, 表示不存在auth_key
. 这种情况是直接后跟序列号格式的消息体, 而不包含内部或外部头. 一个消息标识符(message Identifier 64位) 以及消息体字节长度(32字节)添加到消息体之前.
仅有少量的特殊类型的消息能通过明文传输.
加密组件说明
消息的语义表示
-
Authorization Key
一个由
2048
位整数数字, 是服务器和客户端握手的时候, 通过Diffie-Hellman 秘钥交换
算法由服务器和客户端同时计算出来的.它不会在网络中传输.
每一个
Authorization Key
是和用户相关的.没有什么东西能够阻止用于拥有多个秘钥
(官方英文直译, 也就是说一个用户可以有多个秘钥, 每一个Authorization Key
对应于不同设备上的"持久会话"
).设备丢失的时候, 某些秘钥可以永远被屏蔽.
-
Server Key
一个
2048
位RSA秘钥, 由服务器生成, 用于签名消息应用程序有内置的服务器公钥用于验证一个签名, 但是不能用于签名消息
服务器私钥存储在服务器上, 并且不常改变.
-
Key Identifier
Authorization Key
的SHA1
散列值的低64
位, 用于指出, 哪一个Authorization Key
用于解密消息.秘钥必须通过
Authorization Key
的SHA1
散列值的低64
位唯一地标识.当发生冲突的时候,
Authorization Key
必须重新生成.Key Identifier
为0
的时候表示不使用加密, 一般限制地用于几个消息类型, 比如注册的时候基于Diffie-Hellman 秘钥交换
生成Authorization Key
的时候, 以及时间同步操作.
-
Session
一个由客户端生成的用于区分不同的会话的随机
64
数字(例如, 用相同Authorization Key
创建的一个应用程序的两个不同实例)一个
Session
和一个Key Identifier
用于唯一的标识一个应用程序实例.服务器可以维护会话状态, 并且保证了一个会话中的消息不会发送到另一个会话中.
服务器可以单方面删除任何客户端会话, 客户端应该能够处理这种情况.
-
Server Salt
定期变更(比如, 每24小时)的随机64位数字
对于每个会话都是单独的, 因此需要关联到每一个单独的会话, 所有随后的消息都应该使用这个新的Salt,
(尽管如此, 旧的Salt还是能够在300秒内是可接受的, 目的是保护重放攻击, 以及客户端时间欺骗(调整系统时钟到未来很久)
-
Message Identifier
(msg_id
)一个依赖于时间的
64
位数字, 用于唯一地标识会话中的一条消息.客户端消息标识能够被4整除
如果从服务器发送给客户端的消息是对客户端的应答消息, 消息标识
除以4的余数为1
如果消息不是对客户端请求的应答, 那么消息ID
除以4余数为3
客户端消息必须在一个会话内单向递增.
服务器消息也是一样的需要在一个会话内单向递增. 并且必须近似的等于
unixtime*2^32
. 通过这种方法, 一个消息标识指向一个消息创建时的近似时间.超过创建后300秒, 或超过创建前30秒(为了防止重放攻击, 这是要求的), 消息被拒绝.
这种情况下, 消息必须用一个不同的标识重新发送.(或放在一个具有更高消息标识的容器中).
加密消息, 非加密消息
内容相关的消息
一个要求明确应答的消息. 包括所有用户和多数服务消息, 实际上是所有的除了容器和确认消息本身的其他消息.
Message Sequence Number
(msg_seqno
)
一个32位数字,等于内容相关消息的两倍(这些要求确认, 特别是那些非容器消息)Message Key
一个被加密消息(包括内部头, 不包括对齐字节)SHA1散列的低128
位Internal (cryptographic) Header
内部加密头, 一个添加到消息或容器前的16字节头部, 包含Server Salt
(64位) 和Sessino
(64位)External (cryptographic) Header
外部加密头(24字节), 添加到加密消息之前或容器之前. 包含Key Identifier
(64位), 和一个Message Key
(128位)Payload
外部头 + 加密消息(或容器)
创建授权秘钥
Authorization Key
在客户端应用程序安装的时候自动生成, 注册过程发生在 Authorization Key
生成自后. 用户被提示完成注册表单, 同时Authorization Key
在后台生成.
用户键盘输入的间隔用于生成高质量的随机数的熵的来源, 随机数用于 Authorization Key
的创建.
在 Authorization Key
创建过程中, 客户端获取其服务器的盐(Server Salt), 用于在将来和新的秘钥一起进行所有的通信. 客户端然后使用新生成的秘钥创建一个加密的会话, 随后的通信都在这个会话当中(包括用户注册信息的传输, 电话号码验证), 除非客户端创建了一个新的会话. 客户端可以在任何时候通过选择一个新的随机的会话ID(session_id
)任意创建新的或额外的会话(多个应用程序实例需要创建不同的会话).