telethon代码分析与TL的实现1
前言
之前试用了官方的tdLib包,能调通,但是还是不如telethon包简单易用,python改起来还是十分容易的。
开源代码地址:https://github.com/LonamiWebs/Telethon/
使用方法:
from telethon import TelegramClient, events, sync
import socks
# These example values won't work. You must get your own api_id and
# api_hash from https://my.telegram.org, under API Development.
#api_id = 12345
#api_hash = '0123456789abcdef0123456789abcdef'
api_id = 94575 # 从tdlib中拷贝的参数
api_hash = "a3406de8d171bb422bb6ddf3bbd800e2"
proxy1 = ("socks5", '127.0.0.1', 1081)
client = TelegramClient('session_name', api_id, api_hash, proxy=proxy1)
client.start()
能运行到提示输入手机号,就是能连通了!
关于TL 语言的详细文档:https://core.telegram.org/mtproto/TL
TL类似protobuf实现了数据格式以及RPC调用方式的定义,
但是语法以及思路差别很大,
- 构造器:用于定义一个数据类型,表示序列化以及反序列化方法;
- 方法:描述被调用的方法有哪些参数,参数的序列化方式,以及返回类型,返回类型对应这构造器;
构造器和方法都是使用类似方式定义,定义的语句可以使用CRC32做哈希,得到一个32位的整数,用于唯一标记这个方法或者构造器;
所以每个语言的客户端需要做的第一件事就是读懂TL规则,编写一个TL编译器,按照官方公布的当前版本的TL来生成TL部分代码,用于RPC以及数据编解码;
telethon_generator\data\api.tl
telethon_generator\data\mtproto.tl
一、TL对象与反序列化分析
在TL描述的协议下,所有消息都是是一个TLObject,而发起的所有请求,都可以认为是一个TLRequest,首先我们追究一下到底如何封装以及反序列化相关的数据类。
在github源码中有代码生成器源码,是根据相关的规则生成TL包相关的文件,否则手写会把手写断,而且官方的文档一直在更新;
TL语言主要是类似protobuf实现对象序列化,反序列化以及RPC过程,首先我们需要一个通用的文件定义最基本的规则:
参见代码:telethon\extensions\binaryreader.py
此文件是手动写的,而不是自动生成的部分,包括了最基本的序列化与反序列化协议:
tgread_object方法是所有工作的开始阶段,所以需要先阅读这个方法:
1.1 从二进制中读取对象的方法
def tgread_object(self):
"""Reads a Telegram object."""
# 4字节小端的int作为构造器编号
constructor_id = self.read_int(signed=False)
# telethon\tl\alltlobjects.py 文件定义了所有的对象构造器和方法,封装为一个字典
# 搜索目前是否有这个构造器
clazz = tlobjects.get(constructor_id, None)
if clazz is None:
# 如果找不到对应的号码,尝试解析一些最基本的类型,
# http://crc32.bchrt.com/ 计算工具
#
value = constructor_id
if value == 0x997275b5: # boolTrue
return True
elif value == 0xbc799737: # boolFalse crc32('boolFalse = Bool')
return False
# crc32("vector t:Type # [ t ] = Vector t") = 0x1cb5c415
elif value == 0x1cb5c415: # Vector
return [self.tgread_object() for _ in range(self.read_int())]
clazz = core_objects.get(constructor_id, None)
if clazz is None:
# 实在找不到,就后退4个字节!!
self.seek(-4) # Go back
pos = self.tell_position()
error = TypeNotFoundError(constructor_id, self.read())
self.set_position(pos)
raise error
return clazz.from_reader(self) # 每个类型都实现了classMethod,从二进制构造自己
备注:
0x1cb5c415 是vector类型,需要单独标记!!
上面的代码首先从字节流中读取一个4字节整数,根据(代码生成器产生的)字典查找构造器,用构造器去读取后面的数据;
其中内置的几种类型是单独处理的,因为:
-
BOOL值没有后面的部分,只有构造器;
-
如果是vector比较特殊,需要读出个数,写一个循环,读出N个元素,放到数组中返回;
最后,如果无法匹配标识符,那就是出错了,一般最大的原因就是当前的TL与对方使用的不兼容!
1.2 反序列化举例
这里仅仅举例之前协商密钥过程中的一次RPC过程,这里的类代码是生成器自动生成的,我们来学习一下需要生成哪些东西:
客户调用方法:
req_pq#60469778 nonce:int128 = ResPQ;
服务端使用构造器构造返回数据:
resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector<long> = ResPQ;
先看方法:我们搜索0x60469778,找到了:
0x60469778: functions.ReqPqRequest,
该函数定义为:
class ReqPqRequest(TLRequest):
CONSTRUCTOR_ID = 0x60469778
SUBCLASS_OF_ID = 0x786986b8
# 这里有个特殊性,python的int可以认为是无限大,所以并不是4字节
def __init__(self, nonce: int):
"""
:returns ResPQ: Instance of ResPQ.
"""
self.nonce = nonce
# 将自身转为字典
def to_dict(self):
return {
'_': 'ReqPqRequest',
'nonce': self.nonce
}
# 将自身转为二进制字节流,先拼接4字节函数ID,然后是参数列表,一共20字节
def _bytes(self):
return b''.join((
b'x\x97F`', #4个字节 'x', 0x97, 'F', '`', 也就是0x78,0x97, 0x46, 0x60
self.nonce.to_bytes(16, 'little', signed=True), # 128比特,小端整数
))
# 反序列化很简单,直接读16字节作为小整数
@classmethod
def from_reader(cls, reader):
_nonce = reader.read_large_int(bits=128)
return cls(nonce=_nonce)
这里的读128比特的函数为binaryreader.py中的:
def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value."""
return int.from_bytes(
self.read(bits // 8), byteorder='little', signed=signed)
之后我们看此函数的返回数据如何构造:
telethon\tl\types_init_.py
class ResPQ(TLObject):
CONSTRUCTOR_ID = 0x5162463
SUBCLASS_OF_ID = 0x786986b8
def __init__(self, nonce: int, server_nonce: int, pq: bytes, server_public_key_fingerprints: List[int]):
"""
Constructor for ResPQ: Instance of ResPQ.
"""
self.nonce = nonce
self.server_nonce = server_nonce
self.pq = pq
self.server_public_key_fingerprints = server_public_key_fingerprints
def to_dict(self):
return {
'_': 'ResPQ',
'nonce': self.nonce,
'server_nonce': self.server_nonce,
'pq': self.pq,
'server_public_key_fingerprints': [] if self.server_public_key_fingerprints is None else self.server_public_key_fingerprints[:]
}
def _bytes(self):
return b''.join((
b'c$\x16\x05', # 0x5162463的小端形式表示类型
self.nonce.to_bytes(16, 'little', signed=True), # 16字节
self.server_nonce.to_bytes(16, 'little', signed=True), # 16字节
self.serialize_bytes(self.pq), # 这里是父类中的序列化字符串方法
b'\x15\xc4\xb5\x1c', # 0x1cb5c415 是vector类型
# 之后是4字节小端类型的int 元素个数
struct.pack('<i',len(self.server_public_key_fingerprints)),
# 逐个序列化8字节的long类型元素
b''.join(struct.pack('<q', x) for x in self.server_public_key_fingerprints),
))
@classmethod
def from_reader(cls, reader):
_nonce = reader.read_large_int(bits=128)
_server_nonce = reader.read_large_int(bits=128)
_pq = reader.tgread_bytes()
reader.read_int()
_server_public_key_fingerprints = []
for _ in range(reader.read_int()):
_x = reader.read_long()
_server_public_key_fingerprints.append(_x)
return cls(nonce=_nonce, server_nonce=_server_nonce, pq=_pq, server_public_key_fingerprints=_server_public_key_fingerprints)
备注:关于如何使用struct读写二进制数据,参考:https://blog.csdn.net/qq_30638831/article/details/80421019?spm=1001.2014.3001.5506
备注:每个类都是从TLObject派生的子类,所以这个类封装了最基本的一些方法,基础类型如何序列化,比如字符串如何序列化等。(telethon\tl\tlobject.py 此类也是手工编写的基础类;)
总结:每个类都有:
- 一个静态方法作为工厂函数;
- 构造函数实现输入参数;
- 实现序列化
- 实现转为字典类型
1.3 基础类型的读写
1.3.1 基础类型与复合类型的序列化
1.3.1.1 封装
官方文档https://core.telegram.org/mtproto/serialize认为数据可以分为纯值类型(Bare type)以及封装类型(Boxed type)两大类:
-
封装类型首字母大写,序列化时候:首先就是类型的标识符,然后才是数据,
-
纯值类型小写首字符,序列化时候,不加类型标识符;
-
%X可以用来表示X对应的纯值类型:x
大型的数组,如果使用封装方式将会每个元素都带有一个标识符,浪费存储空间,也浪费带宽,所以就使用对应的纯值类型来表示更合理!
比如
int_couple int int = IntCouple
int_couple
等价于%int_couple
和%IntCouple
一个整数对:
3, 4
如果用封装类型表示:假如intCouple对应标识符是404,那么
404 3 4
这里404并不是真实的标识符,官方文档仅仅是为了举例,标识符是使用CRC32计算出来的。
1.3.1.2 基础类型
基础类型包括,同时有封装形式以及纯值类型2种方式表示,
(int, long, double, string) 分别对应着 (Int, Long, Double, String)
int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;
-
int:小端存储,4字节;
-
long: 小端存储,8字节;
-
double: 小端存储,8字节;
-
string:见后一节,与bytes有同样的含义
但是如果上面4种类型,使用了对应的封装类型,就需要添加标识符。
标识符使用 CRC32计算:
CRC32("int ? = Int")
1.3.1.3 复合类型
官方推荐定义类型时加上字段名字,比如User 和Group,
如果不写变量名,无法识别字段含义,
user int string string = User;
group int string string = Group;
所以推荐如下方式:
user id:int first_name:string last_name:string = User;
group id:int title:string description:string = Group;
加入扩展了user,需要重定义一个构造器,但是产生的类名没有变,序列化与反序列化通过标识符就识别了不同的类型:
userv2 id:int unread_messages:int first_name:string last_name:string in_groups:vector int = User;
1.3.2 vector类型的序列化
vector可以认为是内置类型,也可以认为是复合类型Vector,
vector {t:Type} # [ t ] = Vector t;
这里类似一个模板容器,但是实际上构造器永远使用同一个标识符,
const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t”)
序列化的顺序为:
-
0x1cb5c415 4字节 是vector类型,无论其元素啥类型,这个都不变!
-
之后是4字节小端类型的int 元素个数
-
N个元素安照类型来序列化,(每个元素不包括类型)
反序列化时,根据自定义类型,已知元素具体类型,不需要存储元素类型;
与此相关的是:IntHash 和 StrHash,用于表示哈希类型,也就是键值对数组,
这里:
coupleInt {t:Type} int t = CoupleInt t;
intHash {t:Type} (vector %(CoupleInt t)) = IntHash t;
coupleStr {t:Type} string t = CoupleStr t;
strHash {t:Type} (vector %(CoupleStr t)) = StrHash t;
使用c++描述类似:
using coupleInt = std::pair<int, t>;
using IntHash<t> = std::vector<coupleInt>;
这里使用百分号%,表示数组内存储时,每个元素都不加构造标识符。
1.3.3 string(bytes)字符串序列化方法
https://core.telegram.org/mtproto/serialize
-
如果长度小于254: 用1个字节表示长度,后面是N字节的字节流;总长度最后按照4字节对齐;
-
长度大于等于254:第1字节为254,之后是3字节的小端int,后面是N字节的字节流;总长度最后按照4字节对齐;
关于填充长度:
-
如果长度小于254: 4 - (len(data) + 1) % 4
-
长度大于等于254: 4 - len(data) % 4
代码如下:
@staticmethod
def serialize_bytes(data):
"""Write bytes by using Telegram guidelines"""
if not isinstance(data, bytes):
if isinstance(data, str):
data = data.encode('utf-8')
else:
raise TypeError(
'bytes or str expected, not {}'.format(type(data)))
r = []
if len(data) < 254:
padding = (len(data) + 1) % 4
if padding != 0:
padding = 4 - padding
r.append(bytes([len(data)]))
r.append(data)
else:
padding = len(data) % 4
if padding != 0:
padding = 4 - padding
r.append(bytes([
254,
len(data) % 256,
(len(data) >> 8) % 256,
(len(data) >> 16) % 256
]))
r.append(data)
r.append(bytes(padding))
return b''.join(r)
1.4 内置类型
官方文档说明,内置了相关基础类型 :https://core.telegram.org/mtproto/TL-tl
/
//
// Common Types (source file common.tl, only necessary definitions included)
//
/
// Built-in types
int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;
// Boolean emulation
boolFalse = Bool;
boolTrue = Bool;
// Vector
vector {t:Type} # [t] = Vector t;
tuple {t:Type} {n:#} [t] = Tuple t n;
vectorTotal {t:Type} total_count:int vector:%(Vector t) = VectorTotal t;
Empty False;
true = True;
这里的内置的含义就是我们需要手动实现相关的业务逻辑,后续的功能在代码生成器中调用这些基础功能实现;
代码生成的alltlobjects.py中有1500个标识符以及对一个的类;
代码tlobject.py实现了最基础的功能;但是仅仅定义了2个抽象类,TLObject和TLRequest,
具体读取数据的工厂方法,需要各个类在各种代码中实现;
@classmethod
def from_reader(cls, reader):
1.5 小结
到这里,我们清楚了基础的调用逻辑:
- 业务层收到数据流;
- 使用数据流构造BinaryReader(data);
- 使用reader.tgread_object()作为入口函数,尝试反序列化;
- 该函数根据识别的标识符找到合适的类以及工厂函数去反序列化对象;(过程中还会使用BinaryReader的其他方法)
二、追踪登录验证过程算法实现
TelegramClient是该库给客户直接使用的类,该类继承自一大堆的父类:
-
TelegramBaseClient
-
AuthMethods,
-
AccountMethods,
-
DownloadMethods,
-
DialogMethods,
-
ChatMethods,
-
BotMethods,
-
MessageMethods,
-
UploadMethods,
-
ButtonMethods,
-
UpdateMethods,
-
MessageParseMethods,
-
UserMethods,
在当前阶段,TelegramBaseClient和AuthMethods是确切与服务器建立连接并且执行交换密钥的相关类;
相关类说明如下表,相关官方协议文档:https://core.telegram.org/mtproto/description
类 | 说明 | 文件 |
---|---|---|
AuthKey | 封装了基本的KEY的计算与管理工作 | telethon\crypto\authkey.py |
MTProtoState | 实现了数据的加密和解密工作,包括msg_id和seq_no的计算; | telethon\network\mtprotostate.py |
do_authentication函数 | 认证过程的状态机就是这里实现的!!! | telethon\network\authenticator.py |
MTProtoSender | 管理底层连接;实现最核心的密钥交换过程以及相关的与服务器交互的状态机;收线程函数;发线程函数;收消息后的消息处理事件分发函数; | telethon\network\mtprotosender.py |
MTProtoPlainSender | 交互密钥前需要使用此类发送明文; | telethon\network\mtprotoplainsender.py |
PacketCodec | 定义了一个编码和解码的接口;这是一个纯虚类,啥也没有实现; | telethon\network\connection\connection.py |
Connection | 封装asyncio.open_connection的一个基类;其实子类只是需要重新设置静态成员变量packet_codec即可;实现了TCP的基础连接功能以及收发线程的实现;上层只需要调用connect, send , recv即可; | telethon\network\connection\connection.py |
FullPacketCodec | https://core.telegram.org/mtproto#tcp-transport按照文档实现编解码;发送:4字节总长度,4字节发送计数,数据,4字节校验和;备注总长度等于12+数据长;解码时候格式一致,需要检查校验和; | telethon\network\connection\tcpfull.py |
ConnectionTcpFull | 继承自Connection,仅仅设置了FullPacketCodec类作为编解码器; | telethon\network\connection\tcpfull.py |
HttpPacketCodec | 将data使用HTTP发送出去;也从HTTP包中读取data部分; | telethon\network\connection\http.py |
ConnectionHttp | 继承自Connection,使用了HttpPacketCodec作为编解码类; | telethon\network\connection\http.py |
参考:《python抽象类 abc模块》https://zhuanlan.zhihu.com/p/508700685
《Python asyncio 异步编程》https://www.jianshu.com/p/7fd361cde22c
https://www.jianshu.com/p/eed5da9965f2
MTProtoSender是最核心的工作引擎;_connect(self)是整个开始工作后的入口函数,流程如下:
1)调用self._try_connect
尝试连接底层TCP连接(有可能经过某些其他协议封装,你懂的);
2)如果连接成功后,尝试交换密钥:self._try_gen_auth_key
;
3)尝试self._retries次后都无法连接或者交换密钥,就报错,一般都是无法连接的错误;
4)建立完逻辑连接了,就开始启动2个线程:self._send_loop()
和self._recv_loop()
;
5)这样,连接就完全建立了!
2.1 TCP连接_try_connect
实验的开始我们调用了:
client = TelegramClient('session_name', api_id, api_hash, proxy=proxy1)
client.start()
调用栈是这样的:
-
AuthMethods.start()
-
AuthMethods._start()
-
TelegramBaseClient.connect(),默认构造函数中使用
ConnectionTcpFull
类型作为底层连接类;也就是构造一个,调用self._sender.connect() -
MTProtoSender.connect(),函数内又调用了_connect(),这里就是前一部分分析的逻辑;
备注:telethon\network\connection目录定义若干TCP底层相关的类;
如上文表中描述,Telegram支持2种连接方式,这里讨论的是TCP方式的数据连接,也是默认的连接方式;
**数据包格式为:**https://core.telegram.org/mtproto/mtproto-transports#full
4B(长度) + 4B(序号)+ NBytes( data)+ 4B(CRC32)
+----+----+----...----+----+
|len.|seq.| payload |crc.|
+----+----+----...----+----+
具体代码参考:telethon\network\connection\tcpfull.py
而TCP的连接的封装在 Connection类中实现了;
至此,TCP连接完成了,TCP的数据包的封装与解包也完成了,上层业务就可以愉快的执行逻辑交互了。
备注:数据封装格式为4种:
限于篇幅,这里不再展开讨论;
2.2 密钥交换_try_gen_auth_key
在前一节完成TCP连接之后,
self._try_gen_auth_key
函数执行密钥交换过程:
-
首先创建一个MTProtoPlainSender,用于发送明文,这里需要传递之前的connection;
-
调用authenticator.do_authentication执行状态机,这里函数内部完成密钥交换;成功后,会得到一个授权密钥,以及一个时间偏差;
正如前一篇帖子已经讨论了相关的密钥交换过程:这里对照一下实现过程,
步骤1:发送16字节的一个随机数,得到服务器应答,包括(pq,server_nonce,公钥哈希),
而数据(nonce, server_nonce)后续将作为临时sessionID使用,
# Step 1 sending: PQ Request, endianness doesn't matter since it's random
nonce = int.from_bytes(os.urandom(16), 'big', signed=True)
# 这里就是使用了函数ReqPqMultiRequest构造发送数据,等于远程RPC,返回构造器res_pq类型数据,
# 这里的设计确实很精巧
res_pq = await sender.send(ReqPqMultiRequest(nonce))
assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq
if res_pq.nonce != nonce:
raise SecurityError('Step 1 invalid nonce from server')
# 这里是调用系统库,使用大端模式解析出一个大整数 p*q
pq = get_int(res_pq.pq)
需要提及的是,这里调用了ReqPqMultiRequest构造器,而不是前文我们写的ReqPqRequest,这主要是协议版本的变迁,官网目前的文档也是举例req_pq,实际目前使用的协议版本是2.0,已经使用req_pq_multi
步骤二:执行DH密钥交换,先上报新的随机数,并加密
# 分解大素数乘积,得到 p, q
p, q = Factorization.factorize(pq)
p, q = rsa.get_byte_array(p), rsa.get_byte_array(q)
# 为了后续传递加密信息,创新一个新的随机数 new_nonce,
new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True)
# 构造新的发送的数据
pq_inner_data = bytes(PQInnerData(
pq=rsa.get_byte_array(pq), p=p, q=q,
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
new_nonce=new_nonce
))
加密pq_inner_data:
rsa.py文件中,定义目前服务器使用公钥信息,通过服务返回的索引可以找到合适的公钥
# sha_digest + data + random_bytes
cipher_text, target_fingerprint = None, None
# 从服务器返回的公钥索引中,找到第一个,加密pq_inner_data
for fingerprint in res_pq.server_public_key_fingerprints:
cipher_text = rsa.encrypt(fingerprint, pq_inner_data)
if cipher_text is not None:
target_fingerprint = fingerprint
break
# 这段是为了兼容老服务器的密钥,可以忽略
if cipher_text is None:
# Second attempt, but now we're allowed to use old keys
for fingerprint in res_pq.server_public_key_fingerprints:
cipher_text = rsa.encrypt(fingerprint, pq_inner_data, use_old=True)
if cipher_text is not None:
target_fingerprint = fingerprint
break
if cipher_text is None:
raise SecurityError(
'Step 2 could not find a valid key for fingerprints: {}'
.format(', '.join(
[str(f) for f in res_pq.server_public_key_fingerprints])
)
)
# 发送的数据前2个字段是之前交换的随机数,
server_dh_params = await sender.send(ReqDHParamsRequest(
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
p=p, q=q,
public_key_fingerprint=target_fingerprint,
encrypted_data=cipher_text
))
备注:rsa.encrypt()函数执行了RSA_PAD过程,这个算法比较复杂,后续再单独讨论;
检查服务应答是否合法:包含的随机数与之前的需要一致,检查新的随机数是我们发送,防止中间人攻击:
assert isinstance(
server_dh_params, (ServerDHParamsOk, ServerDHParamsFail)),\
'Step 2.1 answer was %s' % server_dh_params
if server_dh_params.nonce != res_pq.nonce:
raise SecurityError('Step 2 invalid nonce from server')
if server_dh_params.server_nonce != res_pq.server_nonce:
raise SecurityError('Step 2 invalid server nonce from server')
if isinstance(server_dh_params, ServerDHParamsFail):
nnh = int.from_bytes(
sha1(new_nonce.to_bytes(32, 'little', signed=True)).digest()[4:20],
'little', signed=True
)
if server_dh_params.new_nonce_hash != nnh:
raise SecurityError('Step 2 invalid DH fail nonce from server')
assert isinstance(server_dh_params, ServerDHParamsOk),\
'Step 2.2 answer was %s' % server_dh_params
步骤三:计算出自己的的密钥,并与服务器核对是否一致,尝试完成交换过程,上报数据需要使用AES256_ige_encrypt加密算法处理;
此时已经获得了服务应答:但是被服务器加密了,也需要先解开密文,
stuct Server_DH_inner_data
{
int128 nonce,
int128 server_nonce,
int g,
int dh_prime, // pow(g, {a或b}) mod dh_prime
string g_a, // a需要自己珍藏
int server_time
}
// https://blog.csdn.net/robinfoxnan/article/details/127322483
# Step 3 sending: Complete DH Exchange
# 先计算出加密的密钥和初始向量
key, iv = helpers.generate_key_data_from_nonce(
res_pq.server_nonce, new_nonce
)
if len(server_dh_params.encrypted_answer) % 16 != 0:
# See PR#453
raise SecurityError('Step 3 AES block size mismatch')
# 解开应答
plain_text_answer = AES.decrypt_ige(
server_dh_params.encrypted_answer, key, iv
)
# 前20字节是校验,后面就是服务应答的结构体
with BinaryReader(plain_text_answer) as reader:
reader.read(20) # hash sum
server_dh_inner = reader.tgread_object()
assert isinstance(server_dh_inner, ServerDHInnerData),\
'Step 3 answer was %s' % server_dh_inner
if server_dh_inner.nonce != res_pq.nonce:
raise SecurityError('Step 3 Invalid nonce in encrypted answer')
if server_dh_inner.server_nonce != res_pq.server_nonce:
raise SecurityError('Step 3 Invalid server nonce in encrypted answer')
# 这里就是密钥交换的核心参数了
dh_prime = get_int(server_dh_inner.dh_prime, signed=False)
g = server_dh_inner.g
g_a = get_int(server_dh_inner.g_a, signed=False)
time_offset = server_dh_inner.server_time - int(time.time())
b = get_int(os.urandom(256), signed=False)
g_b = pow(g, b, dh_prime)
gab = pow(g_a, b, dh_prime)
这时的密钥其实就是等于gab:
auth_key = (g_a)^b mod dh_prime;
准备后参数后,需要对密钥参数进行核对:
# IMPORTANT: Apart from the conditions on the Diffie-Hellman prime
# dh_prime and generator g, both sides are to check that g, g_a and
# g_b are greater than 1 and less than dh_prime - 1. We recommend
# checking that g_a and g_b are between 2^{2048-64} and
# dh_prime - 2^{2048-64} as well.
# (https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete)
if not (1 < g < (dh_prime - 1)):
raise SecurityError('g_a is not within (1, dh_prime - 1)')
if not (1 < g_a < (dh_prime - 1)):
raise SecurityError('g_a is not within (1, dh_prime - 1)')
if not (1 < g_b < (dh_prime - 1)):
raise SecurityError('g_b is not within (1, dh_prime - 1)')
safety_range = 2 ** (2048 - 64)
if not (safety_range <= g_a <= (dh_prime - safety_range)):
raise SecurityError('g_a is not within (2^{2048-64}, dh_prime - 2^{2048-64})')
if not (safety_range <= g_b <= (dh_prime - safety_range)):
raise SecurityError('g_b is not within (2^{2048-64}, dh_prime - 2^{2048-64})')
仍然使用刚才的AES密钥进行加密
# Prepare client DH Inner Data
client_dh_inner = bytes(ClientDHInnerData(
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
retry_id=0, # TODO Actual retry ID
g_b=rsa.get_byte_array(g_b)
))
client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner
# Encryption
client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv)
# Prepare Set client DH params
dh_gen = await sender.send(SetClientDHParamsRequest(
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
encrypted_data=client_dh_encrypted,
))
服务器应答后,如果正确,双方协商就是达成一致了,
格式如下:
struct dh_gen_ok
{
int128 nonce; // 标记会话
int128 server_nonce; // 标记会话
int128 new_nonce_hash1; // 标记
}
检查结果
# 应答就3种可能性
nonce_types = (DhGenOk, DhGenRetry, DhGenFail)
assert isinstance(dh_gen, nonce_types), 'Step 3.1 answer was %s' % dh_gen
name = dh_gen.__class__.__name__
if dh_gen.nonce != res_pq.nonce:
raise SecurityError('Step 3 invalid {} nonce from server'.format(name))
if dh_gen.server_nonce != res_pq.server_nonce:
raise SecurityError(
'Step 3 invalid {} server nonce from server'.format(name))
auth_key = AuthKey(rsa.get_byte_array(gab))
nonce_number = 1 + nonce_types.index(type(dh_gen))
new_nonce_hash = auth_key.calc_new_nonce_hash(new_nonce, nonce_number)
dh_hash = getattr(dh_gen, 'new_nonce_hash{}'.format(nonce_number))
if dh_hash != new_nonce_hash:
raise SecurityError('Step 3 invalid new nonce hash')
if not isinstance(dh_gen, DhGenOk):
raise AssertionError('Step 3.2 answer was %s' % dh_gen)
return auth_key, time_offset
未完待续……