def test_aes_encrypt(self): value = AES.encrypt_ige(self.plain_text, self.key, self.iv) take = 16 # Don't take all the bytes, since latest involve are random padding assert value[:take] == self.cipher_text[:take],\ ('Ciphered text ("{}") does not equal expected ("{}")' .format(value[:take], self.cipher_text[:take])) value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) assert value == self.cipher_text_padded, ( 'Ciphered text ("{}") does not equal expected ("{}")'.format( value, self.cipher_text_padded))
def test_aes_encrypt(self): value = AES.encrypt_ige(self.plain_text, self.key, self.iv) take = 16 # Don't take all the bytes, since latest involve are random padding assert value[:take] == self.cipher_text[:take],\ ('Ciphered text ("{}") does not equal expected ("{}")' .format(value[:take], self.cipher_text[:take])) value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) assert value == self.cipher_text_padded, ( 'Ciphered text ("{}") does not equal expected ("{}")' .format(value, self.cipher_text_padded))
def decode_msg(self, body): """Decodes an received encrypted message body bytes""" message = None remote_msg_id = None remote_sequence = None with BinaryReader(body) as reader: if len(body) < 8: raise BufferError("Can't decode packet ({})".format(body)) # TODO Check for both auth key ID and msg_key correctness reader.read_long() # remote_auth_key_id msg_key = reader.read(16) key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False) plain_text = AES.decrypt_ige( reader.read(len(body) - reader.tell_position()), key, iv) with BinaryReader(plain_text) as plain_text_reader: plain_text_reader.read_long() # remote_salt plain_text_reader.read_long() # remote_session_id remote_msg_id = plain_text_reader.read_long() remote_sequence = plain_text_reader.read_int() msg_len = plain_text_reader.read_int() message = plain_text_reader.read(msg_len) return message, remote_msg_id, remote_sequence
def send_packet(self, packet, request): """Sends the given packet bytes with the additional information of the original request. This does NOT lock the threads!""" request.msg_id = self.session.get_new_msg_id() # First calculate plain_text to encrypt it with BinaryWriter() as plain_writer: plain_writer.write_long(self.session.salt, signed=False) plain_writer.write_long(self.session.id, signed=False) plain_writer.write_long(request.msg_id) plain_writer.write_int(self.generate_sequence(request.confirmed)) plain_writer.write_int(len(packet)) plain_writer.write(packet) msg_key = utils.calc_msg_key(plain_writer.get_bytes()) key, iv = utils.calc_key(self.session.auth_key.key, msg_key, True) cipher_text = AES.encrypt_ige(plain_writer.get_bytes(), key, iv) # And then finally send the encrypted packet with BinaryWriter() as cipher_writer: cipher_writer.write_long(self.session.auth_key.key_id, signed=False) cipher_writer.write(msg_key) cipher_writer.write(cipher_text) self.transport.send(cipher_writer.get_bytes())
def unpack_message(session, reader): """Unpacks a message following MtProto 2.0 guidelines""" # See https://core.telegram.org/mtproto/description if reader.read_long(signed=False) != session.auth_key.key_id: raise SecurityError('Server replied with an invalid auth key') msg_key = reader.read(16) aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, False) data = BinaryReader(AES.decrypt_ige(reader.read(), aes_key, aes_iv)) data.read_long() # remote_salt if data.read_long() != session.id: raise SecurityError('Server replied with a wrong session ID') remote_msg_id = data.read_long() remote_sequence = data.read_int() msg_len = data.read_int() message = data.read(msg_len) # https://core.telegram.org/mtproto/security_guidelines # Sections "checking sha256 hash" and "message length" if msg_key != sha256(session.auth_key.key[96:96 + 32] + data.get_bytes()).digest()[8:24]: raise SecurityError("Received msg_key doesn't match with expected one") return message, remote_msg_id, remote_sequence
def unpack_message(session, reader): """Unpacks a message following MtProto 2.0 guidelines""" # See https://core.telegram.org/mtproto/description if reader.read_long(signed=False) != session.auth_key.key_id: raise SecurityError('Server replied with an invalid auth key') msg_key = reader.read(16) aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, False) data = BinaryReader(AES.decrypt_ige(reader.read(), aes_key, aes_iv)) data.read_long() # remote_salt if data.read_long() != session.id: raise SecurityError('Server replied with a wrong session ID') remote_msg_id = data.read_long() remote_sequence = data.read_int() msg_len = data.read_int() message = data.read(msg_len) # https://core.telegram.org/mtproto/security_guidelines # Sections "checking sha256 hash" and "message length" if msg_key != sha256( session.auth_key.key[96:96 + 32] + data.get_bytes()).digest()[8:24]: raise SecurityError("Received msg_key doesn't match with expected one") return message, remote_msg_id, remote_sequence
def test_aes_decrypt(self): # The ciphered text must always be padded value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) self.assertEqual( value, self.plain_text_padded, msg='Decrypted text ("{}") does not equal expected ("{}")'.format( value, self.plain_text_padded))
def pack_message(session, message): """Packs a message following MtProto 2.0 guidelines""" # See https://core.telegram.org/mtproto/description data = struct.pack('<qq', session.salt, session.id) + bytes(message) padding = os.urandom(-(len(data) + 12) % 16 + 12) # Being substr(what, offset, length); x = 0 for client # "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)" msg_key_large = sha256(session.auth_key.key[88:88 + 32] + data + padding).digest() # "msg_key = substr (msg_key_large, 8, 16)" msg_key = msg_key_large[8:24] aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, True) key_id = struct.pack('<Q', session.auth_key.key_id) return key_id + msg_key + AES.encrypt_ige(data + padding, aes_key, aes_iv)
def pack_message(session, message): """Packs a message following MtProto 2.0 guidelines""" # See https://core.telegram.org/mtproto/description data = struct.pack('<qq', session.salt, session.id) + bytes(message) padding = os.urandom(-(len(data) + 12) % 16 + 12) # Being substr(what, offset, length); x = 0 for client # "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)" msg_key_large = sha256( session.auth_key.key[88:88 + 32] + data + padding).digest() # "msg_key = substr (msg_key_large, 8, 16)" msg_key = msg_key_large[8:24] aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, True) key_id = struct.pack('<Q', session.auth_key.key_id) return key_id + msg_key + AES.encrypt_ige(data + padding, aes_key, aes_iv)
def decrypt_mtproto1(self, message_key, chat_id, encrypted_data): aes_key, aes_iv = _old_calc_key( self.get_secret_chat(chat_id).auth_key, message_key, True) decrypted_data = AES.decrypt_ige(encrypted_data, aes_key, aes_iv) message_data_length = struct.unpack('<I', decrypted_data[:4])[0] message_data = decrypted_data[4:message_data_length + 4] if message_data_length > len(decrypted_data): raise SecurityError("message data length is too big") if message_key != sha1( decrypted_data[:4 + message_data_length]).digest()[-16:]: raise SecurityError("Message key mismatch") if len(decrypted_data) - 4 - message_data_length > 15: raise SecurityError("Difference is too big") if len(decrypted_data) % 16 != 0: raise SecurityError("Decrypted data can not be divided by 16") return BinaryReader(message_data).tgread_object()
async def encrypt_secret_message( self, peer: [int, SecretChat, InputEncryptedChat, EncryptedChat], message): peer = self.get_secret_chat(peer) peer.ttr -= 1 if peer.layer > 8: if (peer.ttr <= 0 or (time() - peer.updated) > 7 * 24 * 60 * 60 ) and peer.rekeying[0] == 0: await self.rekey(peer) message = DecryptedMessageLayer( layer=peer.layer, random_bytes=os.urandom(15 + 4 * random.randint(0, 2)), in_seq_no=self.generate_secret_in_seq_no(peer.id), out_seq_no=self.generate_secret_out_seq_no(peer.id), message=message) peer.out_seq_no += 1 peer.outgoing[peer.out_seq_no] = message message = bytes(message) message = struct.pack('<I', len(message)) + message if peer.mtproto == 2: padding = (16 - len(message) % 16) % 16 if padding < 12: padding += 16 message += os.urandom(padding) is_admin = (0 if peer.admin else 8) first_str = peer.auth_key[88 + is_admin:88 + 32 + is_admin] message_key = sha256(first_str + message).digest()[8:24] aes_key, aes_iv = MTProtoState._calc_key(peer.auth_key, message_key, peer.admin) else: message_key = sha1(message).digest()[-16:] aes_key, aes_iv = _old_calc_key(peer.auth_key, message_key, True) padding = (16 - len(message) % 16) % 16 message += os.urandom(padding) key_fingerprint = struct.unpack('<q', sha1(peer.auth_key).digest()[-8:])[0] message = struct.pack( '<q', key_fingerprint) + message_key + AES.encrypt_ige( bytes.fromhex(message.hex()), aes_key, aes_iv) return message
def decrypt_mtproto2(self, message_key, chat_id, encrypted_data): peer = self.get_secret_chat(chat_id) aes_key, aes_iv = MTProtoState._calc_key(peer.auth_key, message_key, not peer.admin) decrypted_data = AES.decrypt_ige(encrypted_data, aes_key, aes_iv) message_data_length = struct.unpack('<I', decrypted_data[:4])[0] message_data = decrypted_data[4:message_data_length + 4] if message_data_length > len(decrypted_data): raise SecurityError("message data length is too big") is_admin = (8 if peer.admin else 0) first_str = peer.auth_key[88 + is_admin:88 + 32 + is_admin] if message_key != sha256(first_str + decrypted_data).digest()[8:24]: raise SecurityError("Message key mismatch") if len(decrypted_data) - 4 - message_data_length < 12: raise SecurityError("Padding is too small") if len(decrypted_data) % 16 != 0: raise SecurityError("Decrpyted data not divisble by 16") return BinaryReader(message_data).tgread_object()
def test_aes_decrypt(self): # The ciphered text must always be padded value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) assert value == self.plain_text_padded, ( 'Decrypted text ("{}") does not equal expected ("{}")' .format(value, self.plain_text_padded))
def do_authentication(transport): """Executes the authentication process with the Telegram servers. If no error is rose, returns both the authorization key and the time offset""" sender = MtProtoPlainSender(transport) # Step 1 sending: PQ Request nonce = os.urandom(16) with BinaryWriter() as writer: writer.write_int(0x60469778, signed=False) # Constructor number writer.write(nonce) sender.send(writer.get_bytes()) # Step 1 response: PQ Request pq, pq_bytes, server_nonce, fingerprints = None, None, None, [] with BinaryReader(sender.receive()) as reader: response_code = reader.read_int(signed=False) if response_code != 0x05162463: raise AssertionError('Invalid response code: {}'.format( hex(response_code))) nonce_from_server = reader.read(16) if nonce_from_server != nonce: raise AssertionError('Invalid nonce from server') server_nonce = reader.read(16) pq_bytes = reader.tgread_bytes() pq = get_int(pq_bytes) vector_id = reader.read_int() if vector_id != 0x1cb5c415: raise AssertionError('Invalid vector constructor ID: {}'.format( hex(response_code))) fingerprints = [] fingerprint_count = reader.read_int() for _ in range(fingerprint_count): fingerprints.append(reader.read(8)) # Step 2 sending: DH Exchange new_nonce = os.urandom(32) p, q = Factorizator.factorize(pq) with BinaryWriter() as pq_inner_data_writer: pq_inner_data_writer.write_int( 0x83c95aec, signed=False) # PQ Inner Data pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False)) pq_inner_data_writer.tgwrite_bytes( get_byte_array( min(p, q), signed=False)) pq_inner_data_writer.tgwrite_bytes( get_byte_array( max(p, q), signed=False)) pq_inner_data_writer.write(nonce) pq_inner_data_writer.write(server_nonce) pq_inner_data_writer.write(new_nonce) cipher_text, target_fingerprint = None, None for fingerprint in fingerprints: cipher_text = RSA.encrypt( get_fingerprint_text(fingerprint), pq_inner_data_writer.get_bytes()) if cipher_text is not None: target_fingerprint = fingerprint break if cipher_text is None: raise AssertionError( 'Could not find a valid key for fingerprints: {}' .format(', '.join([get_fingerprint_text(f) for f in fingerprints]))) with BinaryWriter() as req_dh_params_writer: req_dh_params_writer.write_int( 0xd712e4be, signed=False) # Req DH Params req_dh_params_writer.write(nonce) req_dh_params_writer.write(server_nonce) req_dh_params_writer.tgwrite_bytes( get_byte_array( min(p, q), signed=False)) req_dh_params_writer.tgwrite_bytes( get_byte_array( max(p, q), signed=False)) req_dh_params_writer.write(target_fingerprint) req_dh_params_writer.tgwrite_bytes(cipher_text) req_dh_params_bytes = req_dh_params_writer.get_bytes() sender.send(req_dh_params_bytes) # Step 2 response: DH Exchange encrypted_answer = None with BinaryReader(sender.receive()) as reader: response_code = reader.read_int(signed=False) if response_code == 0x79cb045d: raise AssertionError('Server DH params fail: TODO') if response_code != 0xd0e8075c: raise AssertionError('Invalid response code: {}'.format( hex(response_code))) nonce_from_server = reader.read(16) if nonce_from_server != nonce: raise NotImplementedError('Invalid nonce from server') server_nonce_from_server = reader.read(16) if server_nonce_from_server != server_nonce: raise NotImplementedError('Invalid server nonce from server') encrypted_answer = reader.tgread_bytes() # Step 3 sending: Complete DH Exchange key, iv = utils.generate_key_data_from_nonces(server_nonce, new_nonce) plain_text_answer = AES.decrypt_ige(encrypted_answer, key, iv) g, dh_prime, ga, time_offset = None, None, None, None with BinaryReader(plain_text_answer) as dh_inner_data_reader: dh_inner_data_reader.read(20) # hashsum code = dh_inner_data_reader.read_int(signed=False) if code != 0xb5890dba: raise AssertionError('Invalid DH Inner Data code: {}'.format(code)) nonce_from_server1 = dh_inner_data_reader.read(16) if nonce_from_server1 != nonce: raise AssertionError('Invalid nonce in encrypted answer') server_nonce_from_server1 = dh_inner_data_reader.read(16) if server_nonce_from_server1 != server_nonce: raise AssertionError('Invalid server nonce in encrypted answer') g = dh_inner_data_reader.read_int() dh_prime = get_int(dh_inner_data_reader.tgread_bytes(), signed=False) ga = get_int(dh_inner_data_reader.tgread_bytes(), signed=False) server_time = dh_inner_data_reader.read_int() time_offset = server_time - int(time.time()) b = get_int(os.urandom(2048), signed=False) gb = pow(g, b, dh_prime) gab = pow(ga, b, dh_prime) # Prepare client DH Inner Data with BinaryWriter() as client_dh_inner_data_writer: client_dh_inner_data_writer.write_int( 0x6643b654, signed=False) # Client DH Inner Data client_dh_inner_data_writer.write(nonce) client_dh_inner_data_writer.write(server_nonce) client_dh_inner_data_writer.write_long(0) # TODO retry_id client_dh_inner_data_writer.tgwrite_bytes( get_byte_array( gb, signed=False)) with BinaryWriter() as client_dh_inner_data_with_hash_writer: client_dh_inner_data_with_hash_writer.write( utils.sha1(client_dh_inner_data_writer.get_bytes())) client_dh_inner_data_with_hash_writer.write( client_dh_inner_data_writer.get_bytes()) client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes( ) # Encryption client_dh_inner_data_encrypted_bytes = AES.encrypt_ige( client_dh_inner_data_bytes, key, iv) # Prepare Set client DH params with BinaryWriter() as set_client_dh_params_writer: set_client_dh_params_writer.write_int(0xf5045f1f, signed=False) set_client_dh_params_writer.write(nonce) set_client_dh_params_writer.write(server_nonce) set_client_dh_params_writer.tgwrite_bytes( client_dh_inner_data_encrypted_bytes) set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes() sender.send(set_client_dh_params_bytes) # Step 3 response: Complete DH Exchange with BinaryReader(sender.receive()) as reader: code = reader.read_int(signed=False) if code == 0x3bcbf734: # DH Gen OK nonce_from_server = reader.read(16) if nonce_from_server != nonce: raise NotImplementedError('Invalid nonce from server') server_nonce_from_server = reader.read(16) if server_nonce_from_server != server_nonce: raise NotImplementedError('Invalid server nonce from server') new_nonce_hash1 = reader.read(16) auth_key = AuthKey(get_byte_array(gab, signed=False)) new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, 1) if new_nonce_hash1 != new_nonce_hash_calculated: raise AssertionError('Invalid new nonce hash') return auth_key, time_offset elif code == 0x46dc1fb9: # DH Gen Retry raise NotImplementedError('dh_gen_retry') elif code == 0xa69dae02: # DH Gen Fail raise NotImplementedError('dh_gen_fail') else: raise AssertionError('DH Gen unknown: {}'.format(hex(code)))