2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-09-05 00:35:10 +00:00

Merge branch 'master' into new-api

This commit is contained in:
Dan
2018-03-09 13:28:16 +01:00
34 changed files with 9548 additions and 409 deletions

View File

@@ -23,10 +23,12 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance
"e" if sys.getfilesystemencoding() == "ascii" else "\xe8"
)
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.5.0"
__version__ = "0.6.2"
from .api.errors import Error
from .client import ChatAction
from .client import Client
from .client import ParseMode
from .client.input_media import InputMedia
from .client.input_phone_contact import InputPhoneContact
from .client import Emoji

View File

@@ -19,3 +19,4 @@
from .chat_action import ChatAction
from .client import Client
from .parse_mode import ParseMode
from .emoji import Emoji

File diff suppressed because it is too large Load Diff

7829
pyrogram/client/emoji.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,10 +20,11 @@
class InputMedia:
class Photo:
"""This object represents a photo to be sent inside an album.
It is intended to be used with :obj:`pyrogram.Client.send_media_group`.
Args:
media (:obj:`str`):
File to send.
Photo file to send.
Pass a file path as string to send a photo that exists on your local machine.
caption (:obj:`str`):
@@ -45,19 +46,32 @@ class InputMedia:
class Video:
"""This object represents a video to be sent inside an album.
It is intended to be used with :obj:`pyrogram.Client.send_media_group`.
Args:
media (:obj:`str`):
File to send.
Video file to send.
Pass a file path as string to send a video that exists on your local machine.
caption (:obj:`str`):
caption (:obj:`str`, optional):
Caption of the video to be sent, 0-200 characters
parse_mode (:obj:`str`):
parse_mode (:obj:`str`, optional):
Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
width (:obj:`int`, optional):
Video width.
height (:obj:`int`, optional):
Video height
duration (:obj:`int`, optional):
Video duration.
supports_streaming (:obj:`bool`, optional):
Pass True, if the uploaded video is suitable for streaming.
"""
def __init__(self,
@@ -66,10 +80,12 @@ class InputMedia:
parse_mode: str = "",
width: int = 0,
height: int = 0,
duration: int = 0):
duration: int = 0,
supports_streaming: bool = None):
self.media = media
self.caption = caption
self.parse_mode = parse_mode
self.width = width
self.height = height
self.duration = duration
self.supports_streaming = supports_streaming

View File

@@ -16,20 +16,29 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
try:
from pyaes import AESModeOfOperationCTR
except ImportError:
pass
from pyrogram.api.types import InputPhoneContact as RawInputPhoneContact
from pyrogram.session.internals import MsgId
class CTR:
def __init__(self, key: bytes, iv: bytes):
self.ctr = AESModeOfOperationCTR(key)
self.iv = iv
class InputPhoneContact:
"""This object represents a Phone Contact to be added in your Telegram address book.
It is intended to be used with :obj:`pyrogram.Client.add_contacts`
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)
Args:
phone (:obj:`str`):
Contact's phone number
return self.ctr.decrypt(data)
first_name (:obj:`str`):
Contact's first name
last_name (:obj:`str`, optional):
Contact's last name
"""
def __new__(cls, phone: str, first_name: str, last_name: str = ""):
return RawInputPhoneContact(
client_id=MsgId(),
phone="+" + phone.strip("+"),
first_name=first_name,
last_name=last_name
)

View File

@@ -31,7 +31,7 @@ from . import utils
class HTML:
HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])(.*)\2)?>(.*)</\1>")
HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])([^<]+)\2)?>([^>]+)</\1>")
MENTION_RE = re.compile(r"tg://user\?id=(\d+)")
def __init__(self, peers_by_id):
@@ -44,7 +44,7 @@ class HTML:
for match in self.HTML_RE.finditer(text):
start = match.start() - offset
style, url, body = match.groups()
style, url, body = match.group(1, 3, 4)
if url:
mention = self.MENTION_RE.match(url)

View File

@@ -24,95 +24,84 @@ from pyrogram.api.types import (
MessageEntityCode as Code,
MessageEntityTextUrl as Url,
MessageEntityPre as Pre,
MessageEntityMentionName as MentionInvalid,
InputMessageEntityMentionName as Mention
)
from . import utils
class Markdown:
INLINE_DELIMITERS = {
"**": Bold,
"__": Italic,
"`": Code
}
BOLD_DELIMITER = "**"
ITALIC_DELIMITER = "__"
CODE_DELIMITER = "`"
PRE_DELIMITER = "```"
# ``` python
# for i in range(10):
# print(i)
# ```
PRE_RE = r"(?P<pre>```(?P<lang>.*)\n(?P<code>(.|\n)*)\n```)"
# [url](github.com)
URL_RE = r"(?P<url>(\[(?P<url_text>.+?)\]\((?P<url_path>.+?)\)))"
# [name](tg://user?id=123456789)
MENTION_RE = r"(?P<mention>(\[(?P<mention_text>.+?)\]\(tg:\/\/user\?id=(?P<user_id>\d+?)\)))"
# **bold**
# __italic__
# `code`
INLINE_RE = r"(?P<inline>(?P<start_delimiter>{d})(?P<body>.+?)(?P<end_delimiter>{d}))".format(
MARKDOWN_RE = re.compile(r"```([\w ]*)\n([\w\W]*)(?:\n|)```|\[([^[(]+)\]\(([^])]+)\)|({d})(.+?)\5".format(
d="|".join(
["".join(i) for i in [
["\{}".format(j) for j in i]
for i in sorted( # Sort delimiters by length
INLINE_DELIMITERS.keys(),
key=lambda k: len(k), # Or: key=len
reverse=True
)
for i in [
PRE_DELIMITER,
CODE_DELIMITER,
ITALIC_DELIMITER,
BOLD_DELIMITER
]
]]
)
)
))
MENTION_RE = re.compile(r"tg://user\?id=(\d+)")
MARKDOWN_RE = re.compile("|".join([PRE_RE, MENTION_RE, URL_RE, INLINE_RE]))
def __init__(self, peers_by_id):
def __init__(self, peers_by_id: dict):
self.peers_by_id = peers_by_id
def parse(self, text):
def parse(self, message: str):
entities = []
text = utils.add_surrogates(text)
message = utils.add_surrogates(message).strip()
offset = 0
for match in self.MARKDOWN_RE.finditer(text):
for match in self.MARKDOWN_RE.finditer(message):
start = match.start() - offset
lang, pre, text, url, style, body = match.groups()
if match.group("pre"):
pattern = match.group("pre")
lang = match.group("lang")
replace = match.group("code")
entity = Pre(start, len(replace), lang.strip())
offset += len(lang) + 8
elif match.group("url"):
pattern = match.group("url")
replace = match.group("url_text")
path = match.group("url_path")
entity = Url(start, len(replace), path)
offset += len(path) + 4
elif match.group("mention"):
pattern = match.group("mention")
replace = match.group("mention_text")
user_id = match.group("user_id")
entity = Mention(start, len(replace), self.peers_by_id[int(user_id)])
offset += len(user_id) + 17
elif match.group("inline"):
pattern = match.group("inline")
replace = match.group("body")
start_delimiter = match.group("start_delimiter")
end_delimiter = match.group("end_delimiter")
if pre:
body = pre = pre.strip()
entity = Pre(start, len(pre), lang.strip() or "")
offset += len(lang) + len(self.PRE_DELIMITER) * 2
elif url:
mention = self.MENTION_RE.match(url)
if start_delimiter != end_delimiter:
if mention:
user_id = int(mention.group(1))
input_user = self.peers_by_id.get(user_id, None)
entity = (
Mention(start, len(text), input_user)
if input_user
else MentionInvalid(start, len(text), user_id)
)
else:
entity = Url(start, len(text), url)
body = text
offset += len(url) + 4
else:
if style == self.BOLD_DELIMITER:
entity = Bold(start, len(body))
elif style == self.ITALIC_DELIMITER:
entity = Italic(start, len(body))
elif style == self.CODE_DELIMITER:
entity = Code(start, len(body))
elif style == self.PRE_DELIMITER:
entity = Pre(start, len(body), "")
else:
continue
entity = self.INLINE_DELIMITERS[start_delimiter](start, len(replace))
offset += len(start_delimiter) * 2
else:
continue
offset += len(style) * 2
entities.append(entity)
text = text.replace(pattern, replace)
message = message.replace(match.group(), body)
return dict(
message=utils.remove_surrogates(text),
message=utils.remove_surrogates(message),
entities=entities
)

View File

@@ -30,6 +30,7 @@ Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "passwor
class TCP(socks.socksocket):
def __init__(self, proxy: Proxy):
super().__init__()
self.settimeout(10)
self.proxy_enabled = False
if proxy and proxy.enabled:
@@ -43,6 +44,11 @@ class TCP(socks.socksocket):
password=proxy.password
)
log.info("Using proxy {}:{}".format(
proxy.hostname,
proxy.port
))
def close(self):
try:
self.shutdown(socket.SHUT_RDWR)

View File

@@ -16,8 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .ctr import CTR
from .ige import IGE
from .aes import AES
from .kdf import KDF
from .prime import Prime
from .rsa import RSA

View File

@@ -16,21 +16,52 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from pyaes import AES
import logging
BLOCK_SIZE = 16
log = logging.getLogger(__name__)
try:
import tgcrypto
except ImportError:
log.warning(
"TgCrypto is missing! "
"Pyrogram will work the same, but at a much slower speed. "
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
)
is_fast = False
import pyaes
else:
log.info("Using TgCrypto")
is_fast = True
# TODO: Performance optimization
class IGE:
# TODO: Ugly IFs
class AES:
@classmethod
def encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
return cls.ige(data, key, iv, True)
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 decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
return cls.ige(data, key, iv, False)
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:
@@ -42,12 +73,12 @@ class IGE:
@classmethod
def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes:
cipher = AES(key)
cipher = pyaes.AES(key)
iv_1 = iv[:BLOCK_SIZE]
iv_2 = iv[BLOCK_SIZE:]
iv_1 = iv[:16]
iv_2 = iv[16:]
data = [data[i: i + BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)]
data = [data[i: i + 16] for i in range(0, len(data), 16)]
if encrypt:
for i, chunk in enumerate(data):

View File

@@ -23,10 +23,16 @@ PublicKey = namedtuple("PublicKey", ["m", "e"])
class RSA:
# To get modulus and exponent:
#
# [RSA PUBLIC KEY]:
# grep -v -- - public.key | tr -d \\n | base64 -d | openssl asn1parse -inform DER -i
#
# [PUBLIC KEY]:
# openssl rsa -pubin -in key -text -noout
server_public_keys = {
0xc3b42b026ce86b21 - (1 << 64): PublicKey( # Telegram servers
# -4344800451088585951
0xc3b42b026ce86b21 - (1 << 64): PublicKey( # Telegram servers #1
# -----BEGIN RSA PUBLIC KEY-----
# MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
# lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
@@ -48,6 +54,108 @@ class RSA:
), # Modulus
int("010001", 16) # Exponent
),
# 847625836280919973
0x10bc35f3509f7b7a5 - (1 << 64): PublicKey( # Telegram servers #2
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB
# VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx
# hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd
# l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg
# gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O
# 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5
# JwIDAQAB
# -----END PUBLIC KEY-----
int(
"AEEC36C8FFC109CB099624685B97815415657BD76D8C9C3E398103D7AD16C9BB"
"A6F525ED0412D7AE2C2DE2B44E77D72CBF4B7438709A4E646A05C43427C7F184"
"DEBF72947519680E651500890C6832796DD11F772C25FF8F576755AFE055B0A3"
"752C696EB7D8DA0D8BE1FAF38C9BDD97CE0A77D3916230C4032167100EDD0F9E"
"7A3A9B602D04367B689536AF0D64B613CCBA7962939D3B57682BEB6DAE5B6081"
"30B2E52ACA78BA023CF6CE806B1DC49C72CF928A7199D22E3D7AC84E47BC9427"
"D0236945D10DBD15177BAB413FBF0EDFDA09F014C7A7DA088DDE9759702CA760"
"AF2B8E4E97CC055C617BD74C3D97008635B98DC4D621B4891DA9FB0473047927",
16
), # Modulus
int("010001", 16) # Exponent
),
# 1562291298945373506
0x115ae5fa8b5529542 - (1 << 64): PublicKey( # Telegram servers #3
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfLHfYH2r9R70w8prHbl
# Wt/nDkh+XkgpflqQVcnAfSuTtO05lNPspQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOO
# KPi0OfJXoRVylFzAQG/j83u5K3kRLbae7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ
# 3TDS2pQOCtovG4eDl9wacrXOJTG2990VjgnIKNA0UMoP+KF03qzryqIt3oTvZq03
# DyWdGK+AZjgBLaDKSnC6qD2cFY81UryRWOab8zKkWAnhw2kFpcqhI0jdV5QaSCEx
# vnsjVaX0Y1N0870931/5Jb9ICe4nweZ9kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV
# /wIDAQAB
# -----END PUBLIC KEY-----
int(
"BDF2C77D81F6AFD47BD30F29AC76E55ADFE70E487E5E48297E5A9055C9C07D2B"
"93B4ED3994D3ECA5098BF18D978D54F8B7C713EB10247607E69AF9EF44F38E28"
"F8B439F257A11572945CC0406FE3F37BB92B79112DB69EEDF2DC71584A661638"
"EA5BECB9E23585074B80D57D9F5710DD30D2DA940E0ADA2F1B878397DC1A72B5"
"CE2531B6F7DD158E09C828D03450CA0FF8A174DEACEBCAA22DDE84EF66AD370F"
"259D18AF806638012DA0CA4A70BAA83D9C158F3552BC9158E69BF332A45809E1"
"C36905A5CAA12348DD57941A482131BE7B2355A5F4635374F3BD3DDF5FF925BF"
"4809EE27C1E67D9120C5FE08A9DE458B1B4A3C5D0A428437F2BECA81F4E2D5FF",
16
), # Modulus
int("010001", 16) # Exponent
),
# -5859577972006586033
0xaeae98e13cd7f94f - (1 << 64): PublicKey( # Telegram servers #4
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ditzm+mPND6xkhzwFI
# z6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGrzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl
# 4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+th6knSU0yLtNKuQVP6voMrnt9MV1X92L
# GZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvSUwwc+yi1/gGaybwlzZwqXYoPOhwMebzK
# Uk0xW14htcJrRrq+PXXQbRzTMynseCoPIoke0dtCodbA3qQxQovE16q9zz4Otv2k
# 4j63cz53J+mhkVWAeWxVGI0lltJmWtEYK6er8VqqWot3nqmWMXogrgRLggv/Nbbo
# oQIDAQAB
# -----END PUBLIC KEY-----
int(
"B3F762B739BE98F343EB1921CF0148CFA27FF7AF02B6471213FED9DAA0098976"
"E667750324F1ABCEA4C31E43B7D11F1579133F2B3D9FE27474E462058884E5E1"
"B123BE9CBBC6A443B2925C08520E7325E6F1A6D50E117EB61EA49D2534C8BB4D"
"2AE4153FABE832B9EDF4C5755FDD8B19940B81D1D96CF433D19E6A22968A85DC"
"80F0312F596BD2530C1CFB28B5FE019AC9BC25CD9C2A5D8A0F3A1C0C79BCCA52"
"4D315B5E21B5C26B46BABE3D75D06D1CD33329EC782A0F22891ED1DB42A1D6C0"
"DEA431428BC4D7AABDCF3E0EB6FDA4E23EB7733E7727E9A1915580796C55188D"
"2596D2665AD1182BA7ABF15AAA5A8B779EA996317A20AE044B820BFF35B6E8A1",
16
), # Modulus
int("010001", 16) # Exponent
),
# 6491968696586960280
0x15a181b2235057d98 - (1 << 64): PublicKey( # Telegram servers #5
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q0
# 5shjg8/4p6047bn6/m8yPy1RBsvIyvuDuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xb
# nfxL5BXHplJhMtADXKM9bWB11PU1Eioc3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA
# 9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvifRLJbY08/Gp66KpQvy7g8w7VB8wlgePe
# xW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqePji9NP3tJUFQjcECqcm0yV7/2d0t/pbC
# m+ZH1sadZspQCEPPrtbkQBlvHb4OLiIWPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6M
# AQIDAQAB
# -----END PUBLIC KEY-----
int(
"BE6A71558EE577FF03023CFA17AAB4E6C86383CFF8A7AD38EDB9FAFE6F323F2D"
"5106CBC8CAFB83B869CFFD1CCF121CD743D509E589E68765C96601E813DC5B9D"
"FC4BE415C7A6526132D0035CA33D6D6075D4F535122A1CDFE017041F1088D141"
"9F65C8E5490EE613E16DBF662698C0F54870F0475FA893FC41EB55B08FF1AC21"
"1BC045DED31BE27D12C96D8D3CFC6A7AE8AA50BF2EE0F30ED507CC2581E3DEC5"
"6DE94F5DC0A7ABEE0BE990B893F2887BD2C6310A1E0A9E3E38BD34FDED254150"
"8DC102A9C9B4C95EFFD9DD2DFE96C29BE647D6C69D66CA500843CFAED6E44019"
"6F1DBE0E2E22163C61CA48C79116FA77216726749A976A1C4B0944B5121E8C01",
16
), # Modulus
int("010001", 16) # Exponent
),
# 6427105915145367799
0x15931aac70e0d30f7 - (1 << 64): PublicKey( # CDN DC-121
# -----BEGIN RSA PUBLIC KEY-----
# MIIBCgKCAQEA+Lf3PvgE1yxbJUCMaEAkV0QySTVpnaDjiednB5RbtNWjCeqSVakY
@@ -70,6 +178,8 @@ class RSA:
), # Modulus
int("010001", 16) # Exponent
),
# 2685959930972952888
0x1254672538e935938 - (1 << 64): PublicKey( # CDN DC-140
# -----BEGIN RSA PUBLIC KEY-----
# MIIBCgKCAQEAzuHVC7sE50Kho/yDVZtWnlmA5Bf/aM8KZY3WzS16w6w1sBqipj8o

View File

@@ -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__)
@@ -51,12 +51,12 @@ class Auth:
self.test_mode = test_mode
self.connection = Connection(DataCenter(dc_id, test_mode), proxy)
self.msg_id = MsgId()
def pack(self, data: Object) -> bytes:
@staticmethod
def pack(data: Object) -> bytes:
return (
bytes(8)
+ Long(self.msg_id())
+ Long(MsgId())
+ Int(len(data.write()))
+ data.write()
)
@@ -91,8 +91,19 @@ class Auth:
# Step 1; Step 2
nonce = int.from_bytes(urandom(16), "little", signed=True)
log.debug("Send req_pq: {}".format(nonce))
res_pq = self.send(functions.ReqPq(nonce))
res_pq = self.send(functions.ReqPqMulti(nonce))
log.debug("Got ResPq: {}".format(res_pq.server_nonce))
log.debug("Server public key fingerprints: {}".format(res_pq.server_public_key_fingerprints))
for i in res_pq.server_public_key_fingerprints:
if i in RSA.server_public_keys:
log.debug("Using fingerprint: {}".format(i))
public_key_fingerprint = i
break
else:
log.debug("Fingerprint unknown: {}".format(i))
else:
raise Exception("Public key not found")
# Step 3
pq = int.from_bytes(res_pq.pq, "big")
@@ -118,7 +129,7 @@ class Auth:
sha = sha1(data).digest()
padding = urandom(- (len(data) + len(sha)) % 255)
data_with_hash = sha + data + padding
encrypted_data = RSA.encrypt(data_with_hash, res_pq.server_public_key_fingerprints[0])
encrypted_data = RSA.encrypt(data_with_hash, public_key_fingerprint)
log.debug("Done encrypt data with RSA")
@@ -130,7 +141,7 @@ class Auth:
server_nonce,
int.to_bytes(p, 4, "big"),
int.to_bytes(q, 4, "big"),
res_pq.server_public_key_fingerprints[0],
public_key_fingerprint,
encrypted_data
)
)
@@ -152,7 +163,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 +192,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 +247,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")))

View File

@@ -26,14 +26,13 @@ not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer]
class MsgFactory:
def __init__(self, msg_id: MsgId):
self.msg_id = msg_id
def __init__(self):
self.seq_no = SeqNo()
def __call__(self, body: Object) -> Message:
return Message(
body,
self.msg_id(),
MsgId(),
self.seq_no(type(body) not in not_content_related),
len(body)
)

View File

@@ -16,19 +16,20 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from threading import Lock
from time import time
class MsgId:
def __init__(self, delta_time: float = 0.0):
self.delta_time = delta_time
self.last_time = 0
self.offset = 0
last_time = 0
offset = 0
lock = Lock()
def __call__(self) -> int:
now = time()
self.offset = self.offset + 4 if now == self.last_time else 0
msg_id = int((now + self.delta_time) * 2 ** 32) + self.offset
self.last_time = now
def __new__(cls) -> int:
with cls.lock:
now = time()
cls.offset = cls.offset + 4 if now == cls.last_time else 0
msg_id = int(now * 2 ** 32) + cls.offset
cls.last_time = now
return msg_id
return msg_id

View File

@@ -16,15 +16,19 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from threading import Lock
class SeqNo:
def __init__(self):
self.content_related_messages_sent = 0
self.lock = Lock()
def __call__(self, is_content_related: bool) -> int:
seq_no = (self.content_related_messages_sent * 2) + (1 if is_content_related else 0)
with self.lock:
seq_no = (self.content_related_messages_sent * 2) + (1 if is_content_related else 0)
if is_content_related:
self.content_related_messages_sent += 1
if is_content_related:
self.content_related_messages_sent += 1
return seq_no
return seq_no

View File

@@ -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__)
@@ -60,7 +60,7 @@ class Session:
)
INITIAL_SALT = 0x616e67656c696361
NET_WORKERS = 2
NET_WORKERS = 1
WAIT_TIMEOUT = 10
MAX_RETRIES = 5
ACKS_THRESHOLD = 8
@@ -68,6 +68,20 @@ class Session:
notice_displayed = False
BAD_MSG_DESCRIPTION = {
16: "[16] msg_id too low, the client time has to be synchronized",
17: "[17] msg_id too high, the client time has to be synchronized",
18: "[18] incorrect two lower order msg_id bits, the server expects client message msg_id to be divisible by 4",
19: "[19] container msg_id is the same as msg_id of a previously received message",
20: "[20] message too old, it cannot be verified by the server",
32: "[32] msg_seqno too low",
33: "[33] msg_seqno too high",
34: "[34] an even msg_seqno expected, but odd received",
35: "[35] odd msg_seqno expected, but even received",
48: "[48] incorrect server salt",
64: "[64] invalid container"
}
def __init__(self,
dc_id: int,
test_mode: bool,
@@ -89,9 +103,8 @@ class Session:
self.auth_key = auth_key
self.auth_key_id = sha1(auth_key).digest()[-8:]
self.msg_id = MsgId()
self.session_id = Long(self.msg_id())
self.msg_factory = MsgFactory(self.msg_id)
self.session_id = Long(MsgId())
self.msg_factory = MsgFactory()
self.current_salt = None
@@ -146,7 +159,7 @@ class Session:
self.ping_thread.start()
log.info("Connection inited: Layer {}".format(layer))
except (OSError, TimeoutError):
except (OSError, TimeoutError, Error):
self.stop()
else:
break
@@ -192,14 +205,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
@@ -270,7 +283,7 @@ class Session:
msg_id = msg.body.msg_id
else:
if self.client is not None:
self.client.update_queue.put(msg.body)
self.client.updates_queue.put(msg.body)
if msg_id in self.results:
self.results[msg_id].value = getattr(msg.body, "result", msg.body)
@@ -296,7 +309,7 @@ class Session:
break
try:
self._send(functions.Ping(0), False)
self._send(functions.PingDelayDisconnect(0, self.PING_INTERVAL + 15), False)
except (OSError, TimeoutError):
pass
@@ -338,7 +351,10 @@ class Session:
while True:
packet = self.connection.recv()
if packet is None or (len(packet) == 4 and Int.read(BytesIO(packet)) == -404):
if packet is None or len(packet) == 4:
if packet:
log.warning("Server sent \"{}\"".format(Int.read(BytesIO(packet))))
if self.is_connected.is_set():
Thread(target=self.restart, name="RestartThread").start()
break
@@ -370,6 +386,11 @@ class Session:
raise TimeoutError
elif isinstance(result, types.RpcError):
Error.raise_it(result, type(data))
elif isinstance(result, types.BadMsgNotification):
raise Exception(self.BAD_MSG_DESCRIPTION.get(
result.error_code,
"Error code {}".format(result.error_code)
))
else:
return result