mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-26 11:23:46 +03:00
Implement MtProto 2.0 (closes #484, thanks @delivrance!)
Huge shoutout to @delivrance's pyrogram, specially this commit: pyrogram/pyrogram/commit/42f9a2d6994baaf9ecad590d1ff4d175a8c56454
This commit is contained in:
parent
c039ba3e16
commit
3eafe18d0b
|
@ -56,8 +56,11 @@ class BinaryReader:
|
||||||
return int.from_bytes(
|
return int.from_bytes(
|
||||||
self.read(bits // 8), byteorder='little', signed=signed)
|
self.read(bits // 8), byteorder='little', signed=signed)
|
||||||
|
|
||||||
def read(self, length):
|
def read(self, length=None):
|
||||||
"""Read the given amount of bytes."""
|
"""Read the given amount of bytes."""
|
||||||
|
if length is None:
|
||||||
|
return self.reader.read()
|
||||||
|
|
||||||
result = self.reader.read(length)
|
result = self.reader.read(length)
|
||||||
if len(result) != length:
|
if len(result) != length:
|
||||||
raise BufferError(
|
raise BufferError(
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
"""Various helpers not related to the Telegram API itself"""
|
"""Various helpers not related to the Telegram API itself"""
|
||||||
from hashlib import sha1, sha256
|
|
||||||
import os
|
import os
|
||||||
|
import struct
|
||||||
|
from hashlib import sha1, sha256
|
||||||
|
|
||||||
|
from telethon.crypto import AES
|
||||||
|
from telethon.extensions import BinaryReader
|
||||||
|
|
||||||
|
|
||||||
# region Multiple utilities
|
# region Multiple utilities
|
||||||
|
|
||||||
|
@ -21,9 +26,48 @@ def ensure_parent_dir_exists(file_path):
|
||||||
# region Cryptographic related utils
|
# region Cryptographic related utils
|
||||||
|
|
||||||
|
|
||||||
|
def pack_message(session, message):
|
||||||
|
"""Packs a message following MtProto 2.0 guidelines"""
|
||||||
|
# See https://core.telegram.org/mtproto/description
|
||||||
|
data = struct.pack('<qq', session.salt, session.id) + bytes(message)
|
||||||
|
padding = os.urandom(-(len(data) + 12) % 16 + 12)
|
||||||
|
|
||||||
|
# Being substr(what, offset, length); x = 0 for client
|
||||||
|
# "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
|
||||||
|
msg_key_large = sha256(
|
||||||
|
session.auth_key.key[88:88 + 32] + data + padding).digest()
|
||||||
|
|
||||||
|
# "msg_key = substr (msg_key_large, 8, 16)"
|
||||||
|
msg_key = msg_key_large[8:24]
|
||||||
|
aes_key, aes_iv = calc_key_2(session.auth_key.key, msg_key, True)
|
||||||
|
|
||||||
|
key_id = struct.pack('<Q', session.auth_key.key_id)
|
||||||
|
return key_id + msg_key + AES.encrypt_ige(data + padding, aes_key, aes_iv)
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_message(session, reader):
|
||||||
|
"""Unpacks a message following MtProto 2.0 guidelines"""
|
||||||
|
# See https://core.telegram.org/mtproto/description
|
||||||
|
reader.read_long(signed=False) # remote_auth_key_id
|
||||||
|
|
||||||
|
msg_key = reader.read(16)
|
||||||
|
aes_key, aes_iv = calc_key_2(session.auth_key.key, msg_key, False)
|
||||||
|
data = BinaryReader(AES.decrypt_ige(reader.read(), aes_key, aes_iv))
|
||||||
|
|
||||||
|
data.read_long() # remote_salt
|
||||||
|
data.read_long() # remote_session_id
|
||||||
|
remote_msg_id = data.read_long()
|
||||||
|
remote_sequence = data.read_int()
|
||||||
|
msg_len = data.read_int()
|
||||||
|
message = data.read(msg_len)
|
||||||
|
|
||||||
|
return message, remote_msg_id, remote_sequence
|
||||||
|
|
||||||
|
|
||||||
def calc_key(shared_key, msg_key, client):
|
def calc_key(shared_key, msg_key, client):
|
||||||
"""Calculate the key based on Telegram guidelines,
|
"""
|
||||||
specifying whether it's the client or not
|
Calculate the key based on Telegram guidelines,
|
||||||
|
specifying whether it's the client or not.
|
||||||
"""
|
"""
|
||||||
x = 0 if client else 8
|
x = 0 if client else 8
|
||||||
|
|
||||||
|
@ -40,6 +84,23 @@ def calc_key(shared_key, msg_key, client):
|
||||||
return key, iv
|
return key, iv
|
||||||
|
|
||||||
|
|
||||||
|
def calc_key_2(auth_key, msg_key, client):
|
||||||
|
"""
|
||||||
|
Calculate the key based on Telegram guidelines
|
||||||
|
for MtProto 2, specifying whether it's the client or not.
|
||||||
|
"""
|
||||||
|
# https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector
|
||||||
|
x = 0 if client else 8
|
||||||
|
|
||||||
|
sha256a = sha256(msg_key + auth_key[x: x + 36]).digest()
|
||||||
|
sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest()
|
||||||
|
|
||||||
|
aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32]
|
||||||
|
aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32]
|
||||||
|
|
||||||
|
return aes_key, aes_iv
|
||||||
|
|
||||||
|
|
||||||
def calc_msg_key(data):
|
def calc_msg_key(data):
|
||||||
"""Calculates the message key from the given data"""
|
"""Calculates the message key from the given data"""
|
||||||
return sha1(data).digest()[4:20]
|
return sha1(data).digest()[4:20]
|
||||||
|
|
|
@ -156,17 +156,7 @@ class MtProtoSender:
|
||||||
|
|
||||||
:param message: the TLMessage to be sent.
|
:param message: the TLMessage to be sent.
|
||||||
"""
|
"""
|
||||||
plain_text = \
|
self.connection.send(utils.pack_message(self.session, message))
|
||||||
struct.pack('<qq', self.session.salt, self.session.id) \
|
|
||||||
+ bytes(message)
|
|
||||||
|
|
||||||
msg_key = utils.calc_msg_key(plain_text)
|
|
||||||
key_id = struct.pack('<Q', self.session.auth_key.key_id)
|
|
||||||
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, True)
|
|
||||||
cipher_text = AES.encrypt_ige(plain_text, key, iv)
|
|
||||||
|
|
||||||
result = key_id + msg_key + cipher_text
|
|
||||||
self.connection.send(result)
|
|
||||||
|
|
||||||
def _decode_msg(self, body):
|
def _decode_msg(self, body):
|
||||||
"""
|
"""
|
||||||
|
@ -175,34 +165,14 @@ class MtProtoSender:
|
||||||
:param body: the body to be decoded.
|
:param body: the body to be decoded.
|
||||||
:return: a tuple of (decoded message, remote message id, remote seq).
|
:return: a tuple of (decoded message, remote message id, remote seq).
|
||||||
"""
|
"""
|
||||||
message = None
|
if len(body) < 8:
|
||||||
remote_msg_id = None
|
if body == b'l\xfe\xff\xff':
|
||||||
remote_sequence = None
|
raise BrokenAuthKeyError()
|
||||||
|
else:
|
||||||
|
raise BufferError("Can't decode packet ({})".format(body))
|
||||||
|
|
||||||
with BinaryReader(body) as reader:
|
with BinaryReader(body) as reader:
|
||||||
if len(body) < 8:
|
return utils.unpack_message(self.session, reader)
|
||||||
if body == b'l\xfe\xff\xff':
|
|
||||||
raise BrokenAuthKeyError()
|
|
||||||
else:
|
|
||||||
raise BufferError("Can't decode packet ({})".format(body))
|
|
||||||
|
|
||||||
# TODO Check for both auth key ID and msg_key correctness
|
|
||||||
reader.read_long() # remote_auth_key_id
|
|
||||||
msg_key = reader.read(16)
|
|
||||||
|
|
||||||
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False)
|
|
||||||
plain_text = AES.decrypt_ige(
|
|
||||||
reader.read(len(body) - reader.tell_position()), key, iv)
|
|
||||||
|
|
||||||
with BinaryReader(plain_text) as plain_text_reader:
|
|
||||||
plain_text_reader.read_long() # remote_salt
|
|
||||||
plain_text_reader.read_long() # remote_session_id
|
|
||||||
remote_msg_id = plain_text_reader.read_long()
|
|
||||||
remote_sequence = plain_text_reader.read_int()
|
|
||||||
msg_len = plain_text_reader.read_int()
|
|
||||||
message = plain_text_reader.read(msg_len)
|
|
||||||
|
|
||||||
return message, remote_msg_id, remote_sequence
|
|
||||||
|
|
||||||
def _process_msg(self, msg_id, sequence, reader, state):
|
def _process_msg(self, msg_id, sequence, reader, state):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user