|
| 1 | +# Pyrogram - Telegram MTProto API Client Library for Python |
| 2 | +# Copyright (C) 2017 Dan Tès <https://github.com/delivrance> |
| 3 | +# |
| 4 | +# This file is part of Pyrogram. |
| 5 | +# |
| 6 | +# Pyrogram is free software: you can redistribute it and/or modify |
| 7 | +# it under the terms of the GNU Lesser General Public License as published |
| 8 | +# by the Free Software Foundation, either version 3 of the License, or |
| 9 | +# (at your option) any later version. |
| 10 | +# |
| 11 | +# Pyrogram is distributed in the hope that it will be useful, |
| 12 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +# GNU Lesser General Public License for more details. |
| 15 | +# |
| 16 | +# You should have received a copy of the GNU Lesser General Public License |
| 17 | +# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. |
| 18 | + |
| 19 | +import logging |
| 20 | +import time |
| 21 | +from hashlib import sha1 |
| 22 | +from io import BytesIO |
| 23 | +from os import urandom |
| 24 | + |
| 25 | +from pyrogram.api import functions, types |
| 26 | +from pyrogram.api.core import Object, Long, Int |
| 27 | +from pyrogram.connection import Connection |
| 28 | +from pyrogram.crypto import IGE, RSA, Prime |
| 29 | +from .internals import MsgId, DataCenter |
| 30 | + |
| 31 | +log = logging.getLogger(__name__) |
| 32 | + |
| 33 | + |
| 34 | +# TODO: When using TCP connection mode, the server may close it at any time, causing the Auth key creation to fail |
| 35 | +# The above is true when dealing with temporary keys, although for perm keys it didn't happened, yet. |
| 36 | + |
| 37 | +class Auth: |
| 38 | + CURRENT_DH_PRIME = int( |
| 39 | + "C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F" |
| 40 | + "48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37" |
| 41 | + "20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64" |
| 42 | + "2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4" |
| 43 | + "A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754" |
| 44 | + "FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4" |
| 45 | + "E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F" |
| 46 | + "0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B", |
| 47 | + 16 |
| 48 | + ) |
| 49 | + |
| 50 | + def __init__(self, dc_id: int, test_mode: bool): |
| 51 | + self.dc_id = dc_id |
| 52 | + self.test_mode = test_mode |
| 53 | + |
| 54 | + self.connection = Connection(DataCenter(dc_id, test_mode)) |
| 55 | + self.msg_id = MsgId() |
| 56 | + |
| 57 | + def pack(self, data: Object) -> bytes: |
| 58 | + return ( |
| 59 | + bytes(8) |
| 60 | + + Long(self.msg_id()) |
| 61 | + + Int(len(data.write())) |
| 62 | + + data.write() |
| 63 | + ) |
| 64 | + |
| 65 | + @staticmethod |
| 66 | + def unpack(b: BytesIO): |
| 67 | + b.seek(20) # Skip auth_key_id (8), message_id (8) and message_length (4) |
| 68 | + return Object.read(b) |
| 69 | + |
| 70 | + def send(self, data: Object): |
| 71 | + data = self.pack(data) |
| 72 | + self.connection.send(data) |
| 73 | + response = BytesIO(self.connection.recv()) |
| 74 | + |
| 75 | + return self.unpack(response) |
| 76 | + |
| 77 | + def create(self): |
| 78 | + """ |
| 79 | + https://core.telegram.org/mtproto/auth_key |
| 80 | + https://core.telegram.org/mtproto/samples-auth_key |
| 81 | + """ |
| 82 | + log.info("Start creating a new auth key on DC{}".format(self.dc_id)) |
| 83 | + |
| 84 | + self.connection.connect() |
| 85 | + |
| 86 | + # Step 1; Step 2 |
| 87 | + nonce = int.from_bytes(urandom(16), "little", signed=True) |
| 88 | + log.debug("Send req_pq: {}".format(nonce)) |
| 89 | + res_pq = self.send(functions.ReqPq(nonce)) |
| 90 | + log.debug("Got ResPq: {}".format(res_pq.server_nonce)) |
| 91 | + |
| 92 | + # Step 3 |
| 93 | + pq = int.from_bytes(res_pq.pq, "big") |
| 94 | + log.debug("Start PQ factorization: {}".format(pq)) |
| 95 | + start = time.time() |
| 96 | + g = Prime.decompose(pq) |
| 97 | + p, q = sorted((g, pq // g)) # p < q |
| 98 | + log.debug("Done PQ factorization ({}s): {} {}".format(round(time.time() - start, 3), p, q)) |
| 99 | + |
| 100 | + # Step 4 |
| 101 | + server_nonce = res_pq.server_nonce |
| 102 | + new_nonce = int.from_bytes(urandom(32), "little", signed=True) |
| 103 | + |
| 104 | + data = types.PQInnerData( |
| 105 | + res_pq.pq, |
| 106 | + int.to_bytes(p, 4, "big"), |
| 107 | + int.to_bytes(q, 4, "big"), |
| 108 | + nonce, |
| 109 | + server_nonce, |
| 110 | + new_nonce, |
| 111 | + ).write() |
| 112 | + |
| 113 | + sha = sha1(data).digest() |
| 114 | + padding = urandom(- (len(data) + len(sha)) % 255) |
| 115 | + data_with_hash = sha + data + padding |
| 116 | + encrypted_data = RSA.encrypt(data_with_hash, res_pq.server_public_key_fingerprints[0]) |
| 117 | + |
| 118 | + log.debug("Done encrypt data with RSA") |
| 119 | + |
| 120 | + # Step 5. TODO: Handle "server_DH_params_fail". Code assumes response is ok |
| 121 | + log.debug("Send req_DH_params") |
| 122 | + server_dh_params = self.send( |
| 123 | + functions.ReqDhParams( |
| 124 | + nonce, |
| 125 | + server_nonce, |
| 126 | + int.to_bytes(p, 4, "big"), |
| 127 | + int.to_bytes(q, 4, "big"), |
| 128 | + res_pq.server_public_key_fingerprints[0], |
| 129 | + encrypted_data |
| 130 | + ) |
| 131 | + ) |
| 132 | + |
| 133 | + encrypted_answer = server_dh_params.encrypted_answer |
| 134 | + |
| 135 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 136 | + new_nonce = int.to_bytes(new_nonce, 32, "little", signed=True) |
| 137 | + |
| 138 | + tmp_aes_key = ( |
| 139 | + sha1(new_nonce + server_nonce).digest() |
| 140 | + + sha1(server_nonce + new_nonce).digest()[:12] |
| 141 | + ) |
| 142 | + |
| 143 | + tmp_aes_iv = ( |
| 144 | + sha1(server_nonce + new_nonce).digest()[12:] |
| 145 | + + sha1(new_nonce + new_nonce).digest() + new_nonce[:4] |
| 146 | + ) |
| 147 | + |
| 148 | + server_nonce = int.from_bytes(server_nonce, "little", signed=True) |
| 149 | + |
| 150 | + answer_with_hash = IGE.decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) |
| 151 | + answer = answer_with_hash[20:] |
| 152 | + |
| 153 | + server_dh_inner_data = Object.read(BytesIO(answer)) |
| 154 | + |
| 155 | + log.debug("Done decrypting answer") |
| 156 | + |
| 157 | + dh_prime = int.from_bytes(server_dh_inner_data.dh_prime, "big") |
| 158 | + delta_time = server_dh_inner_data.server_time - time.time() |
| 159 | + |
| 160 | + log.debug("Delta time: {}".format(round(delta_time, 3))) |
| 161 | + |
| 162 | + # Step 6 |
| 163 | + g = server_dh_inner_data.g |
| 164 | + b = int.from_bytes(urandom(256), "big") |
| 165 | + g_b = int.to_bytes(pow(g, b, dh_prime), 256, "big") |
| 166 | + |
| 167 | + retry_id = 0 |
| 168 | + |
| 169 | + data = types.ClientDhInnerData( |
| 170 | + nonce, |
| 171 | + server_nonce, |
| 172 | + retry_id, |
| 173 | + g_b |
| 174 | + ).write() |
| 175 | + |
| 176 | + sha = sha1(data).digest() |
| 177 | + padding = urandom(- (len(data) + len(sha)) % 16) |
| 178 | + data_with_hash = sha + data + padding |
| 179 | + encrypted_data = IGE.encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) |
| 180 | + |
| 181 | + log.debug("Send set_client_DH_params") |
| 182 | + set_client_dh_params_answer = self.send( |
| 183 | + functions.SetClientDhParams( |
| 184 | + nonce, |
| 185 | + server_nonce, |
| 186 | + encrypted_data |
| 187 | + ) |
| 188 | + ) |
| 189 | + |
| 190 | + # TODO: Handle "auth_key_aux_hash" if the previous step fails |
| 191 | + |
| 192 | + # Step 7; Step 8 |
| 193 | + g_a = int.from_bytes(server_dh_inner_data.g_a, "big") |
| 194 | + auth_key = int.to_bytes(pow(g_a, b, dh_prime), 256, "big") |
| 195 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 196 | + |
| 197 | + # TODO: Handle errors |
| 198 | + |
| 199 | + ####################### |
| 200 | + # Security checks |
| 201 | + ####################### |
| 202 | + |
| 203 | + assert dh_prime == self.CURRENT_DH_PRIME |
| 204 | + log.debug("DH parameters check: OK") |
| 205 | + |
| 206 | + # https://core.telegram.org/mtproto/security_guidelines#g-a-and-g-b-validation |
| 207 | + g_b = int.from_bytes(g_b, "big") |
| 208 | + assert 1 < g < dh_prime - 1 |
| 209 | + assert 1 < g_a < dh_prime - 1 |
| 210 | + assert 1 < g_b < dh_prime - 1 |
| 211 | + assert 2 ** (2048 - 64) < g_a < dh_prime - 2 ** (2048 - 64) |
| 212 | + assert 2 ** (2048 - 64) < g_b < dh_prime - 2 ** (2048 - 64) |
| 213 | + log.debug("g_a and g_b validation: OK") |
| 214 | + |
| 215 | + # https://core.telegram.org/mtproto/security_guidelines#checking-sha1-hash-values |
| 216 | + answer = server_dh_inner_data.write() # Call .write() to remove padding |
| 217 | + assert answer_with_hash[:20] == sha1(answer).digest() |
| 218 | + log.debug("SHA1 hash values check: OK") |
| 219 | + |
| 220 | + # https://core.telegram.org/mtproto/security_guidelines#checking-nonce-server-nonce-and-new-nonce-fields |
| 221 | + # 1st message |
| 222 | + assert nonce == res_pq.nonce |
| 223 | + # 2nd message |
| 224 | + server_nonce = int.from_bytes(server_nonce, "little", signed=True) |
| 225 | + assert nonce == server_dh_params.nonce |
| 226 | + assert server_nonce == server_dh_params.server_nonce |
| 227 | + # 3rd message |
| 228 | + assert nonce == set_client_dh_params_answer.nonce |
| 229 | + assert server_nonce == set_client_dh_params_answer.server_nonce |
| 230 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 231 | + log.debug("Nonce fields check: OK") |
| 232 | + |
| 233 | + # Step 9 |
| 234 | + server_salt = IGE.xor(new_nonce[:8], server_nonce[:8]) |
| 235 | + |
| 236 | + log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little"))) |
| 237 | + |
| 238 | + log.info( |
| 239 | + "Done auth key exchange: {}".format( |
| 240 | + set_client_dh_params_answer.__class__.__name__ |
| 241 | + ) |
| 242 | + ) |
| 243 | + |
| 244 | + self.connection.close() |
| 245 | + |
| 246 | + return auth_key |
0 commit comments