def wipe(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Reset terminals, wipe all user data from TxM/RxM/NH and power off systems. No effective RAM overwriting tool currently exists, so as long as TxM/RxM use FDE and DDR3 memory, recovery of user data becomes impossible very fast: https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf """ if not yes("Wipe all user data and power off systems?"): raise FunctionReturn("Wipe command aborted.") clear_screen() for q in [COMMAND_PACKET_QUEUE, NH_PACKET_QUEUE]: while queues[q].qsize() != 0: queues[q].get() queue_command(WIPE_USER_DATA_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) if not settings.session_traffic_masking: if settings.local_testing_mode: time.sleep(0.8) if settings.data_diode_sockets: time.sleep(2.2) else: time.sleep(settings.race_condition_delay) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND, settings, queues[NH_PACKET_QUEUE]) os.system('reset')
def change_setting(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Change setting on TxM / RxM.""" try: setting = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No setting specified.") if setting not in settings.key_list: raise FunctionReturn(f"Error: Invalid setting '{setting}'") try: value = user_input.plaintext.split()[2] except IndexError: raise FunctionReturn("Error: No value for setting specified.") pt_cmd = dict(serial_error_correction=UNENCRYPTED_EC_RATIO, serial_baudrate=UNENCRYPTED_BAUDRATE, disable_gui_dialog=UNENCRYPTED_GUI_DIALOG) if setting in pt_cmd: if settings.session_traffic_masking: raise FunctionReturn( "Error: Can't change this setting during traffic masking.") settings.change_setting(setting, value, contact_list, group_list) command = CHANGE_SETTING_HEADER + setting.encode( ) + US_BYTE + value.encode() queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) if setting in pt_cmd: packet = UNENCRYPTED_PACKET_HEADER + pt_cmd[setting] + value.encode() queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE])
def clear_screens(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Clear/reset TxM, RxM and NH screens. Only send unencrypted command to NH if traffic masking is disabled and if some related IM account can be bound to active window. Since reset command removes ephemeral message log on RxM, TxM decides the window to reset (in case e.g. previous window selection command packet dropped and active window state is inconsistent between TxM/RxM). """ cmd = user_input.plaintext.split()[0] command = CLEAR_SCREEN_HEADER if cmd == CLEAR else RESET_SCREEN_HEADER + window.uid.encode( ) queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) clear_screen() if not settings.session_traffic_masking and window.imc_name is not None: im_window = window.imc_name.encode() pt_cmd = UNENCRYPTED_SCREEN_CLEAR if cmd == CLEAR else UNENCRYPTED_SCREEN_RESET packet = UNENCRYPTED_PACKET_HEADER + pt_cmd + im_window queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE]) if cmd == RESET: os.system('reset')
def import_file(settings: 'Settings', nh_queue: 'Queue') -> None: """\ Send unencrypted command to NH that tells it to open RxM upload prompt for received (exported) file. """ if settings.session_traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.") queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_IMPORT_COMMAND, settings, nh_queue)
def export_file(settings: 'Settings', nh_queue: 'Queue') -> None: """Encrypt and export file to NH. This is a faster method to send large files. It is used together with file import (/fi) command that uploads ciphertext to RxM for RxM-side decryption. Key is generated automatically so that bad passwords selected by users do not affect security of ciphertexts. """ if settings.session_traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.") path = ask_path_gui("Select file to export...", settings, get_file=True) name = path.split('/')[-1] data = bytearray() data.extend(str_to_bytes(name)) if not os.path.isfile(path): raise FunctionReturn("Error: File not found.") if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty.") phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase(DONE) phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL)) phase(DONE) phase("Encrypting data") file_key = csprng() file_ct = encrypt_and_sign(comp, key=file_key) phase(DONE) phase("Exporting data") queue_to_nh(EXPORTED_FILE_HEADER + file_ct, settings, nh_queue) phase(DONE) print_key(f"Decryption key for file '{name}':", file_key, settings, no_split=True, file_key=True)
def exit_tfc(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Exit TFC on TxM/RxM/NH.""" for q in [COMMAND_PACKET_QUEUE, NH_PACKET_QUEUE]: while queues[q].qsize() != 0: queues[q].get() queue_command(EXIT_PROGRAM_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) if not settings.session_traffic_masking: if settings.local_testing_mode: time.sleep(0.8) if settings.data_diode_sockets: time.sleep(2.2) else: time.sleep(settings.race_condition_delay) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND, settings, queues[NH_PACKET_QUEUE])
def queue_delayer(): time.sleep(0.1) queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE]) time.sleep(0.1) queue_to_nh( PUBLIC_KEY_PACKET_HEADER + KEY_LENGTH * b'a' + b'*****@*****.**' + US_BYTE + b'*****@*****.**', settings, queues[NH_PACKET_QUEUE]) time.sleep(0.1) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND, settings, queues[NH_PACKET_QUEUE]) time.sleep(0.1) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND, settings, queues[NH_PACKET_QUEUE]) time.sleep(0.1) queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, LOCAL_ID, KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a')) time.sleep(0.1) queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) time.sleep(0.1) queue_message(user_input, window, settings, queues[FILE_PACKET_QUEUE]) time.sleep(0.1) queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, '*****@*****.**', KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a')) time.sleep(0.1) queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) time.sleep(0.1) queue_message(user_input, window, settings, queues[FILE_PACKET_QUEUE]) time.sleep(0.1) queues[UNITTEST_QUEUE].put(EXIT)
def test_loop(self): # Setup queues = { MESSAGE_PACKET_QUEUE: Queue(), FILE_PACKET_QUEUE: Queue(), COMMAND_PACKET_QUEUE: Queue(), NH_PACKET_QUEUE: Queue(), LOG_PACKET_QUEUE: Queue(), NOISE_PACKET_QUEUE: Queue(), NOISE_COMMAND_QUEUE: Queue(), KEY_MANAGEMENT_QUEUE: Queue(), WINDOW_SELECT_QUEUE: Queue(), EXIT_QUEUE: Queue() } settings = Settings(session_traffic_masking=True) gateway = Gateway() key_list = KeyList(nicks=['Alice', LOCAL_ID]) window = TxWindow(log_messages=True) contact_list = ContactList(nicks=['Alice', LOCAL_ID]) window.contact_list = contact_list window.window_contacts = [contact_list.get_contact('Alice')] user_input = UserInput(plaintext='test') queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE]) queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE]) queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE], window) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND, settings, queues[NH_PACKET_QUEUE]) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND, settings, queues[NH_PACKET_QUEUE]) def queue_delayer(): time.sleep(0.1) queues[WINDOW_SELECT_QUEUE].put((window, True)) # Test threading.Thread(target=queue_delayer).start() self.assertIsNone( sender_loop(queues, settings, gateway, key_list, unittest=True)) threading.Thread(target=queue_delayer).start() self.assertIsNone( sender_loop(queues, settings, gateway, key_list, unittest=True)) threading.Thread(target=queue_delayer).start() self.assertIsNone( sender_loop(queues, settings, gateway, key_list, unittest=True)) self.assertEqual(len(gateway.packets), 8) self.assertEqual(queues[EXIT_QUEUE].qsize(), 2) # Teardown for key in queues: while not queues[key].empty(): queues[key].get() time.sleep(0.1) queues[key].close()
def new_local_key(contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Run Tx-side local key exchange protocol. Local key encrypts commands and data sent from TxM to RxM. The key is delivered to RxM in packet encrypted with an ephemeral symmetric key. The checksummed Base58 format key decryption key is typed on RxM manually. This prevents local key leak in following scenarios: 1. CT is intercepted by adversary on compromised NH but no visual eavesdropping takes place. 2. CT is not intercepted by adversary on NH but visual eavesdropping records decryption key. 3. CT is delivered from TxM to RxM (compromised NH is bypassed) and visual eavesdropping records decryption key. Once correct key decryption key is entered on RxM, Receiver program will display the 1-byte confirmation code generated by Transmitter program. The code will be entered on TxM to confirm user has successfully delivered the key decryption key. The protocol is completed with Transmitter program sending an ACK message to Receiver program, that then moves to wait for public keys from contact. """ try: if settings.session_traffic_masking and contact_list.has_local_contact: raise FunctionReturn("Error: Command is disabled during traffic masking.") clear_screen() c_print("Local key setup", head=1, tail=1) c_code = os.urandom(1) key = csprng() hek = csprng() kek = csprng() packet = LOCAL_KEY_PACKET_HEADER + encrypt_and_sign(key + hek + c_code, key=kek) nh_bypass_msg(NH_BYPASS_START, settings) queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE]) while True: print_key("Local key decryption key (to RxM)", kek, settings) purp_code = ask_confirmation_code() if purp_code == c_code.hex(): break elif purp_code == RESEND: phase("Resending local key", head=2) queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE]) phase(DONE) print_on_previous_line(reps=(9 if settings.local_testing_mode else 10)) else: box_print(["Incorrect confirmation code. If RxM did not receive", "encrypted local key, resend it by typing 'resend'."], head=1) print_on_previous_line(reps=(11 if settings.local_testing_mode else 12), delay=2) nh_bypass_msg(NH_BYPASS_STOP, settings) # 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, False) # Add local contact to keyset database queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, LOCAL_ID, key, csprng(), hek, csprng())) # Notify RxM that confirmation code was successfully entered queue_command(LOCAL_KEY_INSTALLED_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) box_print("Successfully added a new local key.") clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("Local key setup aborted.", delay=1, head=3, tail_clear=True)
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)