def rxp_show_sys_win( user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: 'QueueDict', ) -> None: """\ Display a system window on Receiver Program until the user presses Enter. Receiver Program has a dedicated window, WIN_UID_LOCAL, for system messages that shows information about received commands, status messages etc. Receiver Program also has another window, WIN_UID_FILE, that shows progress of file transmission from contacts that have traffic masking enabled. """ cmd = user_input.plaintext.split()[0] win_uid = dict(cmd=WIN_UID_COMMAND, fw=WIN_UID_FILE)[cmd] command = WIN_SELECT + win_uid queue_command(command, settings, queues) try: m_print(f"<Enter> returns Receiver to {window.name}'s window", manual_proceed=True, box=True) except (EOFError, KeyboardInterrupt): pass print_on_previous_line(reps=4, flush=True) command = WIN_SELECT + window.uid queue_command(command, settings, queues)
def clear_screens(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: 'QueueDict') -> None: """Clear/reset screen of Source, Destination, and Networked Computer. Only send an unencrypted command to Networked Computer if traffic masking is disabled. With clear command, sending only the command header is enough. However, as reset command removes the ephemeral message log on Receiver Program, Transmitter Program must define the window to reset (in case, e.g., previous window selection command packet dropped, and active window state is inconsistent between the TCB programs). """ clear = user_input.plaintext.split()[0] == CLEAR command = CLEAR_SCREEN if clear else RESET_SCREEN + window.uid queue_command(command, settings, queues) clear_screen() if not settings.traffic_masking: pt_cmd = UNENCRYPTED_SCREEN_CLEAR if clear else UNENCRYPTED_SCREEN_RESET packet = UNENCRYPTED_DATAGRAM_HEADER + pt_cmd queue_to_nc(packet, queues[RELAY_PACKET_QUEUE]) if not clear: readline.clear_history() reset_terminal()
def rxp_load_psk(window: 'TxWindow', contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict', ) -> None: """Send command to Receiver Program to load PSK for active contact.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) if window.type == WIN_TYPE_GROUP or window.contact is None: raise SoftError("Error: Group is selected.", head_clear=True) if not contact_list.get_contact_by_pub_key(window.uid).uses_psk(): raise SoftError(f"Error: The current key was exchanged with {ECDHE}.", head_clear=True) c_code = blake2b(window.uid, digest_size=CONFIRM_CODE_LENGTH) command = KEY_EX_PSK_RX + c_code + window.uid queue_command(command, settings, queues) while True: try: purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): window.contact.kex_status = KEX_STATUS_HAS_RX_PSK contact_list.store_contacts() raise SoftError(f"Removed PSK reminder for {window.name}.", tail_clear=True, delay=1) m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2) except (EOFError, KeyboardInterrupt): raise SoftError("PSK install verification aborted.", tail_clear=True, delay=1, head=2)
def change_nick(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict') -> None: """Change nick of contact.""" try: nick = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No nick specified.", head_clear=True) if window.type == WIN_TYPE_GROUP: group_rename(nick, window, contact_list, group_list, settings, queues) if window.contact is None: raise SoftError("Error: Window does not have contact.") onion_pub_key = window.contact.onion_pub_key error_msg = validate_nick(nick, (contact_list, group_list, onion_pub_key)) if error_msg: raise SoftError(error_msg, head_clear=True) window.contact.nick = nick window.name = nick contact_list.store_contacts() command = CH_NICKNAME + onion_pub_key + nick.encode() queue_command(command, settings, queues)
def exit_tfc(settings: 'Settings', queues: 'QueueDict', gateway: 'Gateway') -> None: """Exit TFC on all three computers. To exit TFC as fast as possible, this function starts by clearing all command queues before sending the exit command to Receiver Program. It then sends an unencrypted exit command to Relay Program on Networked Computer. As the `sender_loop` process loads the unencrypted exit command from queue, it detects the user's intention, and after outputting the packet, sends the EXIT signal to Transmitter Program's main() method that's running the `monitor_processes` loop. Upon receiving the EXIT signal, `monitor_processes` kills all Transmitter Program's processes and exits the program. During local testing, this function adds some delays to prevent TFC programs from dying when sockets disconnect. """ for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]: while queues[q].qsize() > 0: queues[q].get() queue_command(EXIT_PROGRAM, settings, queues) if not settings.traffic_masking: if settings.local_testing_mode: time.sleep(LOCAL_TESTING_PACKET_DELAY) time.sleep(gateway.settings.data_diode_sockets * 1.5) else: time.sleep(gateway.settings.race_condition_delay) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_EXIT_COMMAND queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
def rename_group( new_name: str, window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', ) -> None: """Rename the active group.""" if window.type == WIN_TYPE_CONTACT or window.group is None: raise FunctionReturn("Error: Selected window is not a group window.", head_clear=True) error_msg = validate_group_name(new_name, contact_list, group_list) if error_msg: raise FunctionReturn(error_msg, head_clear=True) command = GROUP_RENAME + window.uid + new_name.encode() queue_command(command, settings, queues) old_name = window.group.name window.group.name = new_name group_list.store_groups() raise FunctionReturn(f"Renamed group '{old_name}' to '{new_name}'.", delay=1, tail_clear=True)
def select_tx_window( self, settings: 'Settings', # Settings object queues: 'QueueDict', # Dictionary of Queues onion_service: 'OnionService', # OnionService object gateway: 'Gateway', # Gateway object selection: Optional[str] = None, # Selector for window cmd: bool = False # True when `/msg` command is used to switch window ) -> None: """Select specified window or ask the user to specify one.""" if selection is None: selection = self.select_recipient() if selection in self.group_list.get_list_of_group_names(): self.select_group(selection, cmd, settings) elif selection in self.contact_list.contact_selectors(): self.select_contact(selection, cmd, queues, settings) elif selection.startswith("/"): self.window_selection_command(selection, settings, queues, onion_service, gateway) else: raise SoftError("Error: No contact/group was found.") if settings.traffic_masking: queues[WINDOW_SELECT_QUEUE].put(self.window_contacts) packet = WIN_SELECT + self.uid queue_command(packet, settings, queues) clear_screen()
def deliver_contact_data( header: bytes, # Key type (x448, PSK) nick: str, # Contact's nickname onion_pub_key: bytes, # Public key of contact's v3 Onion Service tx_mk: bytes, # Message key for outgoing messages rx_mk: bytes, # Message key for incoming messages tx_hk: bytes, # Header key for outgoing messages rx_hk: bytes, # Header key for incoming messages queues: 'QueueDict', # Dictionary of multiprocessing queues settings: 'Settings', # Settings object ) -> None: """Deliver contact data to Destination Computer.""" c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (header + 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)
def group_add_member(group_name: str, purp_members: List['bytes'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', _: Optional[bytes] = None) -> None: """Add new member(s) to a specified group.""" if group_name not in group_list.get_list_of_group_names(): if not yes(f"Group {group_name} was not found. Create new group?", abort=False, head=1): raise SoftError("Group creation aborted.", head=0, delay=1, tail_clear=True) group_create(group_name, purp_members, contact_list, group_list, settings, queues, master_key) return None purp_pub_keys = set(purp_members) pub_keys = set(contact_list.get_list_of_pub_keys()) before_adding = set( group_list.get_group(group_name).get_list_of_member_pub_keys()) ok_pub_keys_set = set(pub_keys & purp_pub_keys) new_in_group_set = set(ok_pub_keys_set - before_adding) end_assembly = list(before_adding | new_in_group_set) rejected = list(purp_pub_keys - pub_keys) already_in_g = list(before_adding & purp_pub_keys) new_in_group = list(new_in_group_set) ok_pub_keys = list(ok_pub_keys_set) if len(end_assembly) > settings.max_number_of_group_members: raise SoftError( f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.", head_clear=True) group = group_list.get_group(group_name) group.add_members( [contact_list.get_contact_by_pub_key(k) for k in new_in_group]) command = GROUP_ADD + group.group_id + b''.join(ok_pub_keys) queue_command(command, settings, queues) group_management_print(ADDED_MEMBERS, new_in_group, contact_list, group_name) group_management_print(ALREADY_MEMBER, already_in_g, contact_list, group_name) group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name) if new_in_group: if yes("Publish the list of new members to involved?", abort=False): add_packet = (GROUP_MSG_MEMBER_ADD_HEADER + group.group_id + int_to_bytes(len(before_adding)) + b''.join(before_adding) + b''.join(new_in_group)) queue_to_nc(add_packet, queues[RELAY_PACKET_QUEUE])
def log_command(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Display message logs or export them to plaintext file on TCBs. Transmitter Program processes sent, Receiver Program sent and received, messages of all participants in the active window. Having the capability to export the log file from the encrypted database is a bad idea, but as it's required by the GDPR (https://gdpr-info.eu/art-20-gdpr/), it should be done as securely as possible. Therefore, before allowing export, TFC will ask for the master password to ensure no unauthorized user who gains momentary access to the system can the export logs from the database. """ cmd = user_input.plaintext.split()[0] export, header = dict(export=(True, LOG_EXPORT), history=(False, LOG_DISPLAY))[cmd] try: msg_to_load = int(user_input.plaintext.split()[1]) except ValueError: raise SoftError("Error: Invalid number of messages.", head_clear=True) except IndexError: msg_to_load = 0 try: command = header + int_to_bytes(msg_to_load) + window.uid except struct.error: raise SoftError("Error: Invalid number of messages.", head_clear=True) if export and not yes(f"Export logs for '{window.name}' in plaintext?", abort=False): raise SoftError("Log file export aborted.", tail_clear=True, head=0, delay=1) authenticated = master_key.authenticate_action( ) if settings.ask_password_for_log_access else True if authenticated: queue_command(command, settings, queues) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export=export) if export: raise SoftError( f"Exported log file of {window.type} '{window.name}'.", head_clear=True)
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', onion_service: 'OnionService') -> None: """Change the master key on Transmitter/Receiver Program.""" try: if settings.traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) try: device = user_input.plaintext.split()[1].lower() except IndexError: raise FunctionReturn( f"Error: No target-system ('{TX}' or '{RX}') specified.", head_clear=True) if device not in [TX, RX]: raise FunctionReturn(f"Error: Invalid target system '{device}'.", head_clear=True) if device == RX: queue_command(CH_MASTER_KEY, settings, queues) return None old_master_key = master_key.master_key[:] new_master_key = master_key.master_key = master_key.new_master_key() phase("Re-encrypting databases") queues[KEY_MANAGEMENT_QUEUE].put( (KDB_CHANGE_MASTER_KEY_HEADER, master_key)) ensure_dir(DIR_USER_DATA) if os.path.isfile( f'{DIR_USER_DATA}{settings.software_operation}_logs'): change_log_db_key(old_master_key, new_master_key, settings) contact_list.store_contacts() group_list.store_groups() settings.store_settings() onion_service.store_onion_service_private_key() phase(DONE) m_print("Master key successfully changed.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): raise FunctionReturn("Password change aborted.", tail_clear=True, delay=1, head=2)
def group_create(group_name: str, purp_members: List[bytes], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', _: 'MasterKey', group_id: Optional[bytes] = None ) -> None: """Create a new group. Validate the group name and determine what members can be added. """ error_msg = validate_group_name(group_name, contact_list, group_list) if error_msg: raise FunctionReturn(error_msg, head_clear=True) public_keys = set(contact_list.get_list_of_pub_keys()) purp_pub_keys = set(purp_members) accepted = list(purp_pub_keys & public_keys) rejected = list(purp_pub_keys - public_keys) if len(accepted) > settings.max_number_of_group_members: raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} " f"members per group.", head_clear=True) if len(group_list) == settings.max_number_of_groups: raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_groups} groups.", head_clear=True) header = GROUP_MSG_INVITE_HEADER if group_id is None else GROUP_MSG_JOIN_HEADER if group_id is None: while True: group_id = os.urandom(GROUP_ID_LENGTH) if group_id not in group_list.get_list_of_group_ids(): break group_list.add_group(group_name, group_id, settings.log_messages_by_default, settings.show_notifications_by_default, members=[contact_list.get_contact_by_pub_key(k) for k in accepted]) command = GROUP_CREATE + group_id + group_name.encode() + US_BYTE + b''.join(accepted) queue_command(command, settings, queues) group_management_print(NEW_GROUP, accepted, contact_list, group_name) group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name) if accepted: if yes("Publish the list of group members to participants?", abort=False): create_packet = header + group_id + b''.join(accepted) queue_to_nc(create_packet, queues[RELAY_PACKET_QUEUE]) else: m_print(f"Created an empty group '{group_name}'.", bold=True, head=1)
def remove_contact(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove contact from TFC.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) try: selection = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No account specified.", head_clear=True) if not yes(f"Remove contact '{selection}'?", abort=False, head=1): raise SoftError("Removal of contact aborted.", head=0, delay=1, tail_clear=True) if selection in contact_list.contact_selectors(): onion_pub_key = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key else: if validate_onion_addr(selection): raise SoftError("Error: Invalid selection.", head=0, delay=1, tail_clear=True) onion_pub_key = onion_address_to_pub_key(selection) receiver_command = CONTACT_REM + onion_pub_key queue_command(receiver_command, settings, queues) with ignored(SoftError): remove_logs(contact_list, group_list, settings, master_key, onion_pub_key) queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key)) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) target = determine_target(selection, onion_pub_key, contact_list) if any([g.remove_members([onion_pub_key]) for g in group_list]): m_print(f"Removed {target} from group(s).", tail=1) check_for_window_deselection(onion_pub_key, window, group_list)
def group_rm_group(group_name: str, contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', _: Optional[bytes] = None) -> None: """Remove the group with its members.""" if not yes(f"Remove group '{group_name}'?", abort=False): raise FunctionReturn("Group removal aborted.", head=0, delay=1, tail_clear=True) if group_name in group_list.get_list_of_group_names(): group_id = group_list.get_group(group_name).group_id else: try: group_id = b58decode(group_name) except ValueError: raise FunctionReturn("Error: Invalid group name/ID.", head_clear=True) command = LOG_REMOVE + group_id queue_command(command, settings, queues) command = GROUP_DELETE + group_id queue_command(command, settings, queues) if group_list.has_group(group_name): with ignored(FunctionReturn): remove_logs(contact_list, group_list, settings, master_key, group_id) else: raise FunctionReturn( f"Transmitter has no group '{group_name}' to remove.") group = group_list.get_group(group_name) if not group.empty() and yes("Notify members about leaving the group?", abort=False): exit_packet = (GROUP_MSG_EXIT_GROUP_HEADER + group.group_id + b''.join(group.get_list_of_member_pub_keys())) queue_to_nc(exit_packet, queues[RELAY_PACKET_QUEUE]) group_list.remove_group_by_name(group_name) raise FunctionReturn(f"Removed group '{group_name}'.", head=0, delay=1, tail_clear=True, bold=True)
def group_rm_member(group_name: str, purp_members: List[bytes], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', _: Optional[bytes] = None) -> None: """Remove member(s) from the specified group or remove the group itself.""" if not purp_members: group_rm_group(group_name, contact_list, group_list, settings, queues, master_key) if group_name not in group_list.get_list_of_group_names(): raise FunctionReturn(f"Group '{group_name}' does not exist.", head_clear=True) purp_pub_keys = set(purp_members) pub_keys = set(contact_list.get_list_of_pub_keys()) before_removal = set( group_list.get_group(group_name).get_list_of_member_pub_keys()) ok_pub_keys_set = set(purp_pub_keys & pub_keys) removable_set = set(before_removal & ok_pub_keys_set) remaining = list(before_removal - removable_set) not_in_group = list(ok_pub_keys_set - before_removal) rejected = list(purp_pub_keys - pub_keys) removable = list(removable_set) ok_pub_keys = list(ok_pub_keys_set) group = group_list.get_group(group_name) group.remove_members(removable) command = GROUP_REMOVE + group.group_id + b''.join(ok_pub_keys) queue_command(command, settings, queues) group_management_print(REMOVED_MEMBERS, removable, contact_list, group_name) group_management_print(NOT_IN_GROUP, not_in_group, contact_list, group_name) group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name) if removable and remaining and yes( "Publish the list of removed members to remaining members?", abort=False): rem_packet = (GROUP_MSG_MEMBER_REM_HEADER + group.group_id + int_to_bytes(len(remaining)) + b''.join(remaining) + b''.join(removable)) queue_to_nc(rem_packet, queues[RELAY_PACKET_QUEUE])
def contact_setting(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict') -> None: """\ Change logging, file reception, or notification setting of a group or (all) contact(s). """ try: parameters = user_input.plaintext.split() cmd_key = parameters[0] cmd_header = { LOGGING: CH_LOGGING, STORE: CH_FILE_RECV, NOTIFY: CH_NOTIFY }[cmd_key] setting, b_value = dict(on=(ENABLE, True), off=(DISABLE, False))[parameters[1]] except (IndexError, KeyError): raise SoftError("Error: Invalid command.", head_clear=True) # If second parameter 'all' is included, apply setting for all contacts and groups try: win_uid = b'' if parameters[2] == ALL: cmd_value = setting.upper() else: raise SoftError("Error: Invalid command.", head_clear=True) except IndexError: win_uid = window.uid cmd_value = setting + win_uid if win_uid: change_setting_for_selected_contact(cmd_key, b_value, window, contact_list, group_list) else: change_setting_for_all_contacts(cmd_key, b_value, contact_list, group_list) command = cmd_header + cmd_value if settings.traffic_masking and cmd_key == LOGGING: # Send `log_writer_loop` the new logging setting that is loaded # when the next noise packet is loaded from `noise_packet_loop`. queues[LOG_SETTING_QUEUE].put(b_value) window.update_log_messages() queue_command(command, settings, queues)
def remove_log(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove log entries for contact or group.""" try: selection = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No contact/group specified.", head_clear=True) if not yes(f"Remove logs for {selection}?", abort=False, head=1): raise FunctionReturn("Log file removal aborted.", tail_clear=True, delay=1, head=0) # Determine selector (group ID or Onion Service public key) from command parameters if selection in contact_list.contact_selectors(): selector = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key elif selection in group_list.get_list_of_group_names(): selector = group_list.get_group(selection).group_id elif len(selection) == ONION_ADDRESS_LENGTH: if validate_onion_addr(selection): raise FunctionReturn("Error: Invalid account.", head_clear=True) selector = onion_address_to_pub_key(selection) elif len(selection) == GROUP_ID_ENC_LENGTH: try: selector = b58decode(selection) except ValueError: raise FunctionReturn("Error: Invalid group ID.", head_clear=True) else: raise FunctionReturn("Error: Unknown selector.", head_clear=True) # Remove logs that match the selector command = LOG_REMOVE + selector queue_command(command, settings, queues) remove_logs(contact_list, group_list, settings, master_key, selector)
def change_setting_value(setting: str, value: str, relay_settings: Dict[str, bytes], queues: 'QueueDict', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', gateway: 'Gateway') -> None: """Change setting value in setting databases.""" if setting in gateway.settings.key_list: gateway.settings.change_setting(setting, value) else: settings.change_setting(setting, value, contact_list, group_list) receiver_command = CH_SETTING + setting.encode() + US_BYTE + value.encode() queue_command(receiver_command, settings, queues) if setting in relay_settings: if setting == 'allow_contact_requests': value = bool_to_bytes(settings.allow_contact_requests).decode() relay_command = UNENCRYPTED_DATAGRAM_HEADER + relay_settings[ setting] + value.encode() queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
def wipe(settings: 'Settings', queues: 'QueueDict', gateway: 'Gateway' ) -> None: """\ Reset terminals, wipe all TFC user data from Source, Networked, and Destination Computer, and power all three systems off. The purpose of the wipe command is to provide additional protection against physical attackers, e.g. in situation where a dissident gets a knock on their door. By overwriting and deleting user data the program prevents access to encrypted databases. Additional security should be sought with full disk encryption (FDE). Unfortunately, no effective tool for overwriting RAM currently exists. However, as long as Source and Destination Computers use FDE and DDR3 memory, recovery of sensitive 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?", abort=False): raise FunctionReturn("Wipe command aborted.", head_clear=True) clear_screen() for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]: while queues[q].qsize() != 0: queues[q].get() queue_command(WIPE_USR_DATA, settings, queues) if not settings.traffic_masking: if settings.local_testing_mode: time.sleep(0.8) time.sleep(gateway.settings.data_diode_sockets * 2.2) else: time.sleep(gateway.settings.race_condition_delay) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) os.system(RESET)
def log_command(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey' ) -> None: """Display message logs or export them to plaintext file on TCBs. Transmitter Program processes sent, Receiver Program sent and received, messages of all participants in the active window. """ cmd = user_input.plaintext.split()[0] export, header = dict(export =(True, LOG_EXPORT), history=(False, LOG_DISPLAY))[cmd] try: msg_to_load = int(user_input.plaintext.split()[1]) except ValueError: raise FunctionReturn("Error: Invalid number of messages.", head_clear=True) except IndexError: msg_to_load = 0 try: command = header + int_to_bytes(msg_to_load) + window.uid except struct.error: raise FunctionReturn("Error: Invalid number of messages.", head_clear=True) if export: if not yes(f"Export logs for '{window.name}' in plaintext?", abort=False): raise FunctionReturn("Log file export aborted.", tail_clear=True, head=0, delay=1) queue_command(command, settings, queues) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export) if export: raise FunctionReturn(f"Exported log file of {window.type} '{window.name}'.", head_clear=True)
def remove_log(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove log entries for contact or group.""" try: selection = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No contact/group specified.", head_clear=True) if not yes(f"Remove logs for {selection}?", abort=False, head=1): raise SoftError("Log file removal aborted.", tail_clear=True, delay=1, head=0) selector = determine_selector(selection, contact_list, group_list) # Remove logs that match the selector command = LOG_REMOVE + selector queue_command(command, settings, queues) remove_logs(contact_list, group_list, settings, master_key, selector)
def test_queue_command(self): self.assertIsNone(queue_command(os.urandom(200), self.settings, self.queues)) c_pt = self.queues[COMMAND_PACKET_QUEUE].get() self.assertEqual(len(c_pt), ASSEMBLY_PACKET_LENGTH)
def rxp_display_unread(settings: 'Settings', queues: 'QueueDict') -> None: """\ Display the list of windows that contain unread messages on Receiver Program. """ queue_command(WIN_ACTIVITY, settings, queues)
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 change_master_key(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', onion_service: 'OnionService') -> None: """Change the master key on Transmitter/Receiver Program.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) try: device = user_input.plaintext.split()[1].lower() except IndexError: raise SoftError( f"Error: No target-system ('{TX}' or '{RX}') specified.", head_clear=True) if device not in [TX, RX]: raise SoftError(f"Error: Invalid target system '{device}'.", head_clear=True) if device == RX: queue_command(CH_MASTER_KEY, settings, queues) return None authenticated = master_key.authenticate_action() if authenticated: # Cache old master key to allow log file re-encryption. old_master_key = master_key.master_key[:] # Create new master key but do not store new master key data into any database. new_master_key = master_key.master_key = master_key.new_master_key( replace=False) phase("Re-encrypting databases") # Halt `sender_loop` for the duration of database re-encryption. queues[KEY_MANAGEMENT_QUEUE].put((KDB_M_KEY_CHANGE_HALT_HEADER, )) wait_for_key_db_halt(queues) # Load old key_list from database file as it's not used on input_loop side. key_list = KeyList(master_key, settings) # Update encryption keys for databases contact_list.database.database_key = new_master_key key_list.database.database_key = new_master_key group_list.database.database_key = new_master_key settings.database.database_key = new_master_key onion_service.database.database_key = new_master_key # Create temp databases for each database, do not replace original. with ignored(SoftError): change_log_db_key(old_master_key, new_master_key, settings) contact_list.store_contacts(replace=False) key_list.store_keys(replace=False) group_list.store_groups(replace=False) settings.store_settings(replace=False) onion_service.store_onion_service_private_key(replace=False) # At this point all temp files exist and they have been checked to be valid by the respective # temp file writing function. It's now time to create a temp file for the new master key # database. Once the temp master key database is created, the `replace_database_data()` method # will also run the atomic `os.replace()` command for the master key database. master_key.replace_database_data() # Next we do the atomic `os.replace()` for all other files too. replace_log_db(settings) contact_list.database.replace_database() key_list.database.replace_database() group_list.database.replace_database() settings.database.replace_database() onion_service.database.replace_database() # Now all databases have been updated. It's time to let # the key database know what the new master key is. queues[KEY_MANAGEMENT_QUEUE].put(new_master_key) wait_for_key_db_ack(new_master_key, queues) phase(DONE) m_print("Master key successfully changed.", bold=True, tail_clear=True, delay=1, head=1)
def change_setting(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', gateway: 'Gateway') -> None: """Change setting on Transmitter and Receiver Program.""" # Validate the KV-pair try: setting = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No setting specified.", head_clear=True) if setting not in (settings.key_list + gateway.settings.key_list): raise FunctionReturn(f"Error: Invalid setting '{setting}'.", head_clear=True) try: value = user_input.plaintext.split()[2] except IndexError: raise FunctionReturn("Error: No value for setting specified.", head_clear=True) # Check if the setting can be changed relay_settings = dict( serial_error_correction=UNENCRYPTED_EC_RATIO, serial_baudrate=UNENCRYPTED_BAUDRATE, allow_contact_requests=UNENCRYPTED_MANAGE_CONTACT_REQ) if settings.traffic_masking and (setting in relay_settings or setting == 'max_number_of_contacts'): raise FunctionReturn( "Error: Can't change this setting during traffic masking.", head_clear=True) if setting in ['use_serial_usb_adapter', 'built_in_serial_interface']: raise FunctionReturn( "Error: Serial interface setting can only be changed manually.", head_clear=True) if setting == 'ask_password_for_log_access': try: authenticated = master_key.load_master_key( ) == master_key.master_key except (EOFError, KeyboardInterrupt): raise FunctionReturn(f"Setting change aborted.", tail_clear=True, head=2, delay=1) if not authenticated: raise FunctionReturn("Error: No permission to change setting.", head_clear=True) # Change the setting if setting in gateway.settings.key_list: gateway.settings.change_setting(setting, value) else: settings.change_setting(setting, value, contact_list, group_list) receiver_command = CH_SETTING + setting.encode() + US_BYTE + value.encode() queue_command(receiver_command, settings, queues) if setting in relay_settings: if setting == 'allow_contact_requests': value = bool_to_bytes(settings.allow_contact_requests).decode() relay_command = UNENCRYPTED_DATAGRAM_HEADER + relay_settings[ setting] + value.encode() queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) # Propagate the effects of the setting if setting == 'max_number_of_contacts': contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put((KDB_UPDATE_SIZE_HEADER, settings)) if setting in ['max_number_of_group_members', 'max_number_of_groups']: group_list.store_groups() if setting == 'traffic_masking': queues[SENDER_MODE_QUEUE].put(settings) queues[TRAFFIC_MASKING_QUEUE].put(settings.traffic_masking) window.deselect() if setting == 'log_file_masking': queues[LOGFILE_MASKING_QUEUE].put(settings.log_file_masking)
def create_pre_shared_key( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Nick of contact contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object onion_service: 'OnionService', # OnionService object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Generate a new pre-shared key for manual key delivery. Pre-shared keys offer a low-tech solution against the slowly emerging threat of quantum computers. PSKs are less convenient and not usable in every scenario, but until a quantum-safe key exchange algorithm with reasonably short keys is standardized, TFC can't provide a better alternative against quantum computers. The generated keys are protected by a key encryption key, derived from a 256-bit salt and a password (that is to be shared with the recipient) using Argon2d key derivation function. The encrypted message and header keys are stored together with salt on a removable media. This media must be a never-before-used device from sealed packaging. Re-using an old device might infect Source Computer, and the malware could either copy sensitive data on that removable media, or Source Computer might start transmitting the sensitive data covertly over the serial interface to malware on Networked Computer. Once the key has been exported to the clean drive, contact data and keys are exported to the Receiver Program on Destination computer. The transmission is encrypted with the local key. """ try: tx_mk = csprng() tx_hk = csprng() salt = csprng() password = MasterKey.new_password("password for PSK") phase("Deriving key encryption key", head=2) kek = argon2_kdf(password, salt, time_cost=ARGON2_PSK_TIME_COST, memory_cost=ARGON2_PSK_MEMORY_COST) phase(DONE) ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek) while True: trunc_addr = pub_key_to_short_address(onion_pub_key) store_d = ask_path_gui(f"Select removable media for {nick}", settings) f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}" try: with open(f_name, 'wb+') as f: f.write(salt + ct_tag) break except PermissionError: m_print( "Error: Did not have permission to write to the directory.", delay=0.5) continue command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk + csprng() + str_to_bytes(nick)) queue_command(command, settings, queues) contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) 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): raise FunctionReturn("PSK generation aborted.", tail_clear=True, delay=1, head=2)
def new_local_key(contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict') -> None: """Run local key exchange protocol. Local key encrypts commands and data sent from Source Computer to user's Destination Computer. The key is delivered to Destination Computer in packet encrypted with an ephemeral, symmetric, key encryption key. The check-summed Base58 format key decryption key is typed to Receiver Program manually. This prevents local key leak in following scenarios: 1. CT is intercepted by an adversary on compromised Networked Computer, but no visual eavesdropping takes place. 2. CT is not intercepted by an adversary on Networked Computer, but visual eavesdropping records key decryption key. 3. CT is delivered from Source Computer to Destination Computer directly (bypassing compromised Networked Computer), and visual eavesdropping records key decryption key. Once the correct key decryption key is entered to Receiver Program, it will display the 2-hexadecimal confirmation code generated by the Transmitter Program. The code will be entered back to Transmitter Program to confirm the user has successfully delivered the key decryption key. The protocol is completed with Transmitter Program sending LOCAL_KEY_RDY signal to the Receiver Program, that then moves to wait for public keys from contact. """ try: if settings.traffic_masking and contact_list.has_local_contact(): raise SoftError( "Error: Command is disabled during traffic masking.", head_clear=True) m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) if not contact_list.has_local_contact(): time.sleep(0.5) key = csprng() hek = csprng() kek = csprng() c_code = os.urandom(CONFIRM_CODE_LENGTH) local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign( plaintext=key + hek + c_code, key=kek) deliver_local_key(local_key_packet, kek, c_code, settings, queues) # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, blake2b(b58encode(kek).encode()), bytes(FINGERPRINT_LENGTH), KEX_STATUS_LOCAL_KEY, False, False, False) # Add local contact to keyset database queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng())) # Notify Receiver Program that confirmation code was successfully entered queue_command(LOCAL_KEY_RDY, settings, queues) m_print("Successfully completed the local key exchange.", bold=True, tail_clear=True, delay=1, head=1) reset_terminal() except (EOFError, KeyboardInterrupt): raise SoftError("Local key setup aborted.", tail_clear=True, delay=1, head=2)
def select_tx_window( self, settings: 'Settings', # Settings object queues: 'QueueDict', # Dictionary of Queues onion_service: 'OnionService', # OnionService object gateway: 'Gateway', # Gateway object selection: Optional[str] = None, # Selector for window cmd: bool = False # True when `/msg` command is used to switch window ) -> None: """Select specified window or ask the user to specify one.""" if selection is None: self.contact_list.print_contacts() self.group_list.print_groups() if self.contact_list.has_only_pending_contacts(): print( "\n'/connect' sends Onion Service/contact data to Relay" "\n'/add' adds another contact." "\n'/rm <Nick>' removes an existing contact.\n") selection = input("Select recipient: ").strip() if selection in self.group_list.get_list_of_group_names(): if cmd and settings.traffic_masking and selection != self.name: raise FunctionReturn( "Error: Can't change window during traffic masking.", head_clear=True) self.contact = None self.group = self.group_list.get_group(selection) self.window_contacts = self.group.members self.name = self.group.name self.uid = self.group.group_id self.group_id = self.group.group_id self.log_messages = self.group.log_messages self.type = WIN_TYPE_GROUP self.type_print = 'group' elif selection in self.contact_list.contact_selectors(): if cmd and settings.traffic_masking: contact = self.contact_list.get_contact_by_address_or_nick( selection) if contact.onion_pub_key != self.uid: raise FunctionReturn( "Error: Can't change window during traffic masking.", head_clear=True) self.contact = self.contact_list.get_contact_by_address_or_nick( selection) if self.contact.kex_status == KEX_STATUS_PENDING: start_key_exchange(self.contact.onion_pub_key, self.contact.nick, self.contact_list, settings, queues) self.group = None self.group_id = None self.window_contacts = [self.contact] self.name = self.contact.nick self.uid = self.contact.onion_pub_key self.log_messages = self.contact.log_messages self.type = WIN_TYPE_CONTACT self.type_print = 'contact' elif selection.startswith('/'): self.window_selection_command(selection, settings, queues, onion_service, gateway) else: raise FunctionReturn("Error: No contact/group was found.") if settings.traffic_masking: queues[WINDOW_SELECT_QUEUE].put(self.window_contacts) packet = WIN_SELECT + self.uid queue_command(packet, settings, queues) clear_screen()
def new_local_key(contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict') -> None: """Run local key exchange protocol. Local key encrypts commands and data sent from Source Computer to user's Destination Computer. The key is delivered to Destination Computer in packet encrypted with an ephemeral, symmetric, key encryption key. The check-summed Base58 format key decryption key is typed to Receiver Program manually. This prevents local key leak in following scenarios: 1. CT is intercepted by an adversary on compromised Networked Computer, but no visual eavesdropping takes place. 2. CT is not intercepted by an adversary on Networked Computer, but visual eavesdropping records key decryption key. 3. CT is delivered from Source Computer to Destination Computer directly (bypassing compromised Networked Computer), and visual eavesdropping records key decryption key. Once the correct key decryption key is entered to Receiver Program, it will display the 2-hexadecimal confirmation code generated by the Transmitter Program. The code will be entered back to Transmitter Program to confirm the user has successfully delivered the key decryption key. The protocol is completed with Transmitter Program sending LOCAL_KEY_RDY signal to the Receiver Program, that then moves to wait for public keys from contact. """ try: if settings.traffic_masking and contact_list.has_local_contact(): raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) if not contact_list.has_local_contact(): time.sleep(0.5) key = csprng() hek = csprng() kek = csprng() c_code = os.urandom(CONFIRM_CODE_LENGTH) local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign( plaintext=key + hek + c_code, key=kek) # Deliver local key to Destination computer nc_bypass_msg(NC_BYPASS_START, settings) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) while True: print_key("Local key decryption key (to Receiver)", kek, settings) purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): nc_bypass_msg(NC_BYPASS_STOP, settings) break elif purp_code == '': phase("Resending local key", head=2) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) phase(DONE) print_on_previous_line( reps=(9 if settings.local_testing_mode else 10)) else: m_print([ "Incorrect confirmation code. If Receiver did not receive", "the encrypted local key, resend it by pressing <Enter>." ], head=1) print_on_previous_line( reps=(9 if settings.local_testing_mode else 10), delay=2) # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_LOCAL_KEY, False, False, False) # Add local contact to keyset database queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng())) # Notify Receiver that confirmation code was successfully entered queue_command(LOCAL_KEY_RDY, settings, queues) m_print("Successfully completed the local key exchange.", bold=True, tail_clear=True, delay=1, head=1) os.system(RESET) except (EOFError, KeyboardInterrupt): raise FunctionReturn("Local key setup aborted.", tail_clear=True, delay=1, head=2)