Skip to content

Commit

Permalink
Implement MtProto 2.0 (closes #484, thanks @delivrance!)
Browse files Browse the repository at this point in the history
Huge shoutout to @delivrance's pyrogram, specially this commit:
pyrogram/pyrogram/commit/42f9a2d6994baaf9ecad590d1ff4d175a8c56454
  • Loading branch information
Lonami committed Jan 6, 2018
1 parent c039ba3 commit 3eafe18
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 41 deletions.
5 changes: 4 additions & 1 deletion telethon/extensions/binary_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ def read_large_int(self, bits, signed=True):
return int.from_bytes(
self.read(bits // 8), byteorder='little', signed=signed)

def read(self, length):
def read(self, length=None):
"""Read the given amount of bytes."""
if length is None:
return self.reader.read()

result = self.reader.read(length)
if len(result) != length:
raise BufferError(
Expand Down
67 changes: 64 additions & 3 deletions telethon/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""Various helpers not related to the Telegram API itself"""
from hashlib import sha1, sha256
import os
import struct
from hashlib import sha1, sha256

from telethon.crypto import AES
from telethon.extensions import BinaryReader


# region Multiple utilities

Expand All @@ -21,9 +26,48 @@ def ensure_parent_dir_exists(file_path):
# 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):
"""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

Expand All @@ -40,6 +84,23 @@ def calc_key(shared_key, msg_key, client):
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):
"""Calculates the message key from the given data"""
return sha1(data).digest()[4:20]
Expand Down
44 changes: 7 additions & 37 deletions telethon/network/mtproto_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,7 @@ def _send_message(self, message):
:param message: the TLMessage to be sent.
"""
plain_text = \
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)
self.connection.send(utils.pack_message(self.session, message))

def _decode_msg(self, body):
"""
Expand All @@ -175,34 +165,14 @@ def _decode_msg(self, body):
:param body: the body to be decoded.
:return: a tuple of (decoded message, remote message id, remote seq).
"""
message = None
remote_msg_id = None
remote_sequence = None
if len(body) < 8:
if body == b'l\xfe\xff\xff':
raise BrokenAuthKeyError()
else:
raise BufferError("Can't decode packet ({})".format(body))

with BinaryReader(body) as reader:
if len(body) < 8:
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
return utils.unpack_message(self.session, reader)

def _process_msg(self, msg_id, sequence, reader, state):
"""
Expand Down

0 comments on commit 3eafe18

Please sign in to comment.