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 cli_get_file(prompt_msg: str) -> str: """Ask the user to specify file to load.""" while True: try: path_to_file = input(prompt_msg + ": ") if not path_to_file: print_on_previous_line() raise KeyboardInterrupt if os.path.isfile(path_to_file): if path_to_file.startswith('./'): path_to_file = path_to_file[len('./'):] print('') return path_to_file m_print("File selection error.", head=1, tail=1) print_on_previous_line(reps=4, delay=1) except (EOFError, KeyboardInterrupt): print_on_previous_line() raise FunctionReturn("File selection aborted.", head_clear=True)
def change_setting(self, key: str, value_str: str) -> None: """Parse, update and store new setting value.""" attribute = self.__getattribute__(key) try: if isinstance(attribute, bool): value = dict(true=True, false=False)[value_str.lower()] # type: Union[bool, int] elif isinstance(attribute, int): value = int(value_str) if value < 0 or value > MAX_INT: raise ValueError else: raise CriticalError("Invalid attribute type in settings.") except (KeyError, ValueError): raise FunctionReturn(f"Error: Invalid setting value '{value_str}'.", delay=1, tail_clear=True) self.validate_key_value_pair(key, value) setattr(self, key, value) self.store_settings()
def process_long_header(self, packet: bytes, packet_ct: Optional[bytes] = None) -> None: """Process first packet of long transmission.""" if self.long_active: self.add_masking_packet_to_log_file( increase=len(self.assembly_pt_list)) if self.type == FILE: self.new_file_packet() try: lh, no_p_bytes, time_bytes, size_bytes, _ \ = separate_headers(packet, [ASSEMBLY_PACKET_HEADER_LENGTH] + 3*[ENCODED_INTEGER_LENGTH]) self.packets = bytes_to_int( no_p_bytes ) # added by transmitter.packet.split_to_assembly_packets self.time = str(timedelta(seconds=bytes_to_int(time_bytes))) self.size = readable_size(bytes_to_int(size_bytes)) self.name = packet.split(US_BYTE)[0].decode() m_print([ f'Receiving file from {self.contact.nick}:', f'{self.name} ({self.size})', f'ETA {self.time} ({self.packets} packets)' ], bold=True) except (struct.error, UnicodeError, ValueError): self.add_masking_packet_to_log_file() raise FunctionReturn( "Error: Received file packet had an invalid header.") self.assembly_pt_list = [packet] self.long_active = True self.is_complete = False if packet_ct is not None: self.log_ct_list = [packet_ct]
def cli_get_path(prompt_msg: str) -> str: """Ask the user to specify path for file.""" while True: try: directory = input(prompt_msg + ": ") if directory.startswith('./'): directory = directory[len('./'):] if not directory.endswith(os.sep): directory += os.sep if not os.path.isdir(directory): m_print("Error: Invalid directory.", head=1, tail=1) print_on_previous_line(reps=4, delay=1) continue return directory except (EOFError, KeyboardInterrupt): print_on_previous_line() raise FunctionReturn("File path selection aborted.", head_clear=True)
def process_assembled_file( ts: 'datetime', # Timestamp last received packet payload: bytes, # File name and content onion_pub_key: bytes, # Onion Service pubkey of sender nick: str, # Nickname of sender settings: 'Settings', # Settings object window_list: 'WindowList', # WindowList object ) -> None: """Process received file assembly packets.""" try: file_name_b, file_data = payload.split(US_BYTE, 1) except ValueError: raise FunctionReturn("Error: Received file had an invalid structure.") try: file_name = file_name_b.decode() except UnicodeError: raise FunctionReturn("Error: Received file name had invalid encoding.") if not file_name.isprintable() or not file_name or '/' in file_name: raise FunctionReturn("Error: Received file had an invalid name.") file_ct, file_key = separate_trailer(file_data, SYMMETRIC_KEY_LENGTH) if len(file_key) != SYMMETRIC_KEY_LENGTH: raise FunctionReturn("Error: Received file had an invalid key.") try: file_pt = auth_and_decrypt(file_ct, file_key) except nacl.exceptions.CryptoError: raise FunctionReturn("Error: Decryption of file data failed.") try: file_dc = decompress(file_pt, settings.max_decompress_size) except zlib.error: raise FunctionReturn("Error: Decompression of file data failed.") file_dir = f'{DIR_RECV_FILES}{nick}/' final_name = store_unique(file_dc, file_dir, file_name) message = f"Stored file from {nick} as '{final_name}'." if settings.traffic_masking and window_list.active_win is not None: window = window_list.active_win else: window = window_list.get_window(onion_pub_key) window.add_new(ts, message, onion_pub_key, output=True, event_msg=True)
def print_logs(message_list: List[MsgTuple], export: bool, msg_to_load: int, window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings') -> None: """Print list of logged messages to screen or export them to file.""" terminal_width = get_terminal_width() system, m_dir = { TX: ("Transmitter", "sent to"), RX: ("Receiver", "to/from") }[settings.software_operation] f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent " title = textwrap.fill( f"Log file of {subset}message(s) {m_dir} {window.type} {window.name}", terminal_width) packet_list = PacketList(settings, contact_list) log_window = RxWindow(window.uid, contact_list, group_list, settings, packet_list) log_window.is_active = True log_window.message_log = message_list if message_list: if not export: clear_screen() print(title, file=f_name) print(terminal_width * '═', file=f_name) log_window.redraw(file=f_name) print("<End of log file>\n", file=f_name) else: raise FunctionReturn( f"No logged messages for {window.type} '{window.name}'.", head_clear=True) if export: f_name.close()
def group_remove(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList') -> None: """Remove member(s) from the group.""" group_id, ser_members = separate_header(cmd_data, GROUP_ID_LENGTH) purp_pub_keys = set( split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH)) try: group_name = group_list.get_group_by_id(group_id).name except StopIteration: raise FunctionReturn( f"Error: No group with ID '{b58encode(group_id)}' found.") 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_accounts_set = set(purp_pub_keys & pub_keys) removable_set = set(before_removal & ok_accounts_set) not_in_group = list(ok_accounts_set - before_removal) rejected = list(purp_pub_keys - pub_keys) removable = list(removable_set) group = group_list.get_group(group_name) group.remove_members(removable) window = window_list.get_window(group.group_id) window.remove_contacts(removable) 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) local_win = window_list.get_window(WIN_UID_LOCAL) local_win.add_new(ts, f"Removed members from group {group_name}.")
def remove_contact(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList') -> None: """Remove contact from RxM.""" rx_account = cmd_data.decode() key_list.remove_keyset(rx_account) window_list.remove_window(rx_account) if not contact_list.has_contact(rx_account): raise FunctionReturn(f"RxM has no account '{rx_account}' to remove.") nick = contact_list.get_contact(rx_account).nick contact_list.remove_contact(rx_account) message = f"Removed {nick} from contacts." box_print(message, head=1, tail=1) local_win = window_list.get_local_window() local_win.add_new(ts, message) if any([g.remove_members([rx_account]) for g in group_list]): box_print(f"Removed {rx_account} from group(s).", tail=1)
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 whisper(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: 'QueueDict', ) -> None: """\ Send a message to the contact that overrides their enabled logging setting for that message. The functionality of this feature is impossible to enforce, but if the recipient can be trusted and they do not modify their client, this feature can be used to send the message off-the-record. """ try: message = user_input.plaintext.strip().split(' ', 1)[1] except IndexError: raise FunctionReturn("Error: No whisper message specified.", head_clear=True) queue_message(user_input=UserInput(message, MESSAGE), window=window, settings=settings, queues=queues, whisper=True, log_as_ph=True)
def change_setting( self, key: str, # Name of the setting value_str: str, # Value of the setting contact_list: 'ContactList', group_list: 'GroupList') -> None: """Parse, update and store new setting value.""" attribute = self.__getattribute__(key) try: if isinstance(attribute, bool): value = dict(true=True, false=False)[ value_str.lower()] # type: Union[bool, int, float] elif isinstance(attribute, int): value = int(value_str) if value < 0 or value > MAX_INT: raise ValueError elif isinstance(attribute, float): value = float(value_str) if value < 0.0: raise ValueError else: raise CriticalError("Invalid attribute type in settings.") except (KeyError, ValueError): raise FunctionReturn( f"Error: Invalid setting value '{value_str}'.", head_clear=True) self.validate_key_value_pair(key, value, contact_list, group_list) setattr(self, key, value) self.store_settings()
def ch_master_key(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Prompt the user for a new master password and derive a new master key from that.""" try: old_master_key = master_key.master_key[:] master_key.master_key = master_key.new_master_key() phase("Re-encrypting databases") ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' if os.path.isfile(file_name): change_log_db_key(old_master_key, master_key.master_key, settings) key_list.store_keys() settings.store_settings() contact_list.store_contacts() group_list.store_groups() phase(DONE) m_print("Master password successfully changed.", bold=True, tail_clear=True, delay=1, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Changed Receiver master password.") except (EOFError, KeyboardInterrupt): raise FunctionReturn("Password change aborted.", tail_clear=True, delay=1, head=2)
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 process_received_file(payload: bytes, nick: str) -> None: """Process received file assembly packets.""" try: f_name_b, f_data = payload.split(US_BYTE) except ValueError: raise FunctionReturn("Error: Received file had invalid structure.") try: f_name = f_name_b.decode() except UnicodeError: raise FunctionReturn("Error: Received file name had invalid encoding.") if not f_name.isprintable() or not f_name: raise FunctionReturn("Error: Received file had an invalid name.") try: f_data = base64.b85decode(f_data) except (binascii.Error, ValueError): raise FunctionReturn("Error: Received file had invalid encoding.") file_ct = f_data[:-KEY_LENGTH] file_key = f_data[-KEY_LENGTH:] if len(file_key) != KEY_LENGTH: raise FunctionReturn("Error: Received file had an invalid key.") try: file_pt = auth_and_decrypt(file_ct, file_key, soft_e=True) except nacl.exceptions.CryptoError: raise FunctionReturn("Error: Decryption of file data failed.") try: file_dc = zlib.decompress(file_pt) except zlib.error: raise FunctionReturn("Error: Decompression of file data failed.") file_dir = f'{DIR_RX_FILES}{nick}/' final_name = store_unique(file_dc, file_dir, f_name) box_print(f"Stored file from {nick} as '{final_name}'")
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 validate_key_value_pair( key: str, # Name of the setting value: Union[int, float, bool], # Value of the setting contact_list: 'ContactList', group_list: 'GroupList') -> None: """Evaluate values for settings that have further restrictions.""" if key in [ 'max_number_of_group_members', 'max_number_of_groups', 'max_number_of_contacts' ]: if value % 10 != 0 or value == 0: raise FunctionReturn( "Error: Database padding settings must be divisible by 10.", head_clear=True) if key == 'max_number_of_group_members': min_size = round_up(group_list.largest_group()) if value < min_size: raise FunctionReturn( f"Error: Can't set the max number of members lower than {min_size}.", head_clear=True) if key == 'max_number_of_groups': min_size = round_up(len(group_list)) if value < min_size: raise FunctionReturn( f"Error: Can't set the max number of groups lower than {min_size}.", head_clear=True) if key == 'max_number_of_contacts': min_size = round_up(len(contact_list)) if value < min_size: raise FunctionReturn( f"Error: Can't set the max number of contacts lower than {min_size}.", head_clear=True) if key == 'new_message_notify_duration' and value < 0.05: raise FunctionReturn( "Error: Too small value for message notify duration.", head_clear=True) 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 FunctionReturn( 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 FunctionReturn( "Aborted traffic masking setting change.", head_clear=True) m_print("Traffic masking setting will change on restart.", head=1, tail=1)
def process_group_command(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Parse a group command and process it accordingly.""" if settings.traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) input_parameters = user_input.plaintext.split() # type: List[str] try: command_type = input_parameters[1] except IndexError: raise FunctionReturn("Error: Invalid group command.", head_clear=True) if command_type not in ['create', 'join', 'add', 'rm']: raise FunctionReturn("Error: Invalid group command.") group_id = None # type: Optional[bytes] if command_type == 'join': try: group_id_s = input_parameters[2] except IndexError: raise FunctionReturn("Error: No group ID specified.", head_clear=True) try: group_id = b58decode(group_id_s) except ValueError: raise FunctionReturn("Error: Invalid group ID.", head_clear=True) if group_id in group_list.get_list_of_group_ids(): raise FunctionReturn( "Error: Group with matching ID already exists.", head_clear=True) try: name_index = 3 if command_type == 'join' else 2 group_name = input_parameters[name_index] except IndexError: raise FunctionReturn("Error: No group name specified.", head_clear=True) member_index = 4 if command_type == 'join' else 3 purp_members = input_parameters[member_index:] # Swap specified strings to public keys selectors = contact_list.contact_selectors() pub_keys = [ contact_list.get_contact_by_address_or_nick(m).onion_pub_key for m in purp_members if m in selectors ] func = dict(create=group_create, join=group_create, add=group_add_member, rm=group_rm_member)[command_type] # type: Callable func(group_name, pub_keys, contact_list, group_list, settings, queues, master_key, group_id) print('')
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 yes(f"Group {group_name} was not found. Create new group?", abort=False, head=1): group_create(group_name, purp_members, contact_list, group_list, settings, queues, master_key) return None else: raise FunctionReturn("Group creation aborted.", head=0, delay=1, tail_clear=True) 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 FunctionReturn( f"Error: TFC settings only allow {settings.max_number_of_group_members} " f"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 cancel_packet(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: 'QueueDict' ) -> None: """Cancel sent message/file to contact/group. In cases where the assembly packets have not yet been encrypted or output to Networked Computer, the queued messages or files to active window can be cancelled. Any single-packet message and file this function removes from the queue/transfer buffer are unavailable to recipient. However, in the case of multi-packet transmissions, if only the last assembly packet is cancelled, the recipient might obtain large enough section of the key that protects the inner encryption layer to allow them to brute force the rest of the key, and thus, decryption of the packet. There is simply no way to prevent this kind of attack without making TFC proprietary and re-writing it in a compiled language (which is very bad for users' rights). """ header, p_type = dict(cm=(M_C_HEADER, 'messages'), cf=(F_C_HEADER, 'files' ))[user_input.plaintext] if settings.traffic_masking: queue = queues[TM_MESSAGE_PACKET_QUEUE] if header == M_C_HEADER else queues[TM_FILE_PACKET_QUEUE] else: if header == F_C_HEADER: raise FunctionReturn("Files are only queued during traffic masking.", head_clear=True) queue = queues[MESSAGE_PACKET_QUEUE] cancel_pt = header + bytes(PADDING_LENGTH) log_as_ph = False # Never log cancel assembly packets as placeholder data cancel = False if settings.traffic_masking: if queue.qsize() != 0: cancel = True # Get most recent log_messages setting status in queue log_messages = False while queue.qsize() != 0: log_messages = queue.get()[1] queue.put((cancel_pt, log_messages, log_as_ph)) m_print(f"Cancelled queues {p_type}." if cancel else f"No {p_type} to cancel.", head=1, tail=1) else: p_buffer = [] while queue.qsize() != 0: queue_data = queue.get() window_uid = queue_data[4] # Put messages unrelated to the active window into the buffer if window_uid != window.uid: p_buffer.append(queue_data) else: cancel = True # Put cancel packets for each window contact to queue first if cancel: for c in window: queue.put((cancel_pt, c.onion_pub_key, c.log_messages, log_as_ph, window.uid)) # Put buffered tuples back to the queue for p in p_buffer: queue.put(p) if cancel: message = f"Cancelled queued {p_type} to {window.type_print} {window.name}." else: message = f"No {p_type} queued for {window.type_print} {window.name}." raise FunctionReturn(message, head_clear=True)
def key_ex_psk_rx(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Import Rx-PSK of contact.""" c_code, onion_pub_key = separate_header(packet, CONFIRM_CODE_LENGTH) short_addr = pub_key_to_short_address(onion_pub_key) if not contact_list.has_pub_key(onion_pub_key): raise FunctionReturn(f"Error: Unknown account '{short_addr}'.", head_clear=True) contact = contact_list.get_contact_by_pub_key(onion_pub_key) psk_file = ask_path_gui(f"Select PSK for {contact.nick} ({short_addr})", settings, get_file=True) try: with open(psk_file, 'rb') as f: psk_data = f.read() except PermissionError: raise FunctionReturn("Error: No read permission for the PSK file.") if len(psk_data) != PSK_FILE_SIZE: raise FunctionReturn("Error: The PSK data in the file was invalid.", head_clear=True) salt, ct_tag = separate_header(psk_data, ARGON2_SALT_LENGTH) while True: try: password = MasterKey.get_password("PSK password") phase("Deriving the key decryption key", head=2) kdk = argon2_kdf(password, salt, time_cost=ARGON2_PSK_TIME_COST, memory_cost=ARGON2_PSK_MEMORY_COST) psk = auth_and_decrypt(ct_tag, kdk) phase(DONE) break except nacl.exceptions.CryptoError: print_on_previous_line() m_print("Invalid password. Try again.", head=1) print_on_previous_line(reps=5, delay=1) except (EOFError, KeyboardInterrupt): raise FunctionReturn("PSK import aborted.", head=2, delay=1, tail_clear=True) rx_mk, rx_hk = separate_header(psk, SYMMETRIC_KEY_LENGTH) if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [rx_mk, rx_hk]): raise FunctionReturn("Error: Received invalid keys from contact.", head_clear=True) keyset = key_list.get_keyset(onion_pub_key) keyset.rx_mk = rx_mk keyset.rx_hk = rx_hk key_list.store_keys() contact.kex_status = KEX_STATUS_HAS_RX_PSK contact_list.store_contacts() # Pipes protects against shell injection. Source of command's parameter is # the program itself, and therefore trusted, but it's still good practice. subprocess.Popen(f"shred -n 3 -z -u {pipes.quote(psk_file)}", shell=True).wait() if os.path.isfile(psk_file): m_print( f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.", manual_proceed=True, box=True) message = f"Added Rx-side PSK for {contact.nick} ({short_addr})." local_win = window_list.get_local_window() local_win.add_new(ts, message) m_print([ message, '', "Warning!", "Physically destroy the keyfile transmission media ", "to ensure it does not steal data from this computer!", '', f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1, tail=1)
def process_message(ts: 'datetime', assembly_packet_ct: bytes, window_list: 'WindowList', packet_list: 'PacketList', contact_list: 'ContactList', key_list: 'KeyList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', file_keys: Dict[bytes, bytes]) -> None: """Process received private / group message.""" local_window = window_list.get_local_window() onion_pub_key, origin, assembly_packet_ct = separate_headers( assembly_packet_ct, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH]) if onion_pub_key == LOCAL_PUBKEY: raise FunctionReturn( "Warning! Received packet masqueraded as a command.", window=local_window) if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]: raise FunctionReturn( "Error: Received packet had an invalid origin-header.", window=local_window) assembly_packet = decrypt_assembly_packet(assembly_packet_ct, onion_pub_key, origin, window_list, contact_list, key_list) p_type = FILE if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH].isupper( ) else MESSAGE packet = packet_list.get_packet(onion_pub_key, origin, p_type) logging = contact_list.get_contact_by_pub_key(onion_pub_key).log_messages def log_masking_packets(completed: bool = False) -> None: """Add masking packets to log file. If logging and log file masking are enabled, this function will in case of erroneous transmissions, store the correct number of placeholder data packets to log file to hide the quantity of communication that log file observation would otherwise reveal. """ if logging and settings.log_file_masking and (packet.log_masking_ctr or completed): no_masking_packets = len(packet.assembly_pt_list ) if completed else packet.log_masking_ctr for _ in range(no_masking_packets): write_log_entry(PLACEHOLDER_DATA, onion_pub_key, settings, master_key, origin) packet.log_masking_ctr = 0 try: packet.add_packet(assembly_packet) except FunctionReturn: log_masking_packets() raise log_masking_packets() if not packet.is_complete: return None try: if p_type == FILE: packet.assemble_and_store_file(ts, onion_pub_key, window_list) raise FunctionReturn( "File storage complete.", output=False) # Raising allows calling log_masking_packets elif p_type == MESSAGE: whisper_byte, header, assembled = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if len(whisper_byte) != WHISPER_FIELD_LENGTH: raise FunctionReturn( "Error: Message from contact had an invalid whisper header." ) whisper = bytes_to_bool(whisper_byte) if header == GROUP_MESSAGE_HEADER: logging = process_group_message(assembled, ts, onion_pub_key, origin, whisper, group_list, window_list) elif header == PRIVATE_MESSAGE_HEADER: window = window_list.get_window(onion_pub_key) window.add_new(ts, assembled.decode(), onion_pub_key, origin, output=True, whisper=whisper) elif header == FILE_KEY_HEADER: nick = process_file_key_message(assembled, onion_pub_key, origin, contact_list, file_keys) raise FunctionReturn( f"Received file decryption key from {nick}", window=local_window) else: raise FunctionReturn( "Error: Message from contact had an invalid header.") # Logging if whisper: raise FunctionReturn("Whisper message complete.", output=False) if logging: for p in packet.assembly_pt_list: write_log_entry(p, onion_pub_key, settings, master_key, origin) except (FunctionReturn, UnicodeError): log_masking_packets(completed=True) raise finally: packet.clear_assembly_packets()
def ch_contact_s(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', header: bytes) -> None: """Change contact/group related setting.""" setting, win_uid = separate_header(cmd_data, CONTACT_SETTING_HEADER_LENGTH) attr, desc, file_cmd = { CH_LOGGING: ('log_messages', "Logging of messages", False), CH_FILE_RECV: ('file_reception', "Reception of files", True), CH_NOTIFY: ('notifications', "Message notifications", False) }[header] action, b_value = { ENABLE: ('enabled', True), DISABLE: ('disabled', False) }[setting.lower()] if setting.isupper(): # Change settings for all contacts (and groups) enabled = [ getattr(c, attr) for c in contact_list.get_list_of_contacts() ] enabled += [getattr(g, attr) for g in group_list] if not file_cmd else [] status = "was already" if ( (all(enabled) and b_value) or (not any(enabled) and not b_value)) else "has been" specifier = "every " w_type = "contact" w_name = "." if file_cmd else " and group." # Set values for c in contact_list.get_list_of_contacts(): setattr(c, attr, b_value) contact_list.store_contacts() if not file_cmd: for g in group_list: setattr(g, attr, b_value) group_list.store_groups() else: # Change setting for contacts in specified window if not window_list.has_window(win_uid): raise FunctionReturn( f"Error: Found no window for '{pub_key_to_short_address(win_uid)}'." ) window = window_list.get_window(win_uid) group_window = window.type == WIN_TYPE_GROUP contact_window = window.type == WIN_TYPE_CONTACT if contact_window: target = contact_list.get_contact_by_pub_key( win_uid) # type: Union[Contact, Group] else: target = group_list.get_group_by_id(win_uid) if file_cmd: enabled = [getattr(m, attr) for m in window.window_contacts] changed = not all(enabled) if b_value else any(enabled) else: changed = getattr(target, attr) != b_value status = "has been" if changed else "was already" specifier = "members in " if (file_cmd and group_window) else '' w_type = window.type w_name = f" {window.name}." # Set values if contact_window or (group_window and file_cmd): for c in window.window_contacts: setattr(c, attr, b_value) contact_list.store_contacts() elif group_window: setattr(group_list.get_group_by_id(win_uid), attr, b_value) group_list.store_groups() message = f"{desc} {status} {action} for {specifier}{w_type}{w_name}" local_win = window_list.get_local_window() local_win.add_new(ts, message, output=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: 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 print_settings(self) -> None: """\ Print list of settings, their current and default values, and setting descriptions. """ desc_d = { # Common settings "disable_gui_dialog": "True replaces GUI dialogs with CLI prompts", "max_number_of_group_members": "Maximum number of members in a group", "max_number_of_groups": "Maximum number of groups", "max_number_of_contacts": "Maximum number of contacts", "log_messages_by_default": "Default logging setting for new contacts/groups", "accept_files_by_default": "Default file reception setting for new contacts", "show_notifications_by_default": "Default message notification setting for new contacts/groups", "log_file_masking": "True hides real size of log file during traffic masking", "ask_password_for_log_access": "False disables password prompt when viewing/exporting logs", # Transmitter settings "nc_bypass_messages": "False removes Networked Computer bypass interrupt messages", "confirm_sent_files": "False sends files without asking for confirmation", "double_space_exits": "True exits, False clears screen with double space command", "traffic_masking": "True enables traffic masking to hide metadata", "tm_static_delay": "The static delay between traffic masking packets", "tm_random_delay": "Max random delay for traffic masking timing obfuscation", # Relay settings "allow_contact_requests": "When False, does not show TFC contact requests", # Receiver settings "new_message_notify_preview": "When True, shows a preview of the received message", "new_message_notify_duration": "Number of seconds new message notification appears", "max_decompress_size": "Max size Receiver accepts when decompressing file" } # Columns c1 = ['Setting name'] c2 = ['Current value'] c3 = ['Default value'] c4 = ['Description'] terminal_width = get_terminal_width() description_indent = 64 if terminal_width < description_indent + 1: raise FunctionReturn("Error: Screen width is too small.", head_clear=True) # Populate columns with setting data for key in self.defaults: c1.append(key) c2.append(str(self.__getattribute__(key))) c3.append(str(self.defaults[key])) description = desc_d[key] wrapper = textwrap.TextWrapper( width=max(1, (terminal_width - description_indent))) desc_lines = wrapper.fill(description).split('\n') desc_string = desc_lines[0] for line in desc_lines[1:]: desc_string += '\n' + description_indent * ' ' + line if len(desc_lines) > 1: desc_string += '\n' c4.append(desc_string) # Calculate column widths c1w, c2w, c3w = [ max(len(v) for v in column) + SETTINGS_INDENT for column in [c1, c2, c3] ] # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}} {f2:{c2w}} {f3:{c3w}} {f4}' for f1, f2, f3, f4 in zip(c1, c2, c3, c4) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the settings clear_screen() print('\n' + '\n'.join(lines))
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', kdk_hashes: List[bytes], packet_hashes: List[bytes], l_queue: 'Queue') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_keyset() plaintext = None try: packet_hash = blake2b(packet) # Check if the packet is an old one if packet_hash in packet_hashes: raise FunctionReturn("Error: Received old local key packet.", output=False) while True: m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) kdk_hash = blake2b(kdk) try: plaintext = auth_and_decrypt(packet, kdk) break except nacl.exceptions.CryptoError: # Check if key was an old one if kdk_hash in kdk_hashes: m_print("Error: Entered an old local key decryption key.", delay=1) continue # Check if the kdk was for a packet further ahead in the queue buffer = [] # type: List[Tuple[datetime, bytes]] while l_queue.qsize() > 0: tup = l_queue.get() # type: Tuple[datetime, bytes] if tup not in buffer: buffer.append(tup) for i, tup in enumerate(buffer): try: plaintext = auth_and_decrypt(tup[1], kdk) # If we reach this point, decryption was successful. for unexamined in buffer[i + 1:]: l_queue.put(unexamined) buffer = [] ts = tup[0] break except nacl.exceptions.CryptoError: continue else: # Finished the buffer without finding local key CT # for the kdk. Maybe the kdk is from another session. raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1) break # This catches PyCharm's weird claim that plaintext might be referenced before assignment if plaintext is None: # pragma: no cover raise FunctionReturn("Error: Could not decrypt local key.") # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, KEX_STATUS_LOCAL_KEY, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), False, False, True) tx_mk, tx_hk, c_code = separate_headers(plaintext, 2 * [SYMMETRIC_KEY_LENGTH]) # Add local keyset to keyset database key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY, tx_mk=tx_mk, rx_mk=csprng(), tx_hk=tx_hk, rx_hk=csprng()) # Cache hashes needed to recognize reissued local key packets and key decryption keys. packet_hashes.append(packet_hash) kdk_hashes.append(kdk_hash) # Prevent leak of KDK via terminal history / clipboard readline.clear_history() os.system(RESET) root = tkinter.Tk() root.withdraw() try: if root.clipboard_get() == b58encode(kdk): root.clipboard_clear() except tkinter.TclError: pass root.destroy() m_print([ "Local key successfully installed.", f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Added new local key.") if bootstrap: window_list.active_win = local_win except (EOFError, KeyboardInterrupt): m_print("Local key setup aborted.", bold=True, tail_clear=True, delay=1, head=2) if window_list.active_win is not None and not bootstrap: window_list.active_win.redraw() raise FunctionReturn("Local key setup aborted.", output=False)
def send_file(path: str, settings: 'Settings', queues: 'QueueDict', window: 'TxWindow' ) -> None: """Send file to window members in a single transmission. This is the default mode for file transmission, used when traffic masking is not enabled. The file is loaded and compressed before it is encrypted. The encrypted file is then exported to Networked Computer along with a list of Onion Service public keys (members in window) of all recipients to whom the Relay Program will multi-cast the file to. Once the file ciphertext has been exported, this function will multi-cast the file decryption key to each recipient inside an automated key delivery message that uses a special FILE_KEY_HEADER in place of standard PRIVATE_MESSAGE_HEADER. To know for which file ciphertext the key is for, an identifier must be added to the key delivery message. The identifier in this case is the BLAKE2b digest of the ciphertext itself. The reason of using the digest as the identifier is, it authenticates both the ciphertext and its origin. To understand this, consider the following attack scenario: Let the file ciphertext identifier be just a random 32-byte value "ID". 1) Alice sends Bob and Chuck (a malicious common peer) a file ciphertext and identifier CT|ID (where | denotes concatenation). 2) Chuck who has compromised Bob's Networked Computer interdicts the CT|ID from Alice. 3) Chuck decrypts CT in his end, makes edits to the plaintext PT to create PT'. 4) Chuck re-encrypts PT' with the same symmetric key to produce CT'. 5) Chuck re-uses the ID and produces CT'|ID. 6) Chuck uploads the CT'|ID to Bob's Networked Computer and replaces the interdicted CT|ID with it. 7) When Bob' Receiver Program receives the automated key delivery message from Alice, his Receiver program uses the bundled ID to identify the key is for CT'. 8) Bob's Receiver decrypts CT' using the newly received key and obtains Chuck's PT', that appears to come from Alice. Now, consider a situation where the ID is instead calculated ID = BLAKE2b(CT), if Chuck edits the PT, the CT' will by definition be different from CT, and the BLAKE2b digest will also be different. In order to make Bob decrypt CT', Chuck needs to also change the hash in Alice's key delivery message, which means Chuck needs to create an existential forgery of the TFC message. Since the Poly1305 tag prevents this, the calculated ID is enough to authenticate the ciphertext. If Chuck attempts to send their own key delivery message, Chuck's own Onion Service public key used to identify the TFC message key (decryption key for the key delivery message) will be permanently associated with the file hash, so if they inject a file CT, and Bob has decided to enable file reception for Chuck, the file CT will appear to come from Chuck, and not from Alice. From the perspective of Bob, it's as if Chuck had dropped Alice's file and sent him another file instead. """ from src.transmitter.windows import MockWindow # Avoid circular import if settings.traffic_masking: raise FunctionReturn("Error: Command is disabled during traffic masking.", head_clear=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.", head_clear=True) if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty.", head_clear=True) phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase(DONE) print_on_previous_line(flush=True) phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL)) phase(DONE) print_on_previous_line(flush=True) phase("Encrypting data") file_key = csprng() file_ct = encrypt_and_sign(comp, file_key) ct_hash = blake2b(file_ct) phase(DONE) print_on_previous_line(flush=True) phase("Exporting data") no_contacts = int_to_bytes(len(window)) ser_contacts = b''.join([c.onion_pub_key for c in window]) file_packet = FILE_DATAGRAM_HEADER + no_contacts + ser_contacts + file_ct queue_to_nc(file_packet, queues[RELAY_PACKET_QUEUE]) key_delivery_msg = base64.b85encode(ct_hash + file_key).decode() for contact in window: queue_message(user_input=UserInput(key_delivery_msg, MESSAGE), window =MockWindow(contact.onion_pub_key, [contact]), settings =settings, queues =queues, header =FILE_KEY_HEADER, log_as_ph =True) phase(DONE) print_on_previous_line(flush=True) m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
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 process_command(ts: 'datetime', assembly_ct: bytes, window_list: 'WindowList', packet_list: 'PacketList', contact_list: 'ContactList', key_list: 'KeyList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', gateway: 'Gateway', exit_queue: 'Queue[bytes]') -> None: """Decrypt command assembly packet and process command.""" assembly_packet = decrypt_assembly_packet(assembly_ct, LOCAL_PUBKEY, ORIGIN_USER_HEADER, window_list, contact_list, key_list) cmd_packet = packet_list.get_packet(LOCAL_PUBKEY, ORIGIN_USER_HEADER, COMMAND) cmd_packet.add_packet(assembly_packet) if not cmd_packet.is_complete: raise FunctionReturn("Incomplete command.", output=False) header, cmd = separate_header(cmd_packet.assemble_command_packet(), ENCRYPTED_COMMAND_HEADER_LENGTH) no = None # Keyword Function to run ( Parameters ) # -------------------------------------------------------------------------------------------------------------- d = { LOCAL_KEY_RDY: (local_key_rdy, ts, window_list, contact_list), WIN_ACTIVITY: (win_activity, window_list), WIN_SELECT: (win_select, cmd, window_list), CLEAR_SCREEN: (clear_screen, ), RESET_SCREEN: (reset_screen, cmd, window_list), EXIT_PROGRAM: (exit_tfc, exit_queue), LOG_DISPLAY: (log_command, cmd, no, window_list, contact_list, group_list, settings, master_key), LOG_EXPORT: (log_command, cmd, ts, window_list, contact_list, group_list, settings, master_key), LOG_REMOVE: (remove_log, cmd, contact_list, group_list, settings, master_key), CH_MASTER_KEY: (ch_master_key, ts, window_list, contact_list, group_list, key_list, settings, master_key), CH_NICKNAME: ( ch_nick, cmd, ts, window_list, contact_list, ), CH_SETTING: (ch_setting, cmd, ts, window_list, contact_list, group_list, key_list, settings, gateway), CH_LOGGING: (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header), CH_FILE_RECV: (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header), CH_NOTIFY: (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header), GROUP_CREATE: (group_create, cmd, ts, window_list, contact_list, group_list, settings), GROUP_ADD: (group_add, cmd, ts, window_list, contact_list, group_list, settings), GROUP_REMOVE: (group_remove, cmd, ts, window_list, contact_list, group_list), GROUP_DELETE: (group_delete, cmd, ts, window_list, group_list), GROUP_RENAME: (group_rename, cmd, ts, window_list, contact_list, group_list), KEY_EX_ECDHE: (key_ex_ecdhe, cmd, ts, window_list, contact_list, key_list, settings), KEY_EX_PSK_TX: (key_ex_psk_tx, cmd, ts, window_list, contact_list, key_list, settings), KEY_EX_PSK_RX: (key_ex_psk_rx, cmd, ts, window_list, contact_list, key_list, settings), CONTACT_REM: (contact_rem, cmd, ts, window_list, contact_list, group_list, key_list, settings, master_key), WIPE_USR_DATA: (wipe, exit_queue) } # type: Dict[bytes, Any] try: from_dict = d[header] except KeyError: raise FunctionReturn("Error: Received an invalid command.") func = from_dict[0] parameters = from_dict[1:] func(*parameters)
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)