2017-12-05 11:41:07 +00:00
|
|
|
# Pyrogram - Telegram MTProto API Client Library for Python
|
2019-01-01 11:36:16 +00:00
|
|
|
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
|
2017-12-05 11:41:07 +00:00
|
|
|
#
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
from hashlib import sha1
|
|
|
|
from io import BytesIO
|
|
|
|
from os import urandom
|
|
|
|
|
|
|
|
from pyrogram.api import functions, types
|
|
|
|
from pyrogram.api.core import Object, Long, Int
|
|
|
|
from pyrogram.connection import Connection
|
2018-01-28 00:44:38 +00:00
|
|
|
from pyrogram.crypto import AES, RSA, Prime
|
2018-06-13 11:37:12 +00:00
|
|
|
from .internals import MsgId
|
2017-12-05 11:41:07 +00:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class Auth:
|
2017-12-31 10:46:42 +00:00
|
|
|
MAX_RETRIES = 5
|
|
|
|
|
2018-06-13 11:37:12 +00:00
|
|
|
def __init__(self, dc_id: int, test_mode: bool, ipv6: bool, proxy: dict):
|
2017-12-05 11:41:07 +00:00
|
|
|
self.dc_id = dc_id
|
|
|
|
self.test_mode = test_mode
|
2018-06-13 11:37:12 +00:00
|
|
|
self.ipv6 = ipv6
|
2018-05-24 19:19:57 +00:00
|
|
|
self.proxy = 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
|
|
|
|
def pack(data: Object) -> 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)
|
|
|
|
return Object.read(b)
|
|
|
|
|
|
|
|
def send(self, data: Object):
|
|
|
|
data = self.pack(data)
|
|
|
|
self.connection.send(data)
|
|
|
|
response = BytesIO(self.connection.recv())
|
|
|
|
|
|
|
|
return self.unpack(response)
|
|
|
|
|
|
|
|
def create(self):
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
2017-12-09 14:26:33 +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.
|
2017-12-09 14:26:33 +00:00
|
|
|
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
|
|
|
|
2017-12-09 14:26:33 +00:00
|
|
|
try:
|
|
|
|
log.info("Start creating a new auth key on DC{}".format(self.dc_id))
|
|
|
|
|
|
|
|
self.connection.connect()
|
|
|
|
|
|
|
|
# Step 1; Step 2
|
|
|
|
nonce = int.from_bytes(urandom(16), "little", signed=True)
|
|
|
|
log.debug("Send req_pq: {}".format(nonce))
|
2019-03-16 15:50:40 +00:00
|
|
|
res_pq = self.send(functions.ReqPqMulti(nonce=nonce))
|
2017-12-09 14:26:33 +00:00
|
|
|
log.debug("Got ResPq: {}".format(res_pq.server_nonce))
|
2018-03-08 09:14:26 +00:00
|
|
|
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")
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
# Step 3
|
|
|
|
pq = int.from_bytes(res_pq.pq, "big")
|
|
|
|
log.debug("Start PQ factorization: {}".format(pq))
|
|
|
|
start = time.time()
|
|
|
|
g = Prime.decompose(pq)
|
|
|
|
p, q = sorted((g, pq // g)) # p < q
|
|
|
|
log.debug("Done PQ factorization ({}s): {} {}".format(round(time.time() - start, 3), p, q))
|
|
|
|
|
|
|
|
# Step 4
|
|
|
|
server_nonce = res_pq.server_nonce
|
|
|
|
new_nonce = int.from_bytes(urandom(32), "little", signed=True)
|
|
|
|
|
|
|
|
data = types.PQInnerData(
|
2019-03-16 15:50:40 +00:00
|
|
|
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,
|
2017-12-09 14:26:33 +00:00
|
|
|
).write()
|
|
|
|
|
|
|
|
sha = sha1(data).digest()
|
|
|
|
padding = urandom(- (len(data) + len(sha)) % 255)
|
|
|
|
data_with_hash = sha + data + padding
|
2018-03-08 09:14:26 +00:00
|
|
|
encrypted_data = RSA.encrypt(data_with_hash, public_key_fingerprint)
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
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 = self.send(
|
2018-01-04 15:30:29 +00:00
|
|
|
functions.ReqDHParams(
|
2019-03-16 15:50:40 +00:00
|
|
|
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
|
2017-12-09 14:26:33 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
encrypted_answer = server_dh_params.encrypted_answer
|
|
|
|
|
2018-08-31 23:27:22 +00:00
|
|
|
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
|
|
|
|
new_nonce = new_nonce.to_bytes(32, "little", signed=True)
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
tmp_aes_key = (
|
2019-03-16 18:23:23 +00:00
|
|
|
sha1(new_nonce + server_nonce).digest()
|
|
|
|
+ sha1(server_nonce + new_nonce).digest()[:12]
|
2017-12-09 14:26:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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]
|
2017-12-09 14:26:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
|
|
|
|
|
2018-05-18 12:15:35 +00:00
|
|
|
answer_with_hash = AES.ige256_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv)
|
2017-12-09 14:26:33 +00:00
|
|
|
answer = answer_with_hash[20:]
|
|
|
|
|
|
|
|
server_dh_inner_data = Object.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("Delta time: {}".format(round(delta_time, 3)))
|
|
|
|
|
|
|
|
# Step 6
|
|
|
|
g = server_dh_inner_data.g
|
|
|
|
b = int.from_bytes(urandom(256), "big")
|
2018-08-31 23:27:22 +00:00
|
|
|
g_b = pow(g, b, dh_prime).to_bytes(256, "big")
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
retry_id = 0
|
|
|
|
|
2018-01-04 15:30:29 +00:00
|
|
|
data = types.ClientDHInnerData(
|
2019-03-16 15:50:40 +00:00
|
|
|
nonce=nonce,
|
|
|
|
server_nonce=server_nonce,
|
|
|
|
retry_id=retry_id,
|
|
|
|
g_b=g_b
|
2017-12-09 14:26:33 +00:00
|
|
|
).write()
|
|
|
|
|
|
|
|
sha = sha1(data).digest()
|
|
|
|
padding = urandom(- (len(data) + len(sha)) % 16)
|
|
|
|
data_with_hash = sha + data + padding
|
2018-05-18 12:15:35 +00:00
|
|
|
encrypted_data = AES.ige256_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv)
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
log.debug("Send set_client_DH_params")
|
|
|
|
set_client_dh_params_answer = self.send(
|
2018-01-04 15:30:29 +00:00
|
|
|
functions.SetClientDHParams(
|
2019-03-16 15:50:40 +00:00
|
|
|
nonce=nonce,
|
|
|
|
server_nonce=server_nonce,
|
|
|
|
encrypted_data=encrypted_data
|
2017-12-09 14:26:33 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# 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")
|
2018-08-31 23:27:22 +00:00
|
|
|
auth_key = pow(g_a, b, dh_prime).to_bytes(256, "big")
|
|
|
|
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
# TODO: Handle errors
|
|
|
|
|
|
|
|
#######################
|
|
|
|
# Security checks
|
|
|
|
#######################
|
|
|
|
|
2018-08-29 20:04:04 +00:00
|
|
|
assert dh_prime == Prime.CURRENT_DH_PRIME
|
2017-12-09 14:26:33 +00:00
|
|
|
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
|
2018-08-31 23:27:22 +00:00
|
|
|
server_nonce = server_nonce.to_bytes(16, "little", signed=True)
|
2017-12-09 14:26:33 +00:00
|
|
|
log.debug("Nonce fields check: OK")
|
|
|
|
|
|
|
|
# Step 9
|
2018-01-28 00:44:38 +00:00
|
|
|
server_salt = AES.xor(new_nonce[:8], server_nonce[:8])
|
2017-12-09 14:26:33 +00:00
|
|
|
|
|
|
|
log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little")))
|
|
|
|
|
|
|
|
log.info(
|
|
|
|
"Done auth key exchange: {}".format(
|
|
|
|
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
|
|
|
|
|
2017-12-22 08:27:47 +00:00
|
|
|
time.sleep(1)
|
2017-12-09 14:26:33 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
return auth_key
|
|
|
|
finally:
|
|
|
|
self.connection.close()
|