def test_get_b58_pub_key(self, *_: Any) -> None: for local_testing in [True, False]: self.settings.local_testing_mode = local_testing key = get_b58_key(B58_PUBLIC_KEY, self.settings) self.assertIsInstance(key, bytes) self.assertEqual(len(key), TFC_PUBLIC_KEY_LENGTH) with self.assertRaises(ValueError): get_b58_key(B58_PUBLIC_KEY, self.settings, nick_to_short_address('Alice'))
def decrypt_local_key(ts: 'datetime', packet: bytes, kdk_hashes: List[bytes], packet_hashes: List[bytes], settings: 'Settings', l_queue: 'local_key_queue') -> Tuple['datetime', bytes]: """Decrypt local key packet.""" while True: kdk = get_b58_key(B58_LOCAL_KEY, settings) kdk_hash = blake2b(kdk) # Check if the key was an old one. if kdk_hash in kdk_hashes: m_print("Error: Entered an old local key decryption key.", delay=1) continue try: plaintext = auth_and_decrypt(packet, kdk) except nacl.exceptions.CryptoError: ts, plaintext = process_local_key_buffer(kdk, l_queue) protect_kdk(kdk) # Cache hashes needed to recognize reissued local key packets and key decryption keys. kdk_hashes.append(kdk_hash) packet_hashes.append(blake2b(packet)) return ts, plaintext
def process_local_key(packet: bytes, contact_list: 'ContactList', key_list: 'KeyList') -> None: """Decrypt local key packet, add local contact/keyset.""" try: clear_screen() box_print(["Received encrypted local key"], tail=1) kdk = get_b58_key('localkey') try: pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True) except nacl.exceptions.CryptoError: raise FunctionReturn("Invalid key decryption key.", delay=1.5) key = pt[0:32] hek = pt[32:64] conf_code = pt[64:65] # Add local contact to contact list database contact_list.add_contact('local', 'local', 'local', bytes(32), bytes(32), False, False, True) # Add local contact to keyset database key_list.add_keyset('local', key, bytes(32), hek, bytes(32)) box_print([f"Confirmation code for TxM: {conf_code.hex()}"], head=1) except KeyboardInterrupt: raise FunctionReturn("Local key setup aborted.", delay=1)
def test_get_b58_key(self): for kt in ['pubkey', 'localkey', 'imported_file']: input_list = [ 'bad', "2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZa", "2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy" ] gen = iter(input_list) def mock_input(_): return str(next(gen)) builtins.input = mock_input key = get_b58_key(kt) self.assertIsInstance(key, bytes) self.assertEqual(len(key), 32) with self.assertRaises(SystemExit): get_b58_key('invalid_keytype')
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_key() try: while True: clear_screen() box_print("Received encrypted local key", tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) try: pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True) break except nacl.exceptions.CryptoError: if bootstrap: raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1.5) c_print("Incorrect key decryption key.", head=1) clear_screen(delay=1.5) key = pt[0:32] hek = pt[32:64] conf_code = pt[64:65] # Add local contact to contact list database contact_list.add_contact(LOCAL_ID, LOCAL_ID, LOCAL_ID, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), False, False, True) # Add local keyset to keyset database key_list.add_keyset(rx_account=LOCAL_ID, tx_key=key, rx_key=csprng(), tx_hek=hek, rx_hek=csprng()) box_print(f"Confirmation code for TxM: {conf_code.hex()}", head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Added new local key.") if bootstrap: window_list.active_win = local_win except KeyboardInterrupt: raise FunctionReturn("Local key setup aborted.", delay=1, head=3, tail_clear=True)
def process_imported_file(ts: 'datetime', packet: bytes, window_list: 'WindowList', settings: 'Settings'): """Decrypt and store imported file.""" while True: try: print('') key = get_b58_key(B58_FILE_KEY, settings) except KeyboardInterrupt: raise FunctionReturn("File import aborted.", head=2) try: phase("Decrypting file", head=1) file_pt = auth_and_decrypt(packet[1:], key, soft_e=True) phase(DONE) break except (nacl.exceptions.CryptoError, nacl.exceptions.ValueError): phase('ERROR', done=True) c_print("Invalid decryption key. Try again.") print_on_previous_line(reps=7, delay=1.5) except KeyboardInterrupt: phase('ABORT', done=True) raise FunctionReturn("File import aborted.") try: phase("Decompressing file") file_dc = zlib.decompress(file_pt) phase(DONE) except zlib.error: phase('ERROR', done=True) raise FunctionReturn("Error: Decompression of file data failed.") try: f_name = bytes_to_str(file_dc[:PADDED_UTF32_STR_LEN]) except UnicodeError: raise FunctionReturn("Error: Received file name had invalid encoding.") if not f_name.isprintable() or not f_name: raise FunctionReturn("Error: Received file had an invalid name.") f_data = file_dc[PADDED_UTF32_STR_LEN:] final_name = store_unique(f_data, DIR_IMPORTED, f_name) message = f"Stored imported file as '{final_name}'" box_print(message, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, message)
def exchange_public_keys( onion_pub_key: bytes, tfc_public_key_user: bytes, kdk_hash: bytes, contact: 'Contact', settings: 'Settings', queues: 'QueueDict', ) -> bytes: """Exchange public keys with contact. This function outputs the user's public key and waits for user to enter the public key of the contact. If the User presses <Enter>, the function will resend the users' public key to contact. """ public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) while True: try: tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings, contact.short_address) except ValueError as invalid_pub_key: invalid_key = str(invalid_pub_key).encode() # Do not send packet to Relay Program if the user has for some reason # managed to embed the local key decryption key inside the public key. substrings = split_to_substrings(invalid_key, ENCODED_B58_KDK_LENGTH) safe_string = not any( blake2b(substring) == kdk_hash for substring in substrings) if safe_string: public_key_packet = (UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_PUBKEY_CHECK + onion_pub_key + invalid_key) queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) continue if tfc_public_key_contact == b'': public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) continue return tfc_public_key_contact
def process_imported_file(ts: 'datetime', packet: bytes, window_list: 'WindowList'): """Decrypt and store imported file.""" while True: try: print('') key = get_b58_key('imported_file') phase("Decrypting file", head=1) file_pt = auth_and_decrypt(packet[1:], key, soft_e=True) phase("Done") break except nacl.exceptions.CryptoError: c_print("Invalid decryption key. Try again.", head=2) print_on_previous_line(reps=6, delay=1.5) except KeyboardInterrupt: raise FunctionReturn("File import aborted.") try: phase("Decompressing file") file_dc = zlib.decompress(file_pt) phase("Done") except zlib.error: raise FunctionReturn("Decompression of file data failed.") try: f_name = bytes_to_str(file_dc[:1024]) except UnicodeError: raise FunctionReturn("Received file had an invalid name.") if not f_name.isprintable(): raise FunctionReturn("Received file had an invalid name.") f_data = file_dc[1024:] final_name = store_unique(f_data, DIR_IMPORTED, f_name) message = "Stored imported file to {}/{}".format(DIR_IMPORTED, final_name) box_print(message, head=1) local_win = window_list.get_local_window() local_win.print_new(ts, message, print_=False)
def test_get_b58_key(self, *_): for boolean in [True, False]: self.settings.local_testing_mode = boolean key = get_b58_key(B58_LOCAL_KEY, self.settings) self.assertIsInstance(key, bytes) self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH) with self.assertRaises(SystemExit): get_b58_key('invalid_key_type', self.settings) for boolean in [True, False]: self.settings.local_testing_mode = boolean key = get_b58_key(B58_PUBLIC_KEY, self.settings, nick_to_short_address('Alice')) self.assertIsInstance(key, bytes) self.assertEqual(len(key), TFC_PUBLIC_KEY_LENGTH) with self.assertRaises(SystemExit): get_b58_key('invalid_key_type', self.settings)
def test_get_b58_key(self): for boolean in [True, False]: self.settings.local_testing_mode = boolean for key_type in [B58_PUB_KEY, B58_LOCAL_KEY]: input_list = [ "bad", "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTa", "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" ] gen = iter(input_list) builtins.input = lambda _: str(next(gen)) key = get_b58_key(key_type, self.settings) self.assertIsInstance(key, bytes) self.assertEqual(len(key), KEY_LENGTH) with self.assertRaises(SystemExit): get_b58_key('invalid_keytype', self.settings) for boolean in [True, False]: self.settings.local_testing_mode = boolean for key_type in [B58_FILE_KEY]: input_list = [ "bad", "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwi1C2Ga", "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwi1C2GD" ] gen = iter(input_list) builtins.input = lambda _: str(next(gen)) key = get_b58_key(key_type, self.settings) self.assertIsInstance(key, bytes) self.assertEqual(len(key), KEY_LENGTH) with self.assertRaises(SystemExit): get_b58_key('invalid_keytype', self.settings)
def start_key_exchange(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Start X25519 key exchange with recipient. Variable naming: tx = user's key rx = contact's key sk = private (secret) key pk = public key key = message key hek = header key dh_ssk = X25519 shared secret :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Contact's nickname :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :return: None """ try: tx_sk = nacl.public.PrivateKey(csprng()) tx_pk = bytes(tx_sk.public_key) while True: queue_to_nh(PUBLIC_KEY_PACKET_HEADER + tx_pk + user.encode() + US_BYTE + account.encode(), settings, queues[NH_PACKET_QUEUE]) rx_pk = get_b58_key(B58_PUB_KEY, settings) if rx_pk != RESEND.encode(): break if rx_pk == bytes(KEY_LENGTH): # Public key is zero with negligible probability, therefore we # assume such key is malicious and attempts to either result in # zero shared key (pointless considering implementation), or to # DoS the key exchange as libsodium does not accept zero keys. box_print(["Warning!", "Received a malicious public key from network.", "Aborting key exchange for your safety."], tail=1) raise FunctionReturn("Error: Zero public key", output=False) dh_box = nacl.public.Box(tx_sk, nacl.public.PublicKey(rx_pk)) dh_ssk = dh_box.shared_key() # Domain separate each key with key-type specific context variable # and with public keys that both clients know which way to place. tx_key = hash_chain(dh_ssk + rx_pk + b'message_key') rx_key = hash_chain(dh_ssk + tx_pk + b'message_key') tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key') rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key') # Domain separate fingerprints of public keys by using the shared # secret as salt. This way entities who might monitor fingerprint # verification channel are unable to correlate spoken values with # public keys that transit through a compromised IM server. This # protects against de-anonymization of IM accounts in cases where # clients connect to the compromised server via Tor. The preimage # resistance of hash chain protects the shared secret from leaking. tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint') rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint') if not verify_fingerprints(tx_fp, rx_fp): box_print(["Warning!", "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety."], tail=1) raise FunctionReturn("Error: Fingerprint mismatch", output=False) packet = KEY_EX_X25519_HEADER \ + tx_key + tx_hek \ + rx_key + rx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, tx_fp, rx_fp, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) # Use random values as Rx-keys to prevent decryption if they're accidentally used. queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, account, tx_key, csprng(), tx_hek, csprng())) box_print(f"Successfully added {nick}.") clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("Key exchange aborted.", delay=1, head=2, tail_clear=True)
def test_invalid_key_type_raises_critical_error(self) -> None: with self.assertRaises(SystemExit): get_b58_key('invalid_key_type', self.settings)
def test_empty_pub_key_returns_empty_bytes(self, *_): key = get_b58_key(B58_PUBLIC_KEY, self.settings) self.assertEqual(key, b'')
def start_key_exchange(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue'], gateway: 'Gateway') -> None: """Start X25519 key exchange with recipient. Variable naming: tx = user's key rx = contact's key sk = private (secret) key pk = public key key = message key hek = header key dh_ssk = DH shared secret :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Contact's nickname :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :param gateway: Gateway object :return: None """ try: tx_sk = nacl.public.PrivateKey.generate() tx_pk = bytes(tx_sk.public_key) transmit( PUBLIC_KEY_PACKET_HEADER + tx_pk + user.encode() + US_BYTE + account.encode(), settings, gateway) rx_pk = nacl.public.PublicKey(get_b58_key('pubkey')) dh_box = nacl.public.Box(tx_sk, rx_pk) dh_ssk = dh_box.shared_key() rx_pk = bytes(rx_pk) # Domain separate each key with key-type specific byte-string and # with public keys that both clients know which way to place. tx_key = hash_chain(dh_ssk + rx_pk + b'message_key') rx_key = hash_chain(dh_ssk + tx_pk + b'message_key') tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key') rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key') # Domain separate fingerprints of public keys by using the shared # secret as salt. This way entities who might monitor fingerprint # verification channel are unable to correlate spoken values with # public keys that transit through a compromised IM server. This # protects against deanonymization of IM accounts in cases where # clients connect to the compromised server via Tor. tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint') rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint') if not verify_fingerprints(tx_fp, rx_fp): box_print([ "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety." ], tail=1) raise FunctionReturn("Fingerprint mismatch", output=False, delay=2.5) packet = KEY_EX_ECDHE_HEADER \ + tx_key + tx_hek \ + rx_key + rx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, tx_fp, rx_fp, settings.log_msg_by_default, settings.store_file_default, settings.n_m_notify_privacy) # Null-bytes below are fillers for Rx-keys not used by TxM. queues[KEY_MANAGEMENT_QUEUE].put( ('ADD', account, tx_key, bytes(32), tx_hek, bytes(32))) box_print([f"Successfully added {nick}."]) clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("Key exchange aborted.", delay=1)
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', kdk_hashes: List[bytes], packet_hashes: List[bytes], l_queue: 'Queue') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_keyset() plaintext = None try: packet_hash = blake2b(packet) # Check if the packet is an old one if packet_hash in packet_hashes: raise FunctionReturn("Error: Received old local key packet.", output=False) while True: m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) kdk_hash = blake2b(kdk) try: plaintext = auth_and_decrypt(packet, kdk) break except nacl.exceptions.CryptoError: # Check if key was an old one if kdk_hash in kdk_hashes: m_print("Error: Entered an old local key decryption key.", delay=1) continue # Check if the kdk was for a packet further ahead in the queue buffer = [] # type: List[Tuple[datetime, bytes]] while l_queue.qsize() > 0: tup = l_queue.get() # type: Tuple[datetime, bytes] if tup not in buffer: buffer.append(tup) for i, tup in enumerate(buffer): try: plaintext = auth_and_decrypt(tup[1], kdk) # If we reach this point, decryption was successful. for unexamined in buffer[i + 1:]: l_queue.put(unexamined) buffer = [] ts = tup[0] break except nacl.exceptions.CryptoError: continue else: # Finished the buffer without finding local key CT # for the kdk. Maybe the kdk is from another session. raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1) break # This catches PyCharm's weird claim that plaintext might be referenced before assignment if plaintext is None: # pragma: no cover raise FunctionReturn("Error: Could not decrypt local key.") # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, KEX_STATUS_LOCAL_KEY, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), False, False, True) tx_mk, tx_hk, c_code = separate_headers(plaintext, 2 * [SYMMETRIC_KEY_LENGTH]) # Add local keyset to keyset database key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY, tx_mk=tx_mk, rx_mk=csprng(), tx_hk=tx_hk, rx_hk=csprng()) # Cache hashes needed to recognize reissued local key packets and key decryption keys. packet_hashes.append(packet_hash) kdk_hashes.append(kdk_hash) # Prevent leak of KDK via terminal history / clipboard readline.clear_history() os.system(RESET) root = tkinter.Tk() root.withdraw() try: if root.clipboard_get() == b58encode(kdk): root.clipboard_clear() except tkinter.TclError: pass root.destroy() m_print([ "Local key successfully installed.", f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Added new local key.") if bootstrap: window_list.active_win = local_win except (EOFError, KeyboardInterrupt): m_print("Local key setup aborted.", bold=True, tail_clear=True, delay=1, head=2) if window_list.active_win is not None and not bootstrap: window_list.active_win.redraw() raise FunctionReturn("Local key setup aborted.", output=False)
def start_key_exchange( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Contact's nickname contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Start X448 key exchange with the recipient. This function first creates the X448 key pair. It then outputs the public key to Relay Program on Networked Computer, that passes the public key to contact's Relay Program. When contact's public key reaches the user's Relay Program, the user will manually copy the key into their Transmitter Program. The X448 shared secret is used to create unidirectional message and header keys, that will be used in forward secret communication. This is followed by the fingerprint verification where the user manually authenticates the public key. Once the fingerprint has been accepted, this function will add the contact/key data to contact/key databases, and export that data to the Receiver Program on Destination Computer. The transmission is encrypted with the local key. --- TFC provides proactive security by making fingerprint verification part of the key exchange. This prevents the situation where the users don't know about the feature, and thus helps minimize the risk of MITM attack. The fingerprints can be skipped by pressing Ctrl+C. This feature is not advertised however, because verifying fingerprints the only strong way to be sure TFC is not under MITM attack. When verification is skipped, TFC marks the contact's X448 keys as "Unverified". The fingerprints can later be verified with the `/verify` command: answering `yes` to the question on whether the fingerprints match, marks the X448 keys as "Verified". Variable naming: tx = user's key rx = contact's key fp = fingerprint mk = message key hk = header key """ if not contact_list.has_pub_key(onion_pub_key): contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) contact = contact_list.get_contact_by_pub_key(onion_pub_key) # Generate new private key or load cached private key if contact.tfc_private_key is None: tfc_private_key_user = X448.generate_private_key() else: tfc_private_key_user = contact.tfc_private_key try: tfc_public_key_user = X448.derive_public_key(tfc_private_key_user) # Import public key of contact while True: public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings, contact.short_address) if tfc_public_key_contact != b'': break # Validate public key of contact if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH: m_print([ "Warning!", "Received invalid size public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Invalid public key length", output=False) if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH): # The public key of contact is zero with negligible probability, # therefore we assume such key is malicious and attempts to set # the shared key to zero. m_print([ "Warning!", "Received a malicious zero-public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Zero public key", output=False) # Derive shared key dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact) # Domain separate unidirectional keys from shared key by using public # keys as message and the context variable as personalization string. tx_mk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_mk = blake2b(tfc_public_key_user, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) tx_hk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_hk = blake2b(tfc_public_key_user, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) # Domain separate fingerprints of public keys by using the # shared secret as key and the context variable as # personalization string. This way entities who might monitor # fingerprint verification channel are unable to correlate # spoken values with public keys that they might see on RAM or # screen of Networked Computer: Public keys can not be derived # from the fingerprints due to preimage resistance of BLAKE2b, # and fingerprints can not be derived from public key without # the X448 shared secret. Using the context variable ensures # fingerprints are distinct from derived message and header keys. tx_fp = blake2b(tfc_public_key_user, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) rx_fp = blake2b(tfc_public_key_contact, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) # Verify fingerprints try: if not verify_fingerprints(tx_fp, rx_fp): m_print([ "Warning!", "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Fingerprint mismatch", delay=2.5, output=False) kex_status = KEX_STATUS_VERIFIED except (EOFError, KeyboardInterrupt): m_print([ "Skipping fingerprint verification.", '', "Warning!", "Man-in-the-middle attacks can not be detected", "unless fingerprints are verified! To re-verify", "the contact, use the command '/verify'.", '', "Press <enter> to continue." ], manual_proceed=True, box=True, head=2) kex_status = KEX_STATUS_UNVERIFIED # Send keys to the Receiver Program c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (KEY_EX_ECDHE + onion_pub_key + tx_mk + rx_mk + tx_hk + rx_hk + str_to_bytes(nick)) queue_command(command, settings, queues) while True: purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): break elif purp_code == '': phase("Resending contact data", head=2) queue_command(command, settings, queues) phase(DONE) print_on_previous_line(reps=5) else: m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2) # Store contact data into databases contact.tfc_private_key = None contact.tx_fingerprint = tx_fp contact.rx_fingerprint = rx_fp contact.kex_status = kex_status contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): contact.tfc_private_key = tfc_private_key_user raise FunctionReturn("Key exchange interrupted.", tail_clear=True, delay=1, head=2)
def test_get_b58_local_key(self, *_: Any) -> None: key = get_b58_key(B58_LOCAL_KEY, self.settings) self.assertIsInstance(key, bytes) self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH)