def add_complete_message_to_message_list( timestamp: bytes, onion_pub_key: bytes, group_msg_id: bytes, packet: 'Packet', message_list: List[MsgTuple], window: Union['TxWindow', 'RxWindow']) -> bytes: """Add complete log file message to `message_list`.""" whisper_byte, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) whisper = bytes_to_bool(whisper_byte) if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT: message_list.append((bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP: purp_group_id, message = separate_header(message, GROUP_ID_LENGTH) if window.group is not None and purp_group_id != window.group.group_id: return group_msg_id purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH) if packet.origin == ORIGIN_USER_HEADER: if purp_msg_id == group_msg_id: return group_msg_id group_msg_id = purp_msg_id message_list.append((bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) return group_msg_id
def src_incoming(queues: 'QueueDict', gateway: 'Gateway', unittest: bool = False ) -> None: """\ Redirect messages received from Source Computer to appropriate queues. """ packets_from_sc = queues[GATEWAY_QUEUE] packets_to_dc = queues[DST_MESSAGE_QUEUE] commands_to_dc = queues[DST_COMMAND_QUEUE] messages_to_flask = queues[M_TO_FLASK_QUEUE] files_to_flask = queues[F_TO_FLASK_QUEUE] commands_to_relay = queues[SRC_TO_RELAY_QUEUE] while True: with ignored(EOFError, KeyboardInterrupt): while packets_from_sc.qsize() == 0: time.sleep(0.01) ts, packet = packets_from_sc.get() # type: datetime, bytes ts_bytes = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) try: packet = gateway.detect_errors(packet) except FunctionReturn: continue header, packet = separate_header(packet, DATAGRAM_HEADER_LENGTH) if header == UNENCRYPTED_DATAGRAM_HEADER: commands_to_relay.put(packet) elif header in [COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER]: commands_to_dc.put(header + ts_bytes + packet) p_type = 'Command ' if header == COMMAND_DATAGRAM_HEADER else 'Local key' rp_print(f"{p_type} to local Receiver", ts) elif header in [MESSAGE_DATAGRAM_HEADER, PUBLIC_KEY_DATAGRAM_HEADER]: onion_pub_key, payload = separate_header(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) packet_str = header.decode() + b85encode(payload) queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) if header == MESSAGE_DATAGRAM_HEADER: packets_to_dc.put(header + ts_bytes + onion_pub_key + ORIGIN_USER_HEADER + payload) elif header == FILE_DATAGRAM_HEADER: no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH) no_contacts = bytes_to_int(no_contacts_b) ser_accounts, file_ct = separate_header(payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH) pub_keys = split_byte_string(ser_accounts, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: queue_to_flask(file_ct, onion_pub_key, files_to_flask, ts, header) elif header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_EXIT_GROUP_HEADER]: process_group_management_message(ts, packet, header, messages_to_flask) if unittest: break
def process_group_message( ts: 'datetime', # Timestamp of group message assembled: bytes, # Group message and its headers onion_pub_key: bytes, # Onion address of associated contact origin: bytes, # Origin of group message (user / contact) whisper: bool, # When True, message is not logged. group_list: 'GroupList', # GroupList object window_list: 'WindowList' # WindowList object ) -> bool: """Process a group message.""" group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH) if not group_list.has_group_id(group_id): raise SoftError("Error: Received message to an unknown group.", output=False) group = group_list.get_group_by_id(group_id) if not group.has_member(onion_pub_key): raise SoftError("Error: Account is not a member of the group.", output=False) group_msg_id, group_message = separate_header(assembled, GROUP_MSG_ID_LENGTH) try: group_message_str = group_message.decode() except UnicodeError: raise SoftError("Error: Received an invalid group message.") window = window_list.get_window(group.group_id) # All copies of group messages the user sends to members contain # the same message ID. This allows the Receiver Program to ignore # duplicates of outgoing messages sent by the user to each member. if origin == ORIGIN_USER_HEADER: if window.group_msg_id != group_msg_id: window.group_msg_id = group_msg_id window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) elif origin == ORIGIN_CONTACT_HEADER: window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) # Return the group's logging setting because it might be different # from the logging setting of the contact who sent group message. return group.log_messages
def process_file_datagram(ts: 'datetime', packet: bytes, header: bytes, queues: 'QueueDict') -> None: """Process file datagram.""" files_to_flask = queues[F_TO_FLASK_QUEUE] no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH) no_contacts = bytes_to_int(no_contacts_b) ser_accounts, file_ct = separate_header( payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH) pub_keys = split_byte_string(ser_accounts, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: queue_to_flask(file_ct, onion_pub_key, files_to_flask, ts, header)
def process_group_management_message( ts: 'datetime', packet: bytes, header: bytes, messages_to_flask: 'Queue[Tuple[Union[bytes, str], bytes]]') -> None: """Parse and display group management message.""" header_str = header.decode() group_id, packet = separate_header(packet, GROUP_ID_LENGTH) if header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER]: pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: others = [k for k in pub_keys if k != onion_pub_key] packet_str = header_str + b85encode(group_id + b''.join(others)) queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) elif header in [GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER]: first_list_len_b, packet = separate_header(packet, ENCODED_INTEGER_LENGTH) first_list_length = bytes_to_int(first_list_len_b) pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) before_adding = remaining = pub_keys[:first_list_length] new_in_group = removable = pub_keys[first_list_length:] if header == GROUP_MSG_MEMBER_ADD_HEADER: packet_str = GROUP_MSG_MEMBER_ADD_HEADER.decode() + b85encode( group_id + b''.join(new_in_group)) for onion_pub_key in before_adding: queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) for onion_pub_key in new_in_group: other_new = [k for k in new_in_group if k != onion_pub_key] packet_str = ( GROUP_MSG_INVITE_HEADER.decode() + b85encode(group_id + b''.join(other_new + before_adding))) queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) elif header == GROUP_MSG_MEMBER_REM_HEADER: packet_str = header_str + b85encode(group_id + b''.join(removable)) for onion_pub_key in remaining: queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) elif header == GROUP_MSG_EXIT_GROUP_HEADER: pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) packet_str = header_str + b85encode(group_id) for onion_pub_key in pub_keys: queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header)
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(): status, specifier, w_type, w_name = change_setting_for_all_contacts( attr, file_cmd, b_value, contact_list, group_list) else: status, specifier, w_type, w_name = change_setting_for_one_contact( attr, file_cmd, b_value, win_uid, window_list, contact_list, group_list) message = f"{desc} {status} {action} for {specifier}{w_type}{w_name}" cmd_win = window_list.get_command_window() cmd_win.add_new(ts, message, output=True)
def process_file_key_message( assembled: bytes, # File decryption key onion_pub_key: bytes, # Onion address of associated contact origin: bytes, # Origin of file key packet (user / contact) contact_list: 'ContactList', # ContactList object file_keys: Dict[ bytes, bytes] # Dictionary of file identifiers and decryption keys ) -> str: """Process received file key delivery message.""" if origin == ORIGIN_USER_HEADER: raise FunctionReturn("File key message from the user.", output=False) try: decoded = base64.b85decode(assembled) except ValueError: raise FunctionReturn("Error: Received an invalid file key message.") ct_hash, file_key = separate_header(decoded, BLAKE2_DIGEST_LENGTH) if len(ct_hash) != BLAKE2_DIGEST_LENGTH or len( file_key) != SYMMETRIC_KEY_LENGTH: raise FunctionReturn("Error: Received an invalid file key message.") file_keys[onion_pub_key + ct_hash] = file_key nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick return nick
def ch_nick(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList') -> None: """Change nickname of contact.""" onion_pub_key, nick_bytes = separate_header( cmd_data, header_length=ONION_SERVICE_PUBLIC_KEY_LENGTH) nick = nick_bytes.decode() short_addr = pub_key_to_short_address(onion_pub_key) try: contact = contact_list.get_contact_by_pub_key(onion_pub_key) except StopIteration: raise FunctionReturn( f"Error: Receiver has no contact '{short_addr}' to rename.") contact.nick = nick contact_list.store_contacts() window = window_list.get_window(onion_pub_key) window.name = nick window.handle_dict[onion_pub_key] = nick if window.type == WIN_TYPE_CONTACT: window.redraw() cmd_win = window_list.get_local_window() cmd_win.add_new(ts, f"Changed {short_addr} nick to '{nick}'.", output=True)
def log_command(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey') -> None: """Display or export log file for 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. """ export = ts is not None ser_no_msg, uid = separate_header(cmd_data, ENCODED_INTEGER_LENGTH) no_messages = bytes_to_int(ser_no_msg) window = window_list.get_window(uid) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load=no_messages, export=export) if export: local_win = window_list.get_local_window() local_win.add_new( ts, f"Exported log file of {window.type} '{window.name}'.", output=True)
def g_msg_manager(queues: 'QueueDict', unit_test: bool = False) -> None: """Show group management messages according to contact list state. This process keeps track of existing contacts for whom there's a `client` process. When a group management message from a contact is received, existing contacts are displayed under "known contacts", and non-existing contacts are displayed under "unknown contacts". """ existing_contacts = [] # type: List[bytes] group_management_queue = queues[GROUP_MGMT_QUEUE] while True: with ignored(EOFError, KeyboardInterrupt): while queues[GROUP_MSG_QUEUE].qsize() == 0: time.sleep(0.01) header, payload, trunc_addr = queues[GROUP_MSG_QUEUE].get() group_id, data = separate_header(payload, GROUP_ID_LENGTH) if len(group_id) != GROUP_ID_LENGTH: continue group_id_hr = b58encode(group_id) existing_contacts = update_list_of_existing_contacts( group_management_queue, existing_contacts) process_group_management_message(data, existing_contacts, group_id_hr, header, trunc_addr) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: break
def process_file_datagram( ts: 'datetime', packet: bytes, header: bytes, buf_key: bytes, ) -> None: """Process file datagram.""" no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH) no_contacts = bytes_to_int(no_contacts_b) ser_accounts, file_ct = separate_header( payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH) pub_keys = split_byte_string(ser_accounts, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: buffer_to_flask(file_ct, onion_pub_key, ts, header, buf_key, file=True)
def process_command(command: bytes, gateway: 'Gateway', queues: 'QueueDict' ) -> None: """Select function for received Relay Program command.""" header, command = separate_header(command, UNENCRYPTED_COMMAND_HEADER_LENGTH) # Keyword Function to run ( Parameters ) # --------------------------------------------------------------------------------- function_d = {UNENCRYPTED_SCREEN_CLEAR: (clear_windows, gateway, ), UNENCRYPTED_SCREEN_RESET: (reset_windows, gateway, ), UNENCRYPTED_EXIT_COMMAND: (exit_tfc, gateway, queues), UNENCRYPTED_WIPE_COMMAND: (wipe, gateway, queues), UNENCRYPTED_EC_RATIO: (change_ec_ratio, command, gateway, ), UNENCRYPTED_BAUDRATE: (change_baudrate, command, gateway, ), UNENCRYPTED_MANAGE_CONTACT_REQ: (manage_contact_req, command, queues), UNENCRYPTED_ADD_NEW_CONTACT: (add_contact, command, False, queues), UNENCRYPTED_ADD_EXISTING_CONTACT: (add_contact, command, True, queues), UNENCRYPTED_REM_CONTACT: (remove_contact, command, queues), UNENCRYPTED_ONION_SERVICE_DATA: (add_onion_data, command, queues) } # type: Dict[bytes, Any] if header not in function_d: raise FunctionReturn("Error: Received an invalid command.") from_dict = function_d[header] func = from_dict[0] parameters = from_dict[1:] func(*parameters)
def group_rename(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList') -> None: """Rename the group.""" group_id, new_name_bytes = separate_header(cmd_data, GROUP_ID_LENGTH) try: group = group_list.get_group_by_id(group_id) except StopIteration: raise SoftError( f"Error: No group with ID '{b58encode(group_id)}' found.") try: new_name = new_name_bytes.decode() except UnicodeError: raise SoftError( f"Error: New name for group '{group.name}' was invalid.") error_msg = validate_group_name(new_name, contact_list, group_list) if error_msg: raise SoftError(error_msg) old_name = group.name group.name = new_name group_list.store_groups() window = window_list.get_window(group.group_id) window.name = new_name message = f"Renamed group '{old_name}' to '{new_name}'." cmd_win = window_list.get_window(WIN_UID_COMMAND) cmd_win.add_new(ts, message, output=True)
def compare_pub_keys(command: bytes, queues: 'QueueDict') -> None: """\ Compare incorrectly typed public key to what's available on Relay Program. """ account, incorrect_pub_key = separate_header(command, ONION_SERVICE_PUBLIC_KEY_LENGTH) queues[PUB_KEY_CHECK_QUEUE].put((account, incorrect_pub_key))
def process_group_message(assembled: bytes, ts: 'datetime', onion_pub_key: bytes, origin: bytes, whisper: bool, group_list: 'GroupList', window_list: 'WindowList') -> bool: """Process a group message.""" group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH) if not group_list.has_group_id(group_id): raise FunctionReturn("Error: Received message to an unknown group.", output=False) group = group_list.get_group_by_id(group_id) if not group.has_member(onion_pub_key): raise FunctionReturn("Error: Account is not a member of the group.", output=False) group_msg_id, group_message = separate_header(assembled, GROUP_MSG_ID_LENGTH) try: group_message_str = group_message.decode() except UnicodeError: raise FunctionReturn("Error: Received an invalid group message.") window = window_list.get_window(group.group_id) # All copies of group messages the user sends to members contain # the same message ID. This allows the Receiver Program to ignore # duplicates of outgoing messages sent by the user to each member. if origin == ORIGIN_USER_HEADER: if window.group_msg_id != group_msg_id: window.group_msg_id = group_msg_id window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) elif origin == ORIGIN_CONTACT_HEADER: window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) return group.log_messages
def process_group_management_message( ts: 'datetime', packet: bytes, header: bytes, buf_key: bytes, ) -> None: """Parse and display group management message.""" header_str = header.decode() group_id, packet = separate_header(packet, GROUP_ID_LENGTH) if header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER]: pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: others = [k for k in pub_keys if k != onion_pub_key] packet_str = header_str + b85encode(group_id + b''.join(others)) buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key) elif header in [GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER]: first_list_len_b, packet = separate_header(packet, ENCODED_INTEGER_LENGTH) first_list_length = bytes_to_int(first_list_len_b) pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) before_adding = remaining = pub_keys[:first_list_length] new_in_group = removable = pub_keys[first_list_length:] if header == GROUP_MSG_MEMBER_ADD_HEADER: process_add_or_group_remove_member(ts, header, buf_key, header_str, group_id, before_adding, new_in_group) for onion_pub_key in new_in_group: other_new = [k for k in new_in_group if k != onion_pub_key] packet_str = ( GROUP_MSG_INVITE_HEADER.decode() + b85encode(group_id + b''.join(other_new + before_adding))) buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key) elif header == GROUP_MSG_MEMBER_REM_HEADER: process_add_or_group_remove_member(ts, header, buf_key, header_str, group_id, remaining, removable) elif header == GROUP_MSG_EXIT_GROUP_HEADER: process_group_exit_header(ts, packet, header, buf_key, header_str, group_id)
def decrypt_assembly_packet(packet: bytes, # Assembly packet ciphertext onion_pub_key: bytes, # Onion Service pubkey of associated contact origin: bytes, # Direction of packet window_list: 'WindowList', # WindowList object contact_list: 'ContactList', # ContactList object key_list: 'KeyList' # Keylist object ) -> bytes: # Decrypted assembly packet """Decrypt assembly packet from contact/local Transmitter.""" ct_harac, ct_assemby_packet = separate_header(packet, header_length=HARAC_CT_LENGTH) local_window = window_list.get_local_window() command = onion_pub_key == LOCAL_PUBKEY p_type = "command" if command else "packet" direction = "from" if command or (origin == ORIGIN_CONTACT_HEADER) else "sent to" nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick # Load keys keyset = key_list.get_keyset(onion_pub_key) key_dir = TX if origin == ORIGIN_USER_HEADER else RX header_key = getattr(keyset, f'{key_dir}_hk') # type: bytes message_key = getattr(keyset, f'{key_dir}_mk') # type: bytes if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [header_key, message_key]): raise FunctionReturn("Warning! Loaded zero-key for packet decryption.") # Decrypt hash ratchet counter try: harac_bytes = auth_and_decrypt(ct_harac, header_key) except nacl.exceptions.CryptoError: raise FunctionReturn( f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=local_window) # Catch up with hash ratchet offset purp_harac = bytes_to_int(harac_bytes) stored_harac = getattr(keyset, f'{key_dir}_harac') offset = purp_harac - stored_harac if offset < 0: raise FunctionReturn( f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=local_window) process_offset(offset, origin, direction, nick, local_window) for harac in range(stored_harac, stored_harac + offset): message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH) # Decrypt packet try: assembly_packet = auth_and_decrypt(ct_assemby_packet, message_key) except nacl.exceptions.CryptoError: raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=local_window) # Update message key and harac keyset.update_mk(key_dir, blake2b(message_key + int_to_bytes(stored_harac + offset), digest_size=SYMMETRIC_KEY_LENGTH), offset + 1) return assembly_packet
def process_message_datagram(ts: 'datetime', packet: bytes, header: bytes, buf_key: bytes, queues: 'QueueDict') -> None: """Process message or public key datagram.""" packets_to_dst = queues[DST_MESSAGE_QUEUE] onion_pub_key, payload = separate_header(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) packet_str = header.decode() + b85encode(payload) ts_bytes = int_to_bytes(int(ts.strftime("%Y%m%d%H%M%S%f")[:-4])) buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key) if header == MESSAGE_DATAGRAM_HEADER: packets_to_dst.put(header + ts_bytes + onion_pub_key + ORIGIN_USER_HEADER + payload)
def group_add(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings') -> None: """Add member(s) to 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 SoftError( f"Error: No group with ID '{b58encode(group_id)}' found.") 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_accounts = set(pub_keys & purp_pub_keys) new_in_group_set = set(ok_accounts - before_adding) end_assembly = list(before_adding | new_in_group_set) already_in_g = list(purp_pub_keys & before_adding) rejected = list(purp_pub_keys - pub_keys) new_in_group = list(new_in_group_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." ) group = group_list.get_group(group_name) group.add_members( [contact_list.get_contact_by_pub_key(k) for k in new_in_group]) window = window_list.get_window(group.group_id) window.add_contacts(new_in_group) window.create_handle_dict() 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) cmd_win = window_list.get_window(WIN_UID_COMMAND) cmd_win.add_new(ts, f"Added members to group {group_name}.")
def src_incoming(queues: 'QueueDict', gateway: 'Gateway', unit_test: bool = False) -> None: """\ Redirect datagrams received from Source Computer to appropriate queues. """ commands_to_relay = queues[SRC_TO_RELAY_QUEUE] buf_key_queue = queues[TX_BUF_KEY_QUEUE] buf_key = None while True: with ignored(EOFError, KeyboardInterrupt, SoftError): if buf_key is None and buf_key_queue.qsize() > 0: buf_key = buf_key_queue.get() ts, packet = load_packet_from_queue(queues, gateway) header, packet = separate_header(packet, DATAGRAM_HEADER_LENGTH) if header == UNENCRYPTED_DATAGRAM_HEADER: commands_to_relay.put(packet) elif header in [ COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER ]: process_command_datagram(ts, packet, header, queues) elif header in [ MESSAGE_DATAGRAM_HEADER, PUBLIC_KEY_DATAGRAM_HEADER ] and buf_key is not None: process_message_datagram(ts, packet, header, buf_key, queues) elif header == FILE_DATAGRAM_HEADER and buf_key is not None: process_file_datagram(ts, packet, header, buf_key) elif header in [ GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_EXIT_GROUP_HEADER ] and buf_key is not None: process_group_management_message(ts, packet, header, buf_key) if unit_test: break
def group_create(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings') -> None: """Create a new group.""" group_id, variable_len_data = separate_header(cmd_data, GROUP_ID_LENGTH) group_name_bytes, ser_members = variable_len_data.split(US_BYTE, 1) group_name = group_name_bytes.decode() purp_pub_keys = set( split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH)) pub_keys = set(contact_list.get_list_of_pub_keys()) accepted = list(purp_pub_keys & pub_keys) rejected = list(purp_pub_keys - pub_keys) if len(accepted) > settings.max_number_of_group_members: raise SoftError( 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 SoftError( f"Error: TFC settings only allow {settings.max_number_of_groups} groups." ) accepted_contacts = [ contact_list.get_contact_by_pub_key(k) for k in accepted ] group_list.add_group(group_name, group_id, settings.log_messages_by_default, settings.show_notifications_by_default, accepted_contacts) group = group_list.get_group(group_name) window = window_list.get_window(group.group_id) window.window_contacts = accepted_contacts window.message_log = [] window.unread_messages = 0 window.create_handle_dict() group_management_print(NEW_GROUP, accepted, contact_list, group_name) group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name) cmd_win = window_list.get_window(WIN_UID_COMMAND) cmd_win.add_new(ts, f"Created new group {group_name}.")
def check_packet_fate(entries_to_keep: List[bytes], packet: 'Packet', removed: bool, selector: bytes) -> bool: """Check whether the packet should be kept.""" _, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if header == PRIVATE_MESSAGE_HEADER: entries_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() elif header == GROUP_MESSAGE_HEADER: group_id, _ = separate_header(message, GROUP_ID_LENGTH) if group_id == selector: removed = True else: entries_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() return removed
def log_command(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey') -> None: """Display or export log file for the active window.""" export = ts is not None ser_no_msg, uid = separate_header(cmd_data, ENCODED_INTEGER_LENGTH) no_messages = bytes_to_int(ser_no_msg) window = window_list.get_window(uid) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load=no_messages, export=export) if export: local_win = window_list.get_local_window() local_win.add_new( ts, f"Exported log file of {window.type} '{window.name}'.", output=True)
def process_file_key_message(assembled: bytes, onion_pub_key: bytes, origin: bytes, contact_list: 'ContactList', file_keys: Dict[bytes, bytes]) -> str: """Process received file key delivery message.""" if origin == ORIGIN_USER_HEADER: raise FunctionReturn("File key message from the user.", output=False) try: decoded = base64.b85decode(assembled) except ValueError: raise FunctionReturn("Error: Received an invalid file key message.") ct_hash, file_key = separate_header(decoded, BLAKE2_DIGEST_LENGTH) if len(ct_hash) != BLAKE2_DIGEST_LENGTH or len( file_key) != SYMMETRIC_KEY_LENGTH: raise FunctionReturn("Error: Received an invalid file key message.") file_keys[onion_pub_key + ct_hash] = file_key nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick return nick
def auth_and_decrypt( nonce_ct_tag: bytes, # Nonce + ciphertext + tag key: bytes, # 32-byte symmetric key database: str = '', # When provided, gracefully exists TFC when the tag is invalid ad: bytes = b'' # Associated data ) -> bytes: # Plaintext """Authenticate and decrypt XChaCha20-Poly1305 ciphertext. The Poly1305 tag is checked using constant time `sodium_memcmp`: https://download.libsodium.org/doc/helpers#constant-time-test-for-equality When TFC decrypts ciphertext from an untrusted source (i.e., a contact), no `database` parameter is provided. In such situation, if the tag of the untrusted ciphertext is invalid, TFC discards the ciphertext and recovers appropriately. When TFC decrypts ciphertext from a trusted source (i.e., a database), the `database` parameter is provided, so the function knows which database is in question. In case the authentication fails due to invalid tag, the data is assumed to be either tampered or corrupted. TFC will in such case gracefully exit to avoid processing the unsafe data and warn the user in which database the issue was detected. """ if len(key) != SYMMETRIC_KEY_LENGTH: raise CriticalError("Invalid key length.") nonce, ct_tag = separate_header(nonce_ct_tag, XCHACHA20_NONCE_LENGTH) try: plaintext = nacl.bindings.crypto_aead_xchacha20poly1305_ietf_decrypt( ct_tag, ad, nonce, key) # type: bytes return plaintext except nacl.exceptions.CryptoError: if database: raise CriticalError( f"Authentication of data in database '{database}' failed.") raise
def get_data_loop(onion_addr: str, url_token: str, short_addr: str, onion_pub_key: bytes, queues: 'QueueDict', session: 'Session', gateway: 'Gateway') -> None: """Load TFC data from contact's Onion Service using valid URL token.""" while True: try: check_for_files(url_token, onion_pub_key, onion_addr, short_addr, session, queues) try: r = session.get( f'http://{onion_addr}.onion/{url_token}/messages', stream=True) except requests.exceptions.RequestException: return None for line in r.iter_lines( ): # Iterate over newline-separated datagrams if not line: continue try: header, payload = separate_header( line, DATAGRAM_HEADER_LENGTH) # type: bytes, bytes payload_bytes = base64.b85decode(payload) except (UnicodeError, ValueError): continue ts = datetime.now() ts_bytes = int_to_bytes(int( ts.strftime('%Y%m%d%H%M%S%f')[:-4])) process_received_packet(ts, ts_bytes, header, payload_bytes, onion_pub_key, short_addr, queues, gateway) except requests.exceptions.RequestException: break
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 SoftError( 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) cmd_win = window_list.get_window(WIN_UID_COMMAND) cmd_win.add_new(ts, f"Removed members from group {group_name}.")
def _load_groups(self) -> None: """Load groups from the encrypted database. The function first reads, authenticates and decrypts the group database data. Next, it slices and decodes the header values that help the function to properly de-serialize the database content. The function then removes dummy groups based on header data. Next, the function updates the group database settings if necessary. It then splits group data based on header data into blocks, which are further sliced, and processed if necessary, to obtain data required to create Group objects. Finally, if needed, the function will update the group database content. """ pt_bytes = self.database.load_database() # Slice and decode headers group_db_headers, pt_bytes = separate_header(pt_bytes, GROUP_DB_HEADER_LENGTH) padding_for_group_db, padding_for_members, number_of_groups, members_in_largest_group \ = list(map(bytes_to_int, split_byte_string(group_db_headers, ENCODED_INTEGER_LENGTH))) # Slice dummy groups bytes_per_group = GROUP_STATIC_LENGTH + padding_for_members * ONION_SERVICE_PUBLIC_KEY_LENGTH dummy_data_len = (padding_for_group_db - number_of_groups) * bytes_per_group group_data = pt_bytes[:-dummy_data_len] update_db = self._check_db_settings(number_of_groups, members_in_largest_group) blocks = split_byte_string(group_data, item_len=bytes_per_group) all_pub_keys = self.contact_list.get_list_of_pub_keys() dummy_pub_key = onion_address_to_pub_key(DUMMY_MEMBER) # Deserialize group objects for block in blocks: if len(block) != bytes_per_group: raise CriticalError("Invalid data in group database.") name_bytes, group_id, log_messages_byte, notification_byte, ser_pub_keys \ = separate_headers(block, [PADDED_UTF32_STR_LENGTH, GROUP_ID_LENGTH] + 2*[ENCODED_BOOLEAN_LENGTH]) pub_key_list = split_byte_string( ser_pub_keys, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) group_pub_keys = [k for k in pub_key_list if k != dummy_pub_key] group_members = [ self.contact_list.get_contact_by_pub_key(k) for k in group_pub_keys if k in all_pub_keys ] self.groups.append( Group(name=bytes_to_str(name_bytes), group_id=group_id, log_messages=bytes_to_bool(log_messages_byte), notifications=bytes_to_bool(notification_byte), members=group_members, settings=self.settings, store_groups=self.store_groups)) update_db |= set(all_pub_keys) > set(group_pub_keys) if update_db: self.store_groups()
def decrypt_assembly_packet( packet: bytes, # Assembly packet ciphertext onion_pub_key: bytes, # Onion Service pubkey of associated contact origin: bytes, # Direction of packet window_list: 'WindowList', # WindowList object contact_list: 'ContactList', # ContactList object key_list: 'KeyList' # Keylist object ) -> bytes: # Decrypted assembly packet """Decrypt assembly packet from contact/local Transmitter. This function authenticates and decrypts incoming message and command datagrams. This function does not authenticate/decrypt incoming file and/or local key datagrams. While all message datagrams have been implicitly assumed to have originated from some contact until this point, to prevent the possibility of existential forgeries, the origin of the message will be validated at this point with the cryptographic Poly1305-tag. As per the cryptographic doom principle, the message won't be even decrypted unless the Poly1305 tag of the ciphertext is valid. This function also authenticates packets that handle control flow of the Receiver program. Like messages, command datagrams have been implicitly assumed to be commands until this point. However, unless the Poly1305-tag of the purported command is found to be valid with the forward secret local key, it will not be even decrypted, let alone processed. """ ct_harac, ct_assembly_packet = separate_header( packet, header_length=HARAC_CT_LENGTH) cmd_win = window_list.get_command_window() command = onion_pub_key == LOCAL_PUBKEY p_type = "command" if command else "packet" direction = "from" if command or (origin == ORIGIN_CONTACT_HEADER) else "sent to" nick = contact_list.get_nick_by_pub_key(onion_pub_key) # Load keys keyset = key_list.get_keyset(onion_pub_key) key_dir = TX if origin == ORIGIN_USER_HEADER else RX header_key = getattr(keyset, f'{key_dir}_hk') # type: bytes message_key = getattr(keyset, f'{key_dir}_mk') # type: bytes if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [header_key, message_key]): raise SoftError("Warning! Loaded zero-key for packet decryption.") # Decrypt hash ratchet counter try: harac_bytes = auth_and_decrypt(ct_harac, header_key) except nacl.exceptions.CryptoError: raise SoftError( f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=cmd_win) # Catch up with hash ratchet offset purp_harac = bytes_to_int(harac_bytes) stored_harac = getattr(keyset, f'{key_dir}_harac') offset = purp_harac - stored_harac if offset < 0: raise SoftError( f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=cmd_win) process_offset(offset, origin, direction, nick, cmd_win) for harac in range(stored_harac, stored_harac + offset): message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH) # Decrypt packet try: assembly_packet = auth_and_decrypt(ct_assembly_packet, message_key) except nacl.exceptions.CryptoError: raise SoftError( f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=cmd_win) # Update message key and harac new_key = blake2b(message_key + int_to_bytes(stored_harac + offset), digest_size=SYMMETRIC_KEY_LENGTH) keyset.update_mk(key_dir, new_key, offset + 1) return assembly_packet
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)