MTPyroger/pyrogram/session/auth.py

261 lines
9.9 KiB
Python
Raw Normal View History

2020-03-21 14:43:32 +00:00
# Pyrogram - Telegram MTProto API Client Library for Python
2021-01-01 21:58:48 +00:00
# Copyright (C) 2017-2021 Dan <https://github.com/delivrance>
2017-12-05 11:41:07 +00:00
#
2020-03-21 14:43:32 +00:00
# This file is part of Pyrogram.
2017-12-05 11:41:07 +00:00
#
2020-03-21 14:43:32 +00:00
# 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.
2017-12-05 11:41:07 +00:00
#
2020-03-21 14:43:32 +00:00
# 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.
2017-12-05 11:41:07 +00:00
#
2020-03-21 14:43:32 +00:00
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
2017-12-05 11:41:07 +00:00
import asyncio
2017-12-05 11:41:07 +00:00
import logging
import time
from hashlib import sha1
from io import BytesIO
from os import urandom
import pyrogram
from pyrogram import raw
2017-12-05 11:41:07 +00:00
from pyrogram.connection import Connection
from pyrogram.crypto import aes, rsa, prime
from pyrogram.raw.core import TLObject, Long, Int
2018-06-13 11:37:12 +00:00
from .internals import MsgId
2017-12-05 11:41:07 +00:00
log = logging.getLogger(__name__)
2017-12-05 11:41:07 +00:00
class Auth:
2017-12-31 10:46:42 +00:00
MAX_RETRIES = 5
def __init__(self, client: "pyrogram.Client", dc_id: int, test_mode: bool):
2017-12-05 11:41:07 +00:00
self.dc_id = dc_id
self.test_mode = test_mode
self.ipv6 = client.ipv6
self.proxy = client.proxy
2017-12-05 11:41:07 +00:00
2018-05-24 19:19:57 +00:00
self.connection = None
2017-12-05 11:41:07 +00:00
2018-02-18 16:31:00 +00:00
@staticmethod
2019-06-03 12:19:50 +00:00
def pack(data: TLObject) -> bytes:
2017-12-05 11:41:07 +00:00
return (
2019-03-16 18:23:23 +00:00
bytes(8)
+ Long(MsgId())
+ Int(len(data.write()))
+ data.write()
2017-12-05 11:41:07 +00:00
)
@staticmethod
def unpack(b: BytesIO):
b.seek(20) # Skip auth_key_id (8), message_id (8) and message_length (4)
2019-06-03 12:19:50 +00:00
return TLObject.read(b)
2017-12-05 11:41:07 +00:00
async def send(self, data: TLObject):
2017-12-05 11:41:07 +00:00
data = self.pack(data)
await self.connection.send(data)
response = BytesIO(await self.connection.recv())
2017-12-05 11:41:07 +00:00
return self.unpack(response)
async def create(self):
2017-12-05 11:41:07 +00:00
"""
https://core.telegram.org/mtproto/auth_key
https://core.telegram.org/mtproto/samples-auth_key
"""
2017-12-31 10:46:42 +00:00
retries_left = self.MAX_RETRIES
2017-12-05 11:41:07 +00:00
# The server may close the connection at any time, causing the auth key creation to fail.
2017-12-31 10:46:42 +00:00
# If that happens, just try again up to MAX_RETRIES times.
while True:
2018-06-13 11:37:12 +00:00
self.connection = Connection(self.dc_id, self.test_mode, self.ipv6, self.proxy)
2018-05-24 19:19:57 +00:00
try:
log.info(f"Start creating a new auth key on DC{self.dc_id}")
await self.connection.connect()
# Step 1; Step 2
nonce = int.from_bytes(urandom(16), "little", signed=True)
log.debug(f"Send req_pq: {nonce}")
res_pq = await self.send(raw.functions.ReqPqMulti(nonce=nonce))
log.debug(f"Got ResPq: {res_pq.server_nonce}")
log.debug(f"Server public key fingerprints: {res_pq.server_public_key_fingerprints}")
for i in res_pq.server_public_key_fingerprints:
if i in rsa.server_public_keys:
log.debug(f"Using fingerprint: {i}")
public_key_fingerprint = i
break
else:
log.debug(f"Fingerprint unknown: {i}")
else:
raise Exception("Public key not found")
# Step 3
pq = int.from_bytes(res_pq.pq, "big")
log.debug(f"Start PQ factorization: {pq}")
start = time.time()
g = prime.decompose(pq)
p, q = sorted((g, pq // g)) # p < q
log.debug(f"Done PQ factorization ({round(time.time() - start, 3)}s): {p} {q}")
# Step 4
server_nonce = res_pq.server_nonce
new_nonce = int.from_bytes(urandom(32), "little", signed=True)
data = raw.types.PQInnerData(
pq=res_pq.pq,
p=p.to_bytes(4, "big"),
q=q.to_bytes(4, "big"),
nonce=nonce,
server_nonce=server_nonce,
new_nonce=new_nonce,
).write()
sha = sha1(data).digest()
padding = urandom(- (len(data) + len(sha)) % 255)
data_with_hash = sha + data + padding
encrypted_data = rsa.encrypt(data_with_hash, public_key_fingerprint)
log.debug("Done encrypt data with RSA")
# Step 5. TODO: Handle "server_DH_params_fail". Code assumes response is ok
log.debug("Send req_DH_params")
server_dh_params = await self.send(
raw.functions.ReqDHParams(
nonce=nonce,
server_nonce=server_nonce,
p=p.to_bytes(4, "big"),
q=q.to_bytes(4, "big"),
public_key_fingerprint=public_key_fingerprint,
encrypted_data=encrypted_data
)
)
encrypted_answer = server_dh_params.encrypted_answer
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
new_nonce = new_nonce.to_bytes(32, "little", signed=True)
tmp_aes_key = (
2019-03-16 18:23:23 +00:00
sha1(new_nonce + server_nonce).digest()
+ sha1(server_nonce + new_nonce).digest()[:12]
)
tmp_aes_iv = (
2019-03-16 18:23:23 +00:00
sha1(server_nonce + new_nonce).digest()[12:]
+ sha1(new_nonce + new_nonce).digest() + new_nonce[:4]
)
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
answer_with_hash = aes.ige256_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv)
answer = answer_with_hash[20:]
2019-06-03 12:19:50 +00:00
server_dh_inner_data = TLObject.read(BytesIO(answer))
log.debug("Done decrypting answer")
dh_prime = int.from_bytes(server_dh_inner_data.dh_prime, "big")
delta_time = server_dh_inner_data.server_time - time.time()
log.debug(f"Delta time: {round(delta_time, 3)}")
# Step 6
g = server_dh_inner_data.g
b = int.from_bytes(urandom(256), "big")
g_b = pow(g, b, dh_prime).to_bytes(256, "big")
retry_id = 0
data = raw.types.ClientDHInnerData(
nonce=nonce,
server_nonce=server_nonce,
retry_id=retry_id,
g_b=g_b
).write()
sha = sha1(data).digest()
padding = urandom(- (len(data) + len(sha)) % 16)
data_with_hash = sha + data + padding
encrypted_data = aes.ige256_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv)
log.debug("Send set_client_DH_params")
set_client_dh_params_answer = await self.send(
raw.functions.SetClientDHParams(
nonce=nonce,
server_nonce=server_nonce,
encrypted_data=encrypted_data
)
)
# TODO: Handle "auth_key_aux_hash" if the previous step fails
# Step 7; Step 8
g_a = int.from_bytes(server_dh_inner_data.g_a, "big")
auth_key = pow(g_a, b, dh_prime).to_bytes(256, "big")
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
# TODO: Handle errors
#######################
# Security checks
#######################
assert dh_prime == prime.CURRENT_DH_PRIME
log.debug("DH parameters check: OK")
# https://core.telegram.org/mtproto/security_guidelines#g-a-and-g-b-validation
g_b = int.from_bytes(g_b, "big")
assert 1 < g < dh_prime - 1
assert 1 < g_a < dh_prime - 1
assert 1 < g_b < dh_prime - 1
assert 2 ** (2048 - 64) < g_a < dh_prime - 2 ** (2048 - 64)
assert 2 ** (2048 - 64) < g_b < dh_prime - 2 ** (2048 - 64)
log.debug("g_a and g_b validation: OK")
# https://core.telegram.org/mtproto/security_guidelines#checking-sha1-hash-values
answer = server_dh_inner_data.write() # Call .write() to remove padding
assert answer_with_hash[:20] == sha1(answer).digest()
log.debug("SHA1 hash values check: OK")
# https://core.telegram.org/mtproto/security_guidelines#checking-nonce-server-nonce-and-new-nonce-fields
# 1st message
assert nonce == res_pq.nonce
# 2nd message
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
assert nonce == server_dh_params.nonce
assert server_nonce == server_dh_params.server_nonce
# 3rd message
assert nonce == set_client_dh_params_answer.nonce
assert server_nonce == set_client_dh_params_answer.server_nonce
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
log.debug("Nonce fields check: OK")
# Step 9
server_salt = aes.xor(new_nonce[:8], server_nonce[:8])
log.debug(f"Server salt: {int.from_bytes(server_salt, 'little')}")
log.info(f"Done auth key exchange: {set_client_dh_params_answer.__class__.__name__}")
2017-12-23 13:02:14 +00:00
except Exception as e:
2017-12-31 10:46:42 +00:00
if retries_left:
retries_left -= 1
else:
raise e
await asyncio.sleep(1)
continue
else:
return auth_key
finally:
self.connection.close()