|
31 | 31 | log = logging.getLogger(__name__) |
32 | 32 |
|
33 | 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 | 34 | class Auth: |
38 | 35 | CURRENT_DH_PRIME = int( |
39 | 36 | "C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F" |
@@ -79,168 +76,176 @@ def create(self): |
79 | 76 | https://core.telegram.org/mtproto/auth_key |
80 | 77 | https://core.telegram.org/mtproto/samples-auth_key |
81 | 78 | """ |
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 | 79 |
|
246 | | - return auth_key |
| 80 | + # The server may close the connection at any time, causing the auth key creation to fail. |
| 81 | + # If that happens, just try again until it succeed. |
| 82 | + while True: |
| 83 | + try: |
| 84 | + log.info("Start creating a new auth key on DC{}".format(self.dc_id)) |
| 85 | + |
| 86 | + self.connection.connect() |
| 87 | + |
| 88 | + # Step 1; Step 2 |
| 89 | + nonce = int.from_bytes(urandom(16), "little", signed=True) |
| 90 | + log.debug("Send req_pq: {}".format(nonce)) |
| 91 | + res_pq = self.send(functions.ReqPq(nonce)) |
| 92 | + log.debug("Got ResPq: {}".format(res_pq.server_nonce)) |
| 93 | + |
| 94 | + # Step 3 |
| 95 | + pq = int.from_bytes(res_pq.pq, "big") |
| 96 | + log.debug("Start PQ factorization: {}".format(pq)) |
| 97 | + start = time.time() |
| 98 | + g = Prime.decompose(pq) |
| 99 | + p, q = sorted((g, pq // g)) # p < q |
| 100 | + log.debug("Done PQ factorization ({}s): {} {}".format(round(time.time() - start, 3), p, q)) |
| 101 | + |
| 102 | + # Step 4 |
| 103 | + server_nonce = res_pq.server_nonce |
| 104 | + new_nonce = int.from_bytes(urandom(32), "little", signed=True) |
| 105 | + |
| 106 | + data = types.PQInnerData( |
| 107 | + res_pq.pq, |
| 108 | + int.to_bytes(p, 4, "big"), |
| 109 | + int.to_bytes(q, 4, "big"), |
| 110 | + nonce, |
| 111 | + server_nonce, |
| 112 | + new_nonce, |
| 113 | + ).write() |
| 114 | + |
| 115 | + sha = sha1(data).digest() |
| 116 | + padding = urandom(- (len(data) + len(sha)) % 255) |
| 117 | + data_with_hash = sha + data + padding |
| 118 | + encrypted_data = RSA.encrypt(data_with_hash, res_pq.server_public_key_fingerprints[0]) |
| 119 | + |
| 120 | + log.debug("Done encrypt data with RSA") |
| 121 | + |
| 122 | + # Step 5. TODO: Handle "server_DH_params_fail". Code assumes response is ok |
| 123 | + log.debug("Send req_DH_params") |
| 124 | + server_dh_params = self.send( |
| 125 | + functions.ReqDhParams( |
| 126 | + nonce, |
| 127 | + server_nonce, |
| 128 | + int.to_bytes(p, 4, "big"), |
| 129 | + int.to_bytes(q, 4, "big"), |
| 130 | + res_pq.server_public_key_fingerprints[0], |
| 131 | + encrypted_data |
| 132 | + ) |
| 133 | + ) |
| 134 | + |
| 135 | + encrypted_answer = server_dh_params.encrypted_answer |
| 136 | + |
| 137 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 138 | + new_nonce = int.to_bytes(new_nonce, 32, "little", signed=True) |
| 139 | + |
| 140 | + tmp_aes_key = ( |
| 141 | + sha1(new_nonce + server_nonce).digest() |
| 142 | + + sha1(server_nonce + new_nonce).digest()[:12] |
| 143 | + ) |
| 144 | + |
| 145 | + tmp_aes_iv = ( |
| 146 | + sha1(server_nonce + new_nonce).digest()[12:] |
| 147 | + + sha1(new_nonce + new_nonce).digest() + new_nonce[:4] |
| 148 | + ) |
| 149 | + |
| 150 | + server_nonce = int.from_bytes(server_nonce, "little", signed=True) |
| 151 | + |
| 152 | + answer_with_hash = IGE.decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) |
| 153 | + answer = answer_with_hash[20:] |
| 154 | + |
| 155 | + server_dh_inner_data = Object.read(BytesIO(answer)) |
| 156 | + |
| 157 | + log.debug("Done decrypting answer") |
| 158 | + |
| 159 | + dh_prime = int.from_bytes(server_dh_inner_data.dh_prime, "big") |
| 160 | + delta_time = server_dh_inner_data.server_time - time.time() |
| 161 | + |
| 162 | + log.debug("Delta time: {}".format(round(delta_time, 3))) |
| 163 | + |
| 164 | + # Step 6 |
| 165 | + g = server_dh_inner_data.g |
| 166 | + b = int.from_bytes(urandom(256), "big") |
| 167 | + g_b = int.to_bytes(pow(g, b, dh_prime), 256, "big") |
| 168 | + |
| 169 | + retry_id = 0 |
| 170 | + |
| 171 | + data = types.ClientDhInnerData( |
| 172 | + nonce, |
| 173 | + server_nonce, |
| 174 | + retry_id, |
| 175 | + g_b |
| 176 | + ).write() |
| 177 | + |
| 178 | + sha = sha1(data).digest() |
| 179 | + padding = urandom(- (len(data) + len(sha)) % 16) |
| 180 | + data_with_hash = sha + data + padding |
| 181 | + encrypted_data = IGE.encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv) |
| 182 | + |
| 183 | + log.debug("Send set_client_DH_params") |
| 184 | + set_client_dh_params_answer = self.send( |
| 185 | + functions.SetClientDhParams( |
| 186 | + nonce, |
| 187 | + server_nonce, |
| 188 | + encrypted_data |
| 189 | + ) |
| 190 | + ) |
| 191 | + |
| 192 | + # TODO: Handle "auth_key_aux_hash" if the previous step fails |
| 193 | + |
| 194 | + # Step 7; Step 8 |
| 195 | + g_a = int.from_bytes(server_dh_inner_data.g_a, "big") |
| 196 | + auth_key = int.to_bytes(pow(g_a, b, dh_prime), 256, "big") |
| 197 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 198 | + |
| 199 | + # TODO: Handle errors |
| 200 | + |
| 201 | + ####################### |
| 202 | + # Security checks |
| 203 | + ####################### |
| 204 | + |
| 205 | + assert dh_prime == self.CURRENT_DH_PRIME |
| 206 | + log.debug("DH parameters check: OK") |
| 207 | + |
| 208 | + # https://core.telegram.org/mtproto/security_guidelines#g-a-and-g-b-validation |
| 209 | + g_b = int.from_bytes(g_b, "big") |
| 210 | + assert 1 < g < dh_prime - 1 |
| 211 | + assert 1 < g_a < dh_prime - 1 |
| 212 | + assert 1 < g_b < dh_prime - 1 |
| 213 | + assert 2 ** (2048 - 64) < g_a < dh_prime - 2 ** (2048 - 64) |
| 214 | + assert 2 ** (2048 - 64) < g_b < dh_prime - 2 ** (2048 - 64) |
| 215 | + log.debug("g_a and g_b validation: OK") |
| 216 | + |
| 217 | + # https://core.telegram.org/mtproto/security_guidelines#checking-sha1-hash-values |
| 218 | + answer = server_dh_inner_data.write() # Call .write() to remove padding |
| 219 | + assert answer_with_hash[:20] == sha1(answer).digest() |
| 220 | + log.debug("SHA1 hash values check: OK") |
| 221 | + |
| 222 | + # https://core.telegram.org/mtproto/security_guidelines#checking-nonce-server-nonce-and-new-nonce-fields |
| 223 | + # 1st message |
| 224 | + assert nonce == res_pq.nonce |
| 225 | + # 2nd message |
| 226 | + server_nonce = int.from_bytes(server_nonce, "little", signed=True) |
| 227 | + assert nonce == server_dh_params.nonce |
| 228 | + assert server_nonce == server_dh_params.server_nonce |
| 229 | + # 3rd message |
| 230 | + assert nonce == set_client_dh_params_answer.nonce |
| 231 | + assert server_nonce == set_client_dh_params_answer.server_nonce |
| 232 | + server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True) |
| 233 | + log.debug("Nonce fields check: OK") |
| 234 | + |
| 235 | + # Step 9 |
| 236 | + server_salt = IGE.xor(new_nonce[:8], server_nonce[:8]) |
| 237 | + |
| 238 | + log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little"))) |
| 239 | + |
| 240 | + log.info( |
| 241 | + "Done auth key exchange: {}".format( |
| 242 | + set_client_dh_params_answer.__class__.__name__ |
| 243 | + ) |
| 244 | + ) |
| 245 | + except: # TODO: Too broad exception clause |
| 246 | + log.warning("Auth key creation failed. Let's try again.") |
| 247 | + continue |
| 248 | + else: |
| 249 | + return auth_key |
| 250 | + finally: |
| 251 | + self.connection.close() |
0 commit comments