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 setup(self) -> None: """Prompt the user to enter initial serial interface setting. Ensure that the serial interface is available before proceeding. """ if not self.local_testing_mode and not self.qubes: name = { TX: TRANSMITTER, NC: RELAY, RX: RECEIVER }[self.software_operation] self.use_serial_usb_adapter = yes( f"Use USB-to-serial/TTL adapter for {name} Computer?", head=1, tail=1) if self.use_serial_usb_adapter: for f in sorted(os.listdir('/dev/')): if f.startswith('ttyUSB'): return None m_print("Error: USB-to-serial/TTL adapter not found.") self.setup() else: if self.built_in_serial_interface not in sorted( os.listdir('/dev/')): m_print( f"Error: Serial interface /dev/{self.built_in_serial_interface} not found." ) self.setup()
def log_command(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', c_queue: 'Queue', master_key: 'MasterKey') -> None: """Display message logs or export them to plaintext file on TxM/RxM. TxM processes sent messages, RxM processes sent and received messages for all participants in active window. """ cmd = user_input.plaintext.split()[0] export, header = dict(export=(True, LOG_EXPORT_HEADER), history=(False, LOG_DISPLAY_HEADER))[cmd] try: msg_to_load = int(user_input.plaintext.split()[1]) except ValueError: raise FunctionReturn("Error: Invalid number of messages.") except IndexError: msg_to_load = 0 if export and not yes( f"Export logs for '{window.name}' in plaintext?", head=1, tail=1): raise FunctionReturn("Logfile export aborted.") try: command = header + window.uid.encode() + US_BYTE + int_to_bytes( msg_to_load) except struct.error: raise FunctionReturn("Error: Invalid number of messages.") queue_command(command, settings, c_queue) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export)
def validate_traffic_masking_delay(key: str, value: 'SettingType', contact_list: 'ContactList') -> None: """Validate setting value for traffic masking delays.""" if key in ["tm_static_delay", "tm_random_delay"]: for key_, name, min_setting in [ ("tm_static_delay", "static", TRAFFIC_MASKING_MIN_STATIC_DELAY), ("tm_random_delay", "random", TRAFFIC_MASKING_MIN_RANDOM_DELAY) ]: if key == key_ and value < min_setting: raise SoftError( f"Error: Can't set {name} delay lower than {min_setting}.", head_clear=True) if contact_list.settings.software_operation == TX: m_print([ "WARNING!", "Changing traffic masking delay can make your endpoint and traffic look unique!" ], bold=True, head=1, tail=1) if not yes("Proceed anyway?"): raise SoftError("Aborted traffic masking setting change.", head_clear=True) m_print("Traffic masking setting will change on restart.", head=1, tail=1)
def send_onion_service_key(contact_list: 'ContactList', settings: 'Settings', onion_service: 'OnionService', gateway: 'Gateway') -> None: """Resend Onion Service key to Relay Program on Networked Computer. This command is used in cases where Relay Program had to be restarted for some reason (e.g. due to system updates). """ try: if settings.traffic_masking: m_print([ "Warning!", "Exporting Onion Service data to Networked Computer ", "during traffic masking can reveal to an adversary ", "TFC is being used at the moment. You should only do ", "this if you've had to restart the Relay Program." ], bold=True, head=1, tail=1) if not yes("Proceed with the Onion Service data export?", abort=False): raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=0) export_onion_service_data(contact_list, settings, onion_service, gateway) except (EOFError, KeyboardInterrupt): raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=2)
def process_offset( offset: int, # Number of dropped packets origin: bytes, # "to/from" preposition direction: str, # Direction of packet nick: str, # Nickname of associated contact window: 'RxWindow' # RxWindow object ) -> None: """Display warnings about increased offsets. If the offset has increased over the threshold, ask the user to confirm hash ratchet catch up. """ if offset > HARAC_WARN_THRESHOLD and origin == ORIGIN_CONTACT_HEADER: m_print([ f"Warning! {offset} packets from {nick} were not received.", f"This might indicate that {offset} most recent packets were ", f"lost during transmission, or that the contact is attempting ", f"a DoS attack. You can wait for TFC to attempt to decrypt the ", "packet, but it might take a very long time or even forever." ]) if not yes("Proceed with the decryption?", abort=False, tail=1): raise FunctionReturn(f"Dropped packet from {nick}.", window=window) elif offset: m_print( f"Warning! {offset} packet{'s' if offset > 1 else ''} {direction} {nick} were not received." )
def group_rm_group(group_name: str, contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> 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 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 test_yes(self, _: Any) -> None: self.assertTrue(yes('test prompt', head=1, tail=1)) self.assertTrue(yes('test prompt')) self.assertFalse(yes('test prompt', head=1, tail=1)) self.assertFalse(yes('test prompt')) self.assertTrue(yes('test prompt', head=1, tail=1, abort=True)) self.assertFalse(yes('test prompt', abort=False)) self.assertTrue(yes('test prompt', head=1, tail=1, abort=True)) self.assertFalse(yes('test prompt', abort=False)) with self.assertRaises(EOFError): self.assertFalse(yes('test prompt')) with self.assertRaises(KeyboardInterrupt): self.assertFalse(yes('test prompt'))
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 test_yes(self, _): self.assertTrue(yes('test prompt', head=1, tail=1)) self.assertTrue(yes('test prompt')) self.assertFalse(yes('test prompt', head=1, tail=1)) self.assertFalse(yes('test prompt')) self.assertTrue(yes('test prompt', head=1, tail=1, abort=True)) self.assertFalse(yes('test prompt', abort=False)) self.assertTrue(yes('test prompt', head=1, tail=1, abort=True)) self.assertFalse(yes('test prompt', abort=False))
def queue_file(window: 'TxWindow', settings: 'Settings', queues: 'QueueDict') -> None: """Ask file path and load file data. In TFC there are two ways to send a file. For traffic masking, the file is loaded and sent inside normal messages using assembly packet headers dedicated for file transmission. This transmission is much slower, so the File object will determine metadata about the transmission's estimated transfer time, number of packets and the name and size of file. This information is inserted to the first assembly packet so that the recipient can observe the transmission progress from file transfer window. When traffic masking is disabled, file transmission is much faster as the file is only encrypted and transferred over serial once before the Relay Program multi-casts the ciphertext to each specified recipient. See the send_file docstring (below) for more details. """ path = ask_path_gui("Select file to send...", settings, get_file=True) if path.endswith( ('tx_contacts', 'tx_groups', 'tx_keys', 'tx_login_data', 'tx_settings', 'rx_contacts', 'rx_groups', 'rx_keys', 'rx_login_data', 'rx_settings', 'tx_serial_settings.json', 'nc_serial_settings.json', 'rx_serial_settings.json', 'tx_onion_db')): raise FunctionReturn("Error: Can't send TFC database.", head_clear=True) if not settings.traffic_masking: send_file(path, settings, queues, window) return file = File(path, window, settings) assembly_packets = split_to_assembly_packets(file.plaintext, FILE) if settings.confirm_sent_files: try: if not yes( f"Send {file.name.decode()} ({file.size_hr}) to {window.type_print} {window.name} " f"({len(assembly_packets)} packets, time: {file.time_hr})?" ): raise FunctionReturn("File selection aborted.", head_clear=True) except (EOFError, KeyboardInterrupt): raise FunctionReturn("File selection aborted.", head_clear=True) queue_assembly_packets(assembly_packets, FILE, settings, queues, window, log_as_ph=True)
def setup(self) -> None: """Prompt the user to enter initial serial interface setting. Ensure that the serial interface is available before proceeding. """ if not self.local_testing_mode and not self.qubes: name = { TX: TRANSMITTER, NC: RELAY, RX: RECEIVER }[self.software_operation] self.use_serial_usb_adapter = yes( f"Use USB-to-serial/TTL adapter for {name} Computer?", head=1, tail=1) if self.use_serial_usb_adapter: for f in sorted(os.listdir('/dev/')): if f.startswith('ttyUSB'): return None m_print("Error: USB-to-serial/TTL adapter not found.") self.setup() else: if self.built_in_serial_interface not in sorted( os.listdir('/dev/')): m_print( f"Error: Serial interface /dev/{self.built_in_serial_interface} not found." ) self.setup() if self.qubes and self.software_operation != RX: # Check if IP address was stored by the installer. if os.path.isfile(QUBES_RX_IP_ADDR_FILE): cached_ip = open(QUBES_RX_IP_ADDR_FILE).read().strip() os.remove(QUBES_RX_IP_ADDR_FILE) if validate_ip_address(cached_ip) == '': self.rx_udp_ip = cached_ip return # If we reach this point, no cached IP was found, prompt for IP address from the user. rx_device, short = ('Networked', 'NET') if self.software_operation == TX else ( 'Destination', 'DST') m_print(f"Enter the IP address of the {rx_device} Computer", head=1, tail=1) self.rx_udp_ip = box_input(f"{short} IP-address", expected_len=15, validator=validate_ip_address, tail=1)
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 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_create(group_name: str, purp_members: List[str], group_list: 'GroupList', contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue'], _: 'MasterKey') -> None: """Create a new group. Validate group name and determine what members that can be added. """ validate_group_name(group_name, contact_list, group_list) accounts = set(contact_list.get_list_of_accounts()) purp_accounts = set(purp_members) accepted = list(accounts & purp_accounts) rejected = list(purp_accounts - accounts) if len(accepted) > settings.max_number_of_group_members: raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.") if len(group_list) == settings.max_number_of_groups: raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_groups} groups.") group_list.add_group(group_name, settings.log_messages_by_default, settings.show_notifications_by_default, members=[contact_list.get_contact(c) for c in accepted]) fields = [f.encode() for f in ([group_name] + accepted)] command = GROUP_CREATE_HEADER + US_BYTE.join(fields) queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) 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 list of group members to participants?"): for member in accepted: m_list = [m for m in accepted if m != member] queue_message(user_input=UserInput(US_STR.join([group_name] + m_list), MESSAGE), window =MockWindow(member, [contact_list.get_contact(member)]), settings =settings, m_queue =queues[MESSAGE_PACKET_QUEUE], header =GROUP_MSG_INVITEJOIN_HEADER, log_as_ph =True) else: box_print(f"Created an empty group '{group_name}'", head=1)
def remove_contact(user_input: 'UserInput', window: 'Window', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Remove contact on TxM/RxM.""" if settings.session_trickle: raise FunctionReturn("Command disabled during trickle connection.") try: selection = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No account specified.") if not yes(f"Remove {selection} completely?", head=1): raise FunctionReturn("Removal of contact aborted.") # Load account if user enters nick if selection in contact_list.get_list_of_nicks(): selection = contact_list.get_contact(selection).rx_account packet = CONTACT_REMOVE_HEADER + selection.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) if selection in contact_list.get_list_of_accounts(): queues[KEY_MANAGEMENT_QUEUE].put(('REM', selection)) contact_list.remove_contact(selection) box_print(f"Removed {selection} from contacts.", head=1, tail=1) else: box_print(f"TxM has no {selection} to remove.", head=1, tail=1) if any([g.remove_members([selection]) for g in group_list]): box_print(f"Removed {selection} from group(s).", tail=1) for c in window: if selection == c.rx_account: if window.type == 'contact': window.deselect() elif window.type == 'group': window.update_group_win_members(group_list) # If last member from group is removed, deselect group. # This is not done in update_group_win_members because # It would prevent selecting the empty group for group # related commands such as notifications. if not window.window_contacts: window.deselect()
def group_rm_member(group_name: str, purp_members: List[str], group_list: 'GroupList', contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue'], master_key: 'MasterKey') -> None: """Remove member(s) from group or group itself.""" if not purp_members: group_rm_group(group_name, 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.") purp_accounts = set(purp_members) accounts = set(contact_list.get_list_of_accounts()) before_removal = set(group_list.get_group(group_name).get_list_of_member_accounts()) ok_accounts_set = set(purp_accounts & accounts) removable_set = set(before_removal & ok_accounts_set) end_assembly = list(before_removal - removable_set) not_in_group = list(ok_accounts_set - before_removal) rejected = list(purp_accounts - accounts) removable = list(removable_set) ok_accounts = list(ok_accounts_set) group = group_list.get_group(group_name) group.remove_members(removable) fields = [f.encode() for f in ([group_name] + ok_accounts)] command = GROUP_REMOVE_M_HEADER + US_BYTE.join(fields) queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) 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 end_assembly and yes("Publish list of removed members to remaining members?"): for member in end_assembly: queue_message(user_input=UserInput(US_STR.join([group_name] + removable), MESSAGE), window =MockWindow(member, [contact_list.get_contact(member)]), settings =settings, m_queue =queues[MESSAGE_PACKET_QUEUE], header =GROUP_MSG_MEMBER_REM_HEADER, log_as_ph =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 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 window_selection_command(self, selection: str, settings: 'Settings', queues: 'QueueDict', onion_service: 'OnionService', gateway: 'Gateway') -> None: """Commands for adding and removing contacts from contact selection menu. In situations where only pending contacts are available and those contacts are not online, these commands prevent the user from not being able to add new contacts. """ if selection == '/add': add_new_contact(self.contact_list, self.group_list, settings, queues, onion_service) raise FunctionReturn("New contact added.", output=False) elif selection == '/connect': export_onion_service_data(self.contact_list, settings, onion_service, gateway) elif selection.startswith('/rm'): try: selection = selection.split()[1] except IndexError: raise FunctionReturn("Error: No account specified.", delay=1) if not yes(f"Remove contact '{selection}'?", abort=False, head=1): raise FunctionReturn("Removal of contact aborted.", head=0, delay=1) if selection in self.contact_list.contact_selectors(): onion_pub_key = self.contact_list.get_contact_by_address_or_nick( selection).onion_pub_key self.contact_list.remove_contact_by_pub_key(onion_pub_key) self.contact_list.store_contacts() raise FunctionReturn(f"Removed contact '{selection}'.", delay=1) else: raise FunctionReturn(f"Error: Unknown contact '{selection}'.", delay=1) else: raise FunctionReturn("Error: Invalid command.", delay=1)
def queue_file(window: 'TxWindow', settings: 'Settings', f_queue: 'Queue', gateway: 'Gateway') -> None: """Ask file path and load file data.""" path = ask_path_gui("Select file to send...", settings, get_file=True) file = File(path, window, settings, gateway) packet_list = split_to_assembly_packets(file.plaintext, FILE) if settings.confirm_sent_files: try: if not yes(f"Send {file.name.decode()} ({file.size_print}) to {window.type_print} {window.name} " f"({len(packet_list)} packets, time: {file.time_print})?"): raise FunctionReturn("File selection aborted.") except KeyboardInterrupt: raise FunctionReturn("File selection aborted.", head=3) queue_packets(packet_list, FILE, settings, f_queue, window, log_as_ph=True)
def queue_file(window: 'Window', settings: 'Settings', f_queue: 'Queue', gateway: 'Gateway') -> None: """Ask file path and load file data.""" path = ask_path_gui("Select file to send...", settings, get_file=True) file = File(path, window, settings, gateway) name = file.name.decode() size = file.size.decode() payload = file.plaintext if len(payload) < 255: padded = byte_padding(payload) packet_list = [F_S_HEADER + padded] else: payload = bytes(8) + payload padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=255) # < number of packets > packet_list = ( [F_L_HEADER + int_to_bytes(len(p_list)) + p_list[0][8:]] + [F_A_HEADER + p for p in p_list[1:-1]] + [F_E_HEADER + p_list[-1]]) for p in packet_list: assert len(p) == 256 if settings.confirm_sent_files: if not yes( f"Send {name} ({size}) to {window.type} {window.name} " f"({len(packet_list)} packets, time: {file.time_s})?", tail=1): raise FunctionReturn("File selection aborted.") if settings.session_trickle: log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window) for p in packet_list: f_queue.put((p, log_m_dictionary)) else: for c in window: for p in packet_list: f_queue.put((p, settings, c.rx_account, c.tx_account, c.log_messages, window.uid))
def remove_log(user_input: 'UserInput', contact_list: 'ContactList', settings: 'Settings', c_queue: 'Queue', master_key: 'MasterKey') -> None: """Remove log entries for contact.""" try: selection = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No contact/group specified.") if not yes(f"Remove logs for {selection}?", head=1): raise FunctionReturn("Logfile removal aborted.") # Swap specified nick to rx_account if selection in contact_list.get_list_of_nicks(): selection = contact_list.get_contact(selection).rx_account command = LOG_REMOVE_HEADER + selection.encode() queue_command(command, settings, c_queue) remove_logs(selection, settings, master_key)
def verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool: """\ Verify fingerprints over out-of-band channel to detect MITM attacks against TFC's key exchange. :param tx_fp: User's fingerprint :param rx_fp: Contact's fingerprint :return: True if fingerprints match, else False """ clear_screen() message_printer("To verify received public key was not replaced by attacker in network, " "call the contact over end-to-end encrypted line, preferably Signal " "(https://signal.org/). Make sure Signal's safety numbers have been " "verified, and then verbally compare the key fingerprints below.", head=1, tail=1) print_fingerprint(tx_fp, " Your fingerprint (you read) ") print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)") return yes("Is the contact's fingerprint correct?")
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 verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool: """Verify fingerprints over off-band channel to detect MITM attacks between NHs. :param tx_fp: User's fingerprint :param rx_fp: Contact's fingerprint :return: True if fingerprints match, else False """ clear_screen() message_printer( "To verify the public key was not swapped during delivery, " "call your contact over end-to-end encrypted line, preferably " "Signal by Open Whisper Systems. Verify call's Short " "Authentication String and then compare fingerprints below.", head=1, tail=1) print_fingerprints(tx_fp, " Your fingerprint (you read) ") print_fingerprints(rx_fp, "Purported fingerprint for contact (they read)") return yes("Is the contact's fingerprint correct?")
def validate_group_name(group_name: str, contact_list: 'ContactList', group_list: 'GroupList') -> None: """Check that group name is valid.""" # Avoids collision with delimiters if not group_name.isprintable(): raise FunctionReturn("Error: Group name must be printable.") # Length limited by database's unicode padding if len(group_name) >= PADDING_LEN: raise FunctionReturn("Error: Group name must be less than 255 chars long.") if group_name == DUMMY_GROUP: raise FunctionReturn("Error: Group name can't use name reserved for database padding.") if re.match(ACCOUNT_FORMAT, group_name): raise FunctionReturn("Error: Group name can't have format of an account.") if group_name in contact_list.get_list_of_nicks(): raise FunctionReturn("Error: Group name can't be nick of contact.") if group_name in group_list.get_list_of_group_names(): if not yes(f"Group with name '{group_name}' already exists. Overwrite?", head=1): raise FunctionReturn("Group creation aborted.")
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 verify_fingerprints( tx_fp: bytes, # User's fingerprint rx_fp: bytes # Contact's fingerprint ) -> bool: # True if fingerprints match, else False """\ Verify fingerprints over an authenticated out-of-band channel to detect MITM attacks against TFC's key exchange. MITM or man-in-the-middle attack is an attack against an inherent problem in cryptography: Cryptography is math, nothing more. During key exchange public keys are just very large numbers. There is no way to tell by looking if a number (received from an untrusted network / Networked Computer) is the same number the contact generated. Public key fingerprints are values designed to be compared by humans either visually or audibly (or sometimes by using semi-automatic means such as QR-codes). By comparing the fingerprint over an authenticated channel it's possible to verify that the correct key was received from the network. """ m_print( "To verify received public key was not replaced by an attacker " "call the contact over an end-to-end encrypted line, preferably Signal " "(https://signal.org/). Make sure Signal's safety numbers have been " "verified, and then verbally compare the key fingerprints below.", head_clear=True, max_width=49, head=1, tail=1) print_fingerprint(tx_fp, " Your fingerprint (you read) ") print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)") return yes("Is the contact's fingerprint correct?")