def determine_selector(selection: str, contact_list: 'ContactList', group_list: 'GroupList') -> bytes: """Determine selector (group ID or Onion Service public key).""" if selection in contact_list.contact_selectors(): selector = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key elif selection in group_list.get_list_of_group_names(): selector = group_list.get_group(selection).group_id elif len(selection) == ONION_ADDRESS_LENGTH: if validate_onion_addr(selection): raise SoftError("Error: Invalid account.", head_clear=True) selector = onion_address_to_pub_key(selection) elif len(selection) == GROUP_ID_ENC_LENGTH: try: selector = b58decode(selection) except ValueError: raise SoftError("Error: Invalid group ID.", head_clear=True) else: raise SoftError("Error: Unknown selector.", head_clear=True) return selector
def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None: """Manage incoming contact requests.""" existing_contacts = [] # type: List[bytes] contact_requests = [] # type: List[bytes] request_queue = queues[CONTACT_REQ_QUEUE] contact_queue = queues[C_REQ_MGMT_QUEUE] setting_queue = queues[C_REQ_STATE_QUEUE] show_requests = True while True: with ignored(EOFError, KeyboardInterrupt): while request_queue.qsize() == 0: time.sleep(0.1) purp_onion_address = request_queue.get() while setting_queue.qsize() != 0: show_requests = setting_queue.get() # Update list of existing contacts while contact_queue.qsize() > 0: command, ser_onion_pub_keys = contact_queue.get() onion_pub_key_list = split_byte_string( ser_onion_pub_keys, ONION_SERVICE_PUBLIC_KEY_LENGTH) if command == RP_ADD_CONTACT_HEADER: existing_contacts = list( set(existing_contacts) | set(onion_pub_key_list)) elif command == RP_REMOVE_CONTACT_HEADER: existing_contacts = list( set(existing_contacts) - set(onion_pub_key_list)) if validate_onion_addr(purp_onion_address) == '': onion_pub_key = onion_address_to_pub_key(purp_onion_address) if onion_pub_key in existing_contacts: continue if onion_pub_key in contact_requests: continue if show_requests: ts_fmt = datetime.now().strftime( '%b %d - %H:%M:%S.%f')[:-4] m_print([ f"{ts_fmt} - New contact request from an unknown TFC account:", purp_onion_address ], box=True) contact_requests.append(onion_pub_key) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: break
def remove_contact(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove contact from TFC.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) try: selection = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No account specified.", head_clear=True) if not yes(f"Remove contact '{selection}'?", abort=False, head=1): raise SoftError("Removal of contact aborted.", head=0, delay=1, tail_clear=True) if selection in contact_list.contact_selectors(): onion_pub_key = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key else: if validate_onion_addr(selection): raise SoftError("Error: Invalid selection.", head=0, delay=1, tail_clear=True) onion_pub_key = onion_address_to_pub_key(selection) receiver_command = CONTACT_REM + onion_pub_key queue_command(receiver_command, settings, queues) with ignored(SoftError): remove_logs(contact_list, group_list, settings, master_key, onion_pub_key) queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key)) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) target = determine_target(selection, onion_pub_key, contact_list) if any([g.remove_members([onion_pub_key]) for g in group_list]): m_print(f"Removed {target} from group(s).", tail=1) check_for_window_deselection(onion_pub_key, window, group_list)
def generate_dummy_contact() -> Contact: """Generate a dummy Contact object. The dummy contact simplifies the code around the constant length serialization when the data is stored to, or read from the database. """ return Contact(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT), nick=DUMMY_NICK, tx_fingerprint=bytes(FINGERPRINT_LENGTH), rx_fingerprint=bytes(FINGERPRINT_LENGTH), kex_status=KEX_STATUS_NONE, log_messages=False, file_reception=False, notifications=False)
def generate_dummy_keyset() -> 'KeySet': """Generate a dummy KeySet object. The dummy KeySet simplifies the code around the constant length serialization when the data is stored to, or read from the database. In case the dummy keyset would ever be loaded accidentally, it uses a set of random keys to prevent decryption by eavesdropper. """ return KeySet(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT), tx_mk=csprng(), rx_mk=csprng(), tx_hk=csprng(), rx_hk=csprng(), tx_harac=INITIAL_HARAC, rx_harac=INITIAL_HARAC, store_keys=lambda: None)
def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None: """Manage displayed contact requests.""" existing_contacts = [] # type: List[bytes] displayed_requests = [] # type: List[bytes] request_queue = queues[CONTACT_REQ_QUEUE] contact_queue = queues[C_REQ_MGMT_QUEUE] setting_queue = queues[C_REQ_STATE_QUEUE] account_queue = queues[ACCOUNT_SEND_QUEUE] show_requests = True while True: with ignored(EOFError, KeyboardInterrupt): while request_queue.qsize() == 0: time.sleep(0.1) purp_onion_address = request_queue.get() while setting_queue.qsize() != 0: show_requests = setting_queue.get() existing_contacts = update_list_of_existing_contacts( contact_queue, existing_contacts) if validate_onion_addr(purp_onion_address) == '': onion_pub_key = onion_address_to_pub_key(purp_onion_address) if onion_pub_key in existing_contacts: continue if onion_pub_key in displayed_requests: continue if show_requests: ts = datetime.now().strftime('%b %d - %H:%M:%S.%f')[:-4] m_print([ f"{ts} - New contact request from an unknown TFC account:", purp_onion_address ], box=True) account_queue.put(purp_onion_address) displayed_requests.append(onion_pub_key) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: break
def remove_log(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove log entries for contact or group.""" try: selection = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No contact/group specified.", head_clear=True) if not yes(f"Remove logs for {selection}?", abort=False, head=1): raise FunctionReturn("Log file removal aborted.", tail_clear=True, delay=1, head=0) # Determine selector (group ID or Onion Service public key) from command parameters if selection in contact_list.contact_selectors(): selector = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key elif selection in group_list.get_list_of_group_names(): selector = group_list.get_group(selection).group_id elif len(selection) == ONION_ADDRESS_LENGTH: if validate_onion_addr(selection): raise FunctionReturn("Error: Invalid account.", head_clear=True) selector = onion_address_to_pub_key(selection) elif len(selection) == GROUP_ID_ENC_LENGTH: try: selector = b58decode(selection) except ValueError: raise FunctionReturn("Error: Invalid group ID.", head_clear=True) else: raise FunctionReturn("Error: Unknown selector.", head_clear=True) # Remove logs that match the selector command = LOG_REMOVE + selector queue_command(command, settings, queues) remove_logs(contact_list, group_list, settings, master_key, selector)
def serialize_g(self) -> bytes: """Return group data as a constant length bytestring. This function serializes the group's data into a bytestring that always has a constant length. The exact length depends on the attribute `max_number_of_group_members` of TFC's Settings object. With the default setting of 50 members per group, the length of the serialized data is 1024 + 4 + 2*1 + 50*32 = 2630 bytes The purpose of the constant length serialization is to hide any metadata the ciphertext length of the group database could reveal. """ members = self.get_list_of_member_pub_keys() number_of_dummies = self.settings.max_number_of_group_members - len( self.members) members += number_of_dummies * [onion_address_to_pub_key(DUMMY_MEMBER)] member_bytes = b''.join(members) return (str_to_bytes(self.name) + self.group_id + bool_to_bytes(self.log_messages) + bool_to_bytes(self.notifications) + member_bytes)
def test_conversion_back_and_forth(self): pub_key = os.urandom(SYMMETRIC_KEY_LENGTH) self.assertEqual( onion_address_to_pub_key(pub_key_to_onion_address(pub_key)), pub_key)
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 test_local_pubkey(self): """Test that local key's reserved public key is valid.""" self.assertEqual(src.common.statics.LOCAL_PUBKEY, onion_address_to_pub_key(src.common.statics.LOCAL_ID))
def add_new_contact(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', onion_service: 'OnionService') -> None: """Prompt for contact account details and initialize desired key exchange. This function requests the minimum amount of data about the recipient as possible. The TFC account of contact is the same as the Onion URL of contact's v3 Tor Onion Service. Since the accounts are random and hard to remember, the user has to choose a nickname for their contact. Finally, the user must select the key exchange method: ECDHE for convenience in a pre-quantum world, or PSK for situations where physical key exchange is possible, and ciphertext must remain secure even after sufficient QTMs are available to adversaries. Before starting the key exchange, Transmitter Program exports the public key of contact's Onion Service to Relay Program on their Networked Computer so that a connection to the contact can be established. """ try: if settings.traffic_masking: raise SoftError( "Error: Command is disabled during traffic masking.", head_clear=True) if len(contact_list) >= settings.max_number_of_contacts: raise SoftError( f"Error: TFC settings only allow {settings.max_number_of_contacts} accounts.", head_clear=True) m_print("Add new contact", head=1, bold=True, head_clear=True) m_print([ "Your TFC account is", onion_service.user_onion_address, '', "Warning!", "Anyone who knows this account", "can see when your TFC is online" ], box=True) contact_address = get_onion_address_from_user( onion_service.user_onion_address, queues) onion_pub_key = onion_address_to_pub_key(contact_address) contact_nick = box_input( "Contact nick", expected_len= ONION_ADDRESS_LENGTH, # Limited to 255 but such long nick is unpractical. validator=validate_nick, validator_args=(contact_list, group_list, onion_pub_key)).strip() key_exchange = box_input(f"Key exchange ([{ECDHE}],PSK) ", default=ECDHE, expected_len=28, validator=validate_key_exchange).strip() relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_ADD_NEW_CONTACT + onion_pub_key queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) if key_exchange.upper() in ECDHE: start_key_exchange(onion_pub_key, contact_nick, contact_list, settings, queues) elif key_exchange.upper() in PSK: create_pre_shared_key(onion_pub_key, contact_nick, contact_list, settings, onion_service, queues) except (EOFError, KeyboardInterrupt): raise SoftError("Contact creation aborted.", head=2, delay=1, tail_clear=True)
def remove_contact(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove contact from TFC.""" if settings.traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) try: selection = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("Error: No account specified.", head_clear=True) if not yes(f"Remove contact '{selection}'?", abort=False, head=1): raise FunctionReturn("Removal of contact aborted.", head=0, delay=1, tail_clear=True) if selection in contact_list.contact_selectors(): onion_pub_key = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key else: if validate_onion_addr(selection): raise FunctionReturn("Error: Invalid selection.", head=0, delay=1, tail_clear=True) else: onion_pub_key = onion_address_to_pub_key(selection) receiver_command = CONTACT_REM + onion_pub_key queue_command(receiver_command, settings, queues) with ignored(FunctionReturn): remove_logs(contact_list, group_list, settings, master_key, onion_pub_key) queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key)) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) if onion_pub_key in contact_list.get_list_of_pub_keys(): contact = contact_list.get_contact_by_pub_key(onion_pub_key) target = f"{contact.nick} ({contact.short_address})" contact_list.remove_contact_by_pub_key(onion_pub_key) m_print(f"Removed {target} from contacts.", head=1, tail=1) else: target = f"{selection[:TRUNC_ADDRESS_LENGTH]}" m_print(f"Transmitter has no {target} to remove.", head=1, tail=1) if any([g.remove_members([onion_pub_key]) for g in group_list]): m_print(f"Removed {target} from group(s).", tail=1) if window.type == WIN_TYPE_CONTACT: if onion_pub_key == window.uid: window.deselect() if window.type == WIN_TYPE_GROUP: for c in window: if c.onion_pub_key == onion_pub_key: window.update_window(group_list) # If the last member of the group is removed, deselect # the group. Deselection is not done in # update_group_win_members because it would prevent # selecting the empty group for group related commands # such as notifications. if not window.window_contacts: window.deselect()