diff --git a/docs/source/index.rst b/docs/source/index.rst index acd94baa..45fc483f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,6 +63,9 @@ Features `MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing a reliable connection. +- **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install + crypto library written in C. + - **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_. - **Documented**: Pyrogram API public methods are documented and resemble the well @@ -105,6 +108,7 @@ To get started, press Next. resources/ErrorHandling resources/SOCKS5Proxy resources/AutoAuthorization + resources/FastCrypto .. toctree:: :hidden: @@ -121,4 +125,6 @@ To get started, press Next. .. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto +.. _TgCrypto: https://docs.pyrogram.ml/resources/FastCrypto/ + .. _`Layer 75`: https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl \ No newline at end of file diff --git a/docs/source/resources/FastCrypto.rst b/docs/source/resources/FastCrypto.rst new file mode 100644 index 00000000..0cefd146 --- /dev/null +++ b/docs/source/resources/FastCrypto.rst @@ -0,0 +1,37 @@ +Fast Crypto +=========== + +Pyrogram's speed can be *dramatically* boosted up by installing TgCrypto_, a high-performance, easy-to-install crypto +library specifically written in C for Pyrogram [#f1]_. TgCrypto is a replacement for the painfully slow PyAES and +implements the crypto algorithms MTProto requires, namely AES-IGE and AES-CTR 256 bit. + +Installation +------------ + +.. code-block:: bash + + $ pip install --upgrade tgcrypto + + +.. note:: Being a C extension for Python, TgCrypto is an optional but *highly recommended* dependency; when TgCrypto + is not detected on your system, Pyrogram will automatically fall back to PyAES and will show you a warning. + +The reason about being an optional package is that TgCrypto requires some extra system tools in order to be compiled. +Usually the errors you receive when trying to install TgCrypto are enough to understand what you should do next. + +- **Windows**: Install `Visual C++ 2015 Build Tools `_. + +- **macOS**: A pop-up will automatically ask you to install the command line developer tools as soon as you issue the + installation command. + +- **Linux**: Depending on your distro, install a proper C compiler (``gcc``, ``clang``) and the Python header files + (``python3-dev``). + +- **Termux (Android)**: Install ``clang`` and ``python-dev`` packages. + +More help on the `Pyrogram group chat `_. + +.. _TgCrypto: https://github.com/pyrogram/tgcrypto + +.. [#f1] Although TgCrypto is intended for Pyrogram, it is shipped as a standalone package and can thus be used for + other projects too. \ No newline at end of file diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index bb470421..934197e8 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès . -from .ctr import CTR -from .ige import IGE +from .aes import AES from .kdf import KDF from .prime import Prime from .rsa import RSA diff --git a/pyrogram/crypto/aes.py b/pyrogram/crypto/aes.py new file mode 100644 index 00000000..8d971370 --- /dev/null +++ b/pyrogram/crypto/aes.py @@ -0,0 +1,88 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import logging + +log = logging.getLogger(__name__) + +try: + import tgcrypto +except ImportError: + logging.warning("Warning: TgCrypto is missing") + is_fast = False + import pyaes +else: + log.info("Using TgCrypto") + is_fast = True + + +# TODO: Ugly IFs +class AES: + @classmethod + def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: + if is_fast: + return tgcrypto.ige_encrypt(data, key, iv) + else: + return cls.ige(data, key, iv, True) + + @classmethod + def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: + if is_fast: + return tgcrypto.ige_decrypt(data, key, iv) + else: + return cls.ige(data, key, iv, False) + + @staticmethod + def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes: + replace = int.to_bytes(offset // 16, byteorder="big", length=4) + iv = iv[:-4] + replace + + if is_fast: + return tgcrypto.ctr_decrypt(data, key, iv) + else: + ctr = pyaes.AESModeOfOperationCTR(key) + ctr._counter._counter = list(iv) + return ctr.decrypt(data) + + @staticmethod + def xor(a: bytes, b: bytes) -> bytes: + return int.to_bytes( + int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), + len(a), + "big", + ) + + @classmethod + def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes: + cipher = pyaes.AES(key) + + iv_1 = iv[:16] + iv_2 = iv[16:] + + data = [data[i: i + 16] for i in range(0, len(data), 16)] + + if encrypt: + for i, chunk in enumerate(data): + iv_1 = data[i] = cls.xor(cipher.encrypt(cls.xor(chunk, iv_1)), iv_2) + iv_2 = chunk + else: + for i, chunk in enumerate(data): + iv_2 = data[i] = cls.xor(cipher.decrypt(cls.xor(chunk, iv_2)), iv_1) + iv_1 = chunk + + return b"".join(data) diff --git a/pyrogram/crypto/ctr.py b/pyrogram/crypto/ctr.py deleted file mode 100644 index 25cd4181..00000000 --- a/pyrogram/crypto/ctr.py +++ /dev/null @@ -1,35 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -try: - from pyaes import AESModeOfOperationCTR -except ImportError: - pass - - -class CTR: - def __init__(self, key: bytes, iv: bytes): - self.ctr = AESModeOfOperationCTR(key) - self.iv = iv - - def decrypt(self, data: bytes, offset: int) -> bytes: - replace = int.to_bytes(offset // 16, byteorder="big", length=4) - iv = self.iv[:-4] + replace - self.ctr._counter._counter = list(iv) - - return self.ctr.decrypt(data) diff --git a/pyrogram/crypto/ige.py b/pyrogram/crypto/ige.py deleted file mode 100644 index 03b4c399..00000000 --- a/pyrogram/crypto/ige.py +++ /dev/null @@ -1,64 +0,0 @@ -# Pyrogram - Telegram MTProto API Client Library for Python -# Copyright (C) 2017-2018 Dan Tès -# -# This file is part of Pyrogram. -# -# Pyrogram is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pyrogram is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Pyrogram. If not, see . - -# from pyaes import AES -import tgcrypto - -BLOCK_SIZE = 16 - - -# TODO: Performance optimization - -class IGE: - @classmethod - def encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: - return tgcrypto.ige_encrypt(data, key, iv) - # return cls.ige(data, key, iv, True) - - @classmethod - def decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: - return tgcrypto.ige_decrypt(data, key, iv) - # return cls.ige(data, key, iv, False) - - @staticmethod - def xor(a: bytes, b: bytes) -> bytes: - return int.to_bytes( - int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), - len(a), - "big", - ) - - # @classmethod - # def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes: - # cipher = AES(key) - # - # iv_1 = iv[:BLOCK_SIZE] - # iv_2 = iv[BLOCK_SIZE:] - # - # data = [data[i: i + BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)] - # - # if encrypt: - # for i, chunk in enumerate(data): - # iv_1 = data[i] = cls.xor(cipher.encrypt(cls.xor(chunk, iv_1)), iv_2) - # iv_2 = chunk - # else: - # for i, chunk in enumerate(data): - # iv_2 = data[i] = cls.xor(cipher.decrypt(cls.xor(chunk, iv_2)), iv_1) - # iv_1 = chunk - # - # return b"".join(data) diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index f3d7a3a3..741e9a44 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -25,7 +25,7 @@ from os import urandom from pyrogram.api import functions, types from pyrogram.api.core import Object, Long, Int from pyrogram.connection import Connection -from pyrogram.crypto import IGE, RSA, Prime +from pyrogram.crypto import AES, RSA, Prime from .internals import MsgId, DataCenter log = logging.getLogger(__name__) @@ -152,7 +152,7 @@ class Auth: server_nonce = int.from_bytes(server_nonce, "little", signed=True) - answer_with_hash = IGE.decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) + answer_with_hash = AES.ige_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) answer = answer_with_hash[20:] server_dh_inner_data = Object.read(BytesIO(answer)) @@ -181,7 +181,7 @@ class Auth: sha = sha1(data).digest() padding = urandom(- (len(data) + len(sha)) % 16) data_with_hash = sha + data + padding - encrypted_data = IGE.encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) + encrypted_data = AES.ige_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) log.debug("Send set_client_DH_params") set_client_dh_params_answer = self.send( @@ -236,7 +236,7 @@ class Auth: log.debug("Nonce fields check: OK") # Step 9 - server_salt = IGE.xor(new_nonce[:8], server_nonce[:8]) + server_salt = AES.xor(new_nonce[:8], server_nonce[:8]) log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little"))) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index c0410e3f..9010e2fe 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -33,7 +33,7 @@ from pyrogram.api.all import layer from pyrogram.api.core import Message, Object, MsgContainer, Long, FutureSalt, Int from pyrogram.api.errors import Error from pyrogram.connection import Connection -from pyrogram.crypto import IGE, KDF +from pyrogram.crypto import AES, KDF from .internals import MsgId, MsgFactory, DataCenter log = logging.getLogger(__name__) @@ -192,14 +192,14 @@ class Session: msg_key = msg_key_large[8:24] aes_key, aes_iv = KDF(self.auth_key, msg_key, True) - return self.auth_key_id + msg_key + IGE.encrypt(data + padding, aes_key, aes_iv) + return self.auth_key_id + msg_key + AES.ige_encrypt(data + padding, aes_key, aes_iv) def unpack(self, b: BytesIO) -> Message: assert b.read(8) == self.auth_key_id, b.getvalue() msg_key = b.read(16) aes_key, aes_iv = KDF(self.auth_key, msg_key, False) - data = BytesIO(IGE.decrypt(b.read(), aes_key, aes_iv)) + data = BytesIO(AES.ige_decrypt(b.read(), aes_key, aes_iv)) data.read(8) # https://core.telegram.org/mtproto/security_guidelines#checking-session-id diff --git a/setup.py b/setup.py index c2d65073..a58bc7bf 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,6 @@ setup( ], packages=find_packages(), zip_safe=False, - install_requires=["pyaes", "pysocks", "tgcrypto"], + install_requires=["pyaes", "pysocks"], include_package_data=True, )