MTProto协议第一部分: 云聊天

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_idAuthorization 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_keymsg_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 KeySHA1散列值的低64位, 用于指出, 哪一个Authorization Key用于解密消息.

    • 秘钥必须通过Authorization KeySHA1散列值的低64位唯一地标识.

    • 当发生冲突的时候, Authorization Key 必须重新生成.

    • Key Identifier0 的时候表示不使用加密, 一般限制地用于几个消息类型, 比如注册的时候基于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)任意创建新的或额外的会话(多个应用程序实例需要创建不同的会话).

你可能感兴趣的:(telegram,elixir)