From 45d0ba9e2f504f0c9721ab4f72607b203dfc4d8c Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Mon, 11 Feb 2019 09:16:46 +1000 Subject: [PATCH] Initial implementation of MTProxy support (#1107) --- telethon/client/telegrambaseclient.py | 10 ++++- telethon/network/__init__.py | 4 +- telethon/network/authenticator.py | 4 +- telethon/network/connection/__init__.py | 3 +- telethon/network/connection/connection.py | 9 +---- telethon/network/connection/tcpfull.py | 5 ++- telethon/network/connection/tcpmtproxy.py | 40 ++++++++++++++++++++ telethon/network/connection/tcpobfuscated.py | 26 +++++++++---- 8 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 telethon/network/connection/tcpmtproxy.py diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index 56a4ae0f..76227e35 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone from .. import version, __name__ as __base_name__ from ..crypto import rsa from ..extensions import markdown -from ..network import MTProtoSender, ConnectionTcpFull +from ..network import MTProtoSender, ConnectionTcpFull, ConnectionTcpMTProxy from ..sessions import Session, SQLiteSession, MemorySession from ..tl import TLObject, functions, types from ..tl.alltlobjects import LAYER @@ -245,6 +245,8 @@ class TelegramBaseClient(abc.ABC): assert isinstance(connection, type) self._connection = connection + init_proxy = None if connection is not ConnectionTcpMTProxy else \ + types.InputClientProxy(*ConnectionTcpMTProxy.address_info(proxy)) # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayerRequest. @@ -258,7 +260,8 @@ class TelegramBaseClient(abc.ABC): lang_code=lang_code, system_lang_code=system_lang_code, lang_pack='', # "langPacks are for official apps only" - query=x + query=x, + proxy=init_proxy ) ) @@ -345,6 +348,7 @@ class TelegramBaseClient(abc.ABC): await self._sender.connect(self._connection( self.session.server_address, self.session.port, + self.session.dc_id, loop=self._loop, loggers=self._log, proxy=self._proxy @@ -474,6 +478,7 @@ class TelegramBaseClient(abc.ABC): await sender.connect(self._connection( dc.ip_address, dc.port, + dc.id, loop=self._loop, loggers=self._log, proxy=self._proxy @@ -505,6 +510,7 @@ class TelegramBaseClient(abc.ABC): await sender.connect(self._connection( dc.ip_address, dc.port, + dc.id, loop=self._loop, loggers=self._log, proxy=self._proxy diff --git a/telethon/network/__init__.py b/telethon/network/__init__.py index e8070b21..d8f79d8b 100644 --- a/telethon/network/__init__.py +++ b/telethon/network/__init__.py @@ -6,6 +6,6 @@ from .mtprotoplainsender import MTProtoPlainSender from .authenticator import do_authentication from .mtprotosender import MTProtoSender from .connection import ( - ConnectionTcpFull, ConnectionTcpAbridged, ConnectionTcpObfuscated, - ConnectionTcpIntermediate, ConnectionHttp + ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, + ConnectionTcpObfuscated, ConnectionTcpMTProxy, ConnectionHttp ) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index bdf0f664..8b6f9374 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -10,7 +10,7 @@ from ..tl.types import ( ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk, ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail ) -from .. import helpers as utils +from .. import helpers from ..crypto import AES, AuthKey, Factorization, rsa from ..errors import SecurityError from ..extensions import BinaryReader @@ -94,7 +94,7 @@ async def do_authentication(sender): 'Step 2.2 answer was %s' % server_dh_params # Step 3 sending: Complete DH Exchange - key, iv = utils.generate_key_data_from_nonce( + key, iv = helpers.generate_key_data_from_nonce( res_pq.server_nonce, new_nonce ) if len(server_dh_params.encrypted_answer) % 16 != 0: diff --git a/telethon/network/connection/__init__.py b/telethon/network/connection/__init__.py index 262aaa3a..c890da1b 100644 --- a/telethon/network/connection/__init__.py +++ b/telethon/network/connection/__init__.py @@ -1,5 +1,6 @@ from .tcpfull import ConnectionTcpFull +from .tcpintermediate import ConnectionTcpIntermediate from .tcpabridged import ConnectionTcpAbridged from .tcpobfuscated import ConnectionTcpObfuscated -from .tcpintermediate import ConnectionTcpIntermediate +from .tcpmtproxy import ConnectionTcpMTProxy from .http import ConnectionHttp diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py index b354d3d8..029c19fe 100644 --- a/telethon/network/connection/connection.py +++ b/telethon/network/connection/connection.py @@ -18,9 +18,10 @@ class Connection(abc.ABC): ``ConnectionError``, which will raise when attempting to send if the client is disconnected (includes remote disconnections). """ - def __init__(self, ip, port, *, loop, loggers, proxy=None): + def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): self._ip = ip self._port = port + self._dc_id = dc_id # only for MTProxy, it's an abstraction leak self._loop = loop self._log = loggers[__name__] self._proxy = proxy @@ -94,12 +95,6 @@ class Connection(abc.ABC): if self._writer: self._writer.close() - def clone(self): - """ - Creates a clone of the connection. - """ - return self.__class__(self._ip, self._port, loop=self._loop) - def send(self, data): """ Sends a packet of data through this connection mode. diff --git a/telethon/network/connection/tcpfull.py b/telethon/network/connection/tcpfull.py index 7c7ff41a..9eb0d934 100644 --- a/telethon/network/connection/tcpfull.py +++ b/telethon/network/connection/tcpfull.py @@ -10,8 +10,9 @@ class ConnectionTcpFull(Connection): Default Telegram mode. Sends 12 additional bytes and needs to calculate the CRC value of the packet itself. """ - def __init__(self, ip, port, *, loop, loggers, proxy=None): - super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy) + def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): + super().__init__( + ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy) self._send_counter = 0 async def connect(self, timeout=None, ssl=None): diff --git a/telethon/network/connection/tcpmtproxy.py b/telethon/network/connection/tcpmtproxy.py new file mode 100644 index 00000000..ed437a28 --- /dev/null +++ b/telethon/network/connection/tcpmtproxy.py @@ -0,0 +1,40 @@ +import hashlib + +from .tcpobfuscated import ConnectionTcpObfuscated +from ...crypto import AESModeCTR + + +class ConnectionTcpMTProxy(ConnectionTcpObfuscated): + """ + Wrapper around the "obfuscated2" mode that modifies it a little and allows + user to connect to the Telegram proxy servers commonly known as MTProxy. + Implemented very ugly due to the leaky abstractions in Telethon networking + classes that should be refactored later (TODO). + """ + @staticmethod + def address_info(proxy_info): + if proxy_info is None: + raise ValueError("No proxy info specified for MTProxy connection") + return proxy_info[:2] + + def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): + proxy_host, proxy_port = self.address_info(proxy) + super().__init__( + proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers) + + # TODO: Implement the dd-secret secure mode (adds noise to fool DPI) + self._secret = bytes.fromhex(proxy[2]) + if len(self._secret) != 16: + raise ValueError( + "MTProxy secure mode is not implemented for now" + if len(self._secret) == 17 and self._secret[0] == 0xDD else + "MTProxy secret must be a hex-string representing 16 bytes" + ) + + def _compose_key(self, data): + return hashlib.sha256(data + self._secret).digest() + + def _compose_tail(self, data): + dc_id_bytes = self._dc_id.to_bytes(2, "little", signed=True) + tail_bytes = super()._compose_tail(data) + return tail_bytes[:4] + dc_id_bytes + tail_bytes[6:] diff --git a/telethon/network/connection/tcpobfuscated.py b/telethon/network/connection/tcpobfuscated.py index 7c83dfa6..6506c982 100644 --- a/telethon/network/connection/tcpobfuscated.py +++ b/telethon/network/connection/tcpobfuscated.py @@ -7,12 +7,14 @@ from ...crypto import AESModeCTR class ConnectionTcpObfuscated(ConnectionTcpAbridged): """ - Encodes the packet just like `ConnectionTcpAbridged`, but encrypts - every message with a randomly generated key using the - AES-CTR mode so the packets are harder to discern. + Mode that Telegram defines as "obfuscated2". Encodes the packet + just like `ConnectionTcpAbridged`, but encrypts every message with + a randomly generated key using the AES-CTR mode so the packets are + harder to discern. """ - def __init__(self, ip, port, *, loop, loggers, proxy=None): - super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy) + def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): + super().__init__( + ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy) self._aes_encrypt = None self._aes_decrypt = None @@ -40,14 +42,22 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged): random_reversed = random[55:7:-1] # Reversed (8, len=48) # Encryption has "continuous buffer" enabled - encrypt_key = bytes(random[8:40]) + encrypt_key = self._compose_key(bytes(random[8:40])) encrypt_iv = bytes(random[40:56]) - decrypt_key = bytes(random_reversed[:32]) + decrypt_key = self._compose_key(bytes(random_reversed[:32])) decrypt_iv = bytes(random_reversed[32:48]) self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv) self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv) - random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64] + random[56:64] = self._compose_tail(bytes(random)) self._writer.write(random) await self._writer.drain() + + # Next functions provide the variable parts of the connection handshake. + # This is necessary to modify obfuscated2 the way that MTProxy requires. + def _compose_key(self, data): + return data + + def _compose_tail(self, data): + return self._aes_encrypt.encrypt(data)[56:64]