def setUp(self) -> None: """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_keys' self.keylist = KeyList(self.master_key, self.settings) self.full_contact_list = ['Alice', 'Bob', 'Charlie', LOCAL_ID] self.keylist.keysets = [create_keyset(n, store_f=self.keylist.store_keys) for n in self.full_contact_list]
def setUp(self): self.master_key = MasterKey() self.settings = Settings() self.keylist = KeyList(MasterKey(), Settings()) self.keylist.keysets = [ create_keyset(n, store_f=self.keylist.store_keys) for n in ['Alice', 'Bob', 'Charlie'] ] self.keylist.store_keys()
def test_load_of_modified_database_raises_critical_error(self): self.keylist.store_keys() # Test reading works normally self.assertIsInstance(KeyList(self.master_key, self.settings), KeyList) # Test loading of the tampered database raises CriticalError tamper_file(self.file_name, tamper_size=1) with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings)
def test_storing_and_loading_of_keysets(self): # Test store self.keylist.store_keys() self.assertEqual( os.path.getsize(self.file_name), XCHACHA20_NONCE_LENGTH + (self.settings.max_number_of_contacts + 1) * KEYSET_LENGTH + POLY1305_TAG_LENGTH) # Test load key_list2 = KeyList(MasterKey(), Settings()) self.assertEqual(len(key_list2.keysets), len(self.full_contact_list))
def test_storing_and_loading_of_keysets(self): # Test Store self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}ut_keys')) self.assertEqual( os.path.getsize(f'{DIR_USER_DATA}ut_keys'), XSALSA20_NONCE_LEN + self.settings.max_number_of_contacts * KEYSET_LENGTH + POLY1305_TAG_LEN) # Test load keylist2 = KeyList(MasterKey(), Settings()) self.assertEqual(len(keylist2.keysets), 3)
def test_invalid_content_raises_critical_error(self) -> None: # Setup invalid_data = b'a' pt_bytes = b''.join([k.serialize_k() for k in self.keylist.keysets + self.keylist._dummy_keysets()]) ct_bytes = encrypt_and_sign(pt_bytes + invalid_data, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes) # Test with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings)
def test_change_master_key(self): key = SYMMETRIC_KEY_LENGTH * b'\x01' master_key2 = MasterKey(master_key=key) # Test that new key is different from existing one self.assertNotEqual(key, self.master_key.master_key) # Change master key self.assertIsNone(self.keylist.change_master_key(master_key2)) # Test that master key has changed self.assertEqual(self.keylist.master_key.master_key, key) # Test that loading of the database with new key succeeds self.assertIsInstance(KeyList(master_key2, self.settings), KeyList)
class TestKeyList(unittest.TestCase): def setUp(self): self.unittest_dir = cd_unittest() self.master_key = MasterKey() self.settings = Settings() self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_keys' self.keylist = KeyList(self.master_key, self.settings) self.full_contact_list = ['Alice', 'Bob', 'Charlie', LOCAL_ID] self.keylist.keysets = [ create_keyset(n, store_f=self.keylist.store_keys) for n in self.full_contact_list ] def tearDown(self): cleanup(self.unittest_dir) def test_storing_and_loading_of_keysets(self): # Test store self.keylist.store_keys() self.assertEqual( os.path.getsize(self.file_name), XCHACHA20_NONCE_LENGTH + (self.settings.max_number_of_contacts + 1) * KEYSET_LENGTH + POLY1305_TAG_LENGTH) # Test load key_list2 = KeyList(MasterKey(), Settings()) self.assertEqual(len(key_list2.keysets), len(self.full_contact_list)) def test_load_of_modified_database_raises_critical_error(self): self.keylist.store_keys() # Test reading works normally self.assertIsInstance(KeyList(self.master_key, self.settings), KeyList) # Test loading of the tampered database raises CriticalError tamper_file(self.file_name, tamper_size=1) with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings) def test_invalid_content_raises_critical_error(self): # Setup invalid_data = b'a' pt_bytes = b''.join([ k.serialize_k() for k in self.keylist.keysets + self.keylist._dummy_keysets() ]) ct_bytes = encrypt_and_sign(pt_bytes + invalid_data, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes) # Test with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings) def test_generate_dummy_keyset(self): dummy_keyset = self.keylist.generate_dummy_keyset() self.assertEqual(len(dummy_keyset.serialize_k()), KEYSET_LENGTH) self.assertIsInstance(dummy_keyset, KeySet) def test_dummy_keysets(self): dummies = self.keylist._dummy_keysets() self.assertEqual(len(dummies), (self.settings.max_number_of_contacts + 1) - len(self.full_contact_list)) for c in dummies: self.assertIsInstance(c, KeySet) def test_add_keyset(self): new_key = bytes(SYMMETRIC_KEY_LENGTH) self.keylist.keysets = [create_keyset(LOCAL_ID)] # Check that KeySet exists and that its keys are different self.assertNotEqual(self.keylist.keysets[0].rx_hk, new_key) # Replace existing KeySet self.assertIsNone( self.keylist.add_keyset(LOCAL_PUBKEY, new_key, new_key, new_key, new_key)) # Check that new KeySet replaced the old one self.assertEqual(self.keylist.keysets[0].onion_pub_key, LOCAL_PUBKEY) self.assertEqual(self.keylist.keysets[0].rx_hk, new_key) def test_remove_keyset(self): # Test KeySet for Bob exists self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('Bob'))) # Remove KeySet for Bob self.assertIsNone(self.keylist.remove_keyset(nick_to_pub_key('Bob'))) # Test KeySet was removed self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('Bob'))) def test_change_master_key(self): key = SYMMETRIC_KEY_LENGTH * b'\x01' master_key2 = MasterKey(master_key=key) # Test that new key is different from existing one self.assertNotEqual(key, self.master_key.master_key) # Change master key self.assertIsNone(self.keylist.change_master_key(master_key2)) # Test that master key has changed self.assertEqual(self.keylist.master_key.master_key, key) # Test that loading of the database with new key succeeds self.assertIsInstance(KeyList(master_key2, self.settings), KeyList) def test_update_database(self): self.assertEqual(os.path.getsize(self.file_name), 9016) self.assertIsNone( self.keylist.manage(KDB_UPDATE_SIZE_HEADER, Settings(max_number_of_contacts=100))) self.assertEqual(os.path.getsize(self.file_name), 17816) self.assertEqual(self.keylist.settings.max_number_of_contacts, 100) def test_get_keyset(self): keyset = self.keylist.get_keyset(nick_to_pub_key('Alice')) self.assertIsInstance(keyset, KeySet) def test_get_list_of_pub_keys(self): self.assertEqual(self.keylist.get_list_of_pub_keys(), [ nick_to_pub_key("Alice"), nick_to_pub_key("Bob"), nick_to_pub_key("Charlie") ]) def test_has_keyset(self): self.keylist.keysets = [] self.assertFalse(self.keylist.has_keyset(nick_to_pub_key("Alice"))) self.keylist.keysets = [create_keyset('Alice')] self.assertTrue(self.keylist.has_keyset(nick_to_pub_key("Alice"))) def test_has_rx_mk(self): self.assertTrue(self.keylist.has_rx_mk(nick_to_pub_key('Bob'))) self.keylist.get_keyset( nick_to_pub_key('Bob')).rx_mk = bytes(SYMMETRIC_KEY_LENGTH) self.keylist.get_keyset( nick_to_pub_key('Bob')).rx_hk = bytes(SYMMETRIC_KEY_LENGTH) self.assertFalse(self.keylist.has_rx_mk(nick_to_pub_key('Bob'))) def test_has_local_keyset(self): self.keylist.keysets = [] self.assertFalse(self.keylist.has_local_keyset()) self.assertIsNone( self.keylist.add_keyset(LOCAL_PUBKEY, bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH))) self.assertTrue(self.keylist.has_local_keyset()) def test_manage(self): # Test that KeySet for David does not exist self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test adding KeySet self.assertIsNone( self.keylist.manage(KDB_ADD_ENTRY_HEADER, nick_to_pub_key('David'), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH))) self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test removing KeySet self.assertIsNone( self.keylist.manage(KDB_REMOVE_ENTRY_HEADER, nick_to_pub_key('David'))) self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test changing master key new_key = SYMMETRIC_KEY_LENGTH * b'\x01' self.assertNotEqual(self.master_key.master_key, new_key) self.assertIsNone( self.keylist.manage(KDB_CHANGE_MASTER_KEY_HEADER, MasterKey(master_key=new_key))) self.assertEqual(self.keylist.master_key.master_key, new_key) # Test updating key_database with new settings changes database size. self.assertEqual(os.path.getsize(self.file_name), 9016) self.assertIsNone( self.keylist.manage(KDB_UPDATE_SIZE_HEADER, Settings(max_number_of_contacts=100))) self.assertEqual(os.path.getsize(self.file_name), 17816) # Test invalid KeyList management command raises Critical Error with self.assertRaises(SystemExit): self.keylist.manage('invalid_key', None)
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', onion_service: 'OnionService') -> None: """Change the master key on Transmitter/Receiver Program.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) try: device = user_input.plaintext.split()[1].lower() except IndexError: raise SoftError( f"Error: No target-system ('{TX}' or '{RX}') specified.", head_clear=True) if device not in [TX, RX]: raise SoftError(f"Error: Invalid target system '{device}'.", head_clear=True) if device == RX: queue_command(CH_MASTER_KEY, settings, queues) return None authenticated = master_key.authenticate_action() if authenticated: # Cache old master key to allow log file re-encryption. old_master_key = master_key.master_key[:] # Create new master key but do not store new master key data into any database. new_master_key = master_key.master_key = master_key.new_master_key( replace=False) phase("Re-encrypting databases") # Halt `sender_loop` for the duration of database re-encryption. queues[KEY_MANAGEMENT_QUEUE].put((KDB_M_KEY_CHANGE_HALT_HEADER, )) wait_for_key_db_halt(queues) # Load old key_list from database file as it's not used on input_loop side. key_list = KeyList(master_key, settings) # Update encryption keys for databases contact_list.database.database_key = new_master_key key_list.database.database_key = new_master_key group_list.database.database_key = new_master_key settings.database.database_key = new_master_key onion_service.database.database_key = new_master_key # Create temp databases for each database, do not replace original. with ignored(SoftError): change_log_db_key(old_master_key, new_master_key, settings) contact_list.store_contacts(replace=False) key_list.store_keys(replace=False) group_list.store_groups(replace=False) settings.store_settings(replace=False) onion_service.store_onion_service_private_key(replace=False) # At this point all temp files exist and they have been checked to be valid by the respective # temp file writing function. It's now time to create a temp file for the new master key # database. Once the temp master key database is created, the `replace_database_data()` method # will also run the atomic `os.replace()` command for the master key database. master_key.replace_database_data() # Next we do the atomic `os.replace()` for all other files too. replace_log_db(settings) contact_list.database.replace_database() key_list.database.replace_database() group_list.database.replace_database() settings.database.replace_database() onion_service.database.replace_database() # Now all databases have been updated. It's time to let # the key database know what the new master key is. queues[KEY_MANAGEMENT_QUEUE].put(new_master_key) wait_for_key_db_ack(new_master_key, queues) phase(DONE) m_print("Master key successfully changed.", bold=True, tail_clear=True, delay=1, head=1)
def main() -> None: """Load persistent data and launch the Transmitter/Receiver Program. This function decrypts user data from databases and launches processes for Transmitter or Receiver Program. It then monitors the EXIT_QUEUE for EXIT/WIPE signals and each process in case one of them dies. If you're reading this code to get the big picture on how TFC works, start by looking at the loop functions below, defined as the target for each process, from top to bottom: From `input_loop` process, you can see how the Transmitter Program processes a message or command from the user, creates assembly packets for a message/file/command, and how those are eventually pushed into a multiprocessing queue, from where they are loaded by the `sender_loop`. The `sender_loop` process encrypts outgoing assembly packets, and outputs the encrypted datagrams to the Networked Computer. The process also sends assembly packets to the `log_writer_loop`. The `log_writer_loop` process filters out non-message assembly packets and if logging for contact is enabled, stores the message assembly packet into an encrypted log database. The `noise_loop` processes are used to provide the `sender_loop` an interface identical to that of the `input_loop`. The `sender_loop` uses the interface to load noise packets/commands when traffic masking is enabled. Refer to the file `relay.py` to see how the Relay Program on Networked Computer manages datagrams between the network and Source/Destination Computer. In Receiver Program (also launched by this file), the `gateway_loop` process acts as a buffer for incoming datagrams. This buffer is consumed by the `receiver_loop` process that organizes datagrams loaded from the buffer into a set of queues depending on datagram type. Finally, the `output_loop` process loads and processes datagrams from the queues in the order of priority. """ working_dir = f'{os.getenv("HOME")}/{DIR_TFC}' ensure_dir(working_dir) os.chdir(working_dir) operation, local_test, data_diode_sockets = process_arguments() check_kernel_version() check_kernel_entropy() print_title(operation) master_key = MasterKey( operation, local_test) gateway = Gateway( operation, local_test, data_diode_sockets) settings = Settings( master_key, operation, local_test) contact_list = ContactList(master_key, settings) key_list = KeyList( master_key, settings) group_list = GroupList( master_key, settings, contact_list) if settings.software_operation == TX: onion_service = OnionService(master_key) queues = {MESSAGE_PACKET_QUEUE: Queue(), # Standard messages COMMAND_PACKET_QUEUE: Queue(), # Standard commands TM_MESSAGE_PACKET_QUEUE: Queue(), # Traffic masking messages TM_FILE_PACKET_QUEUE: Queue(), # Traffic masking files TM_COMMAND_PACKET_QUEUE: Queue(), # Traffic masking commands TM_NOISE_PACKET_QUEUE: Queue(), # Traffic masking noise packets TM_NOISE_COMMAND_QUEUE: Queue(), # Traffic masking noise commands RELAY_PACKET_QUEUE: Queue(), # Unencrypted datagrams to Networked Computer LOG_PACKET_QUEUE: Queue(), # `log_writer_loop` assembly packets to be logged LOG_SETTING_QUEUE: Queue(), # `log_writer_loop` logging state management between noise packets TRAFFIC_MASKING_QUEUE: Queue(), # `log_writer_loop` traffic masking setting management commands LOGFILE_MASKING_QUEUE: Queue(), # `log_writer_loop` logfile masking setting management commands KEY_MANAGEMENT_QUEUE: Queue(), # `sender_loop` key database management commands SENDER_MODE_QUEUE: Queue(), # `sender_loop` default/traffic masking mode switch commands WINDOW_SELECT_QUEUE: Queue(), # `sender_loop` window selection commands during traffic masking EXIT_QUEUE: Queue() # EXIT/WIPE signal from `input_loop` to `main` } # type: Dict[bytes, Queue] process_list = [Process(target=input_loop, args=(queues, settings, gateway, contact_list, group_list, master_key, onion_service, sys.stdin.fileno())), Process(target=sender_loop, args=(queues, settings, gateway, key_list)), Process(target=log_writer_loop, args=(queues, settings)), Process(target=noise_loop, args=(queues, contact_list)), Process(target=noise_loop, args=(queues,))] else: queues = {GATEWAY_QUEUE: Queue(), # Buffer for incoming datagrams LOCAL_KEY_DATAGRAM_HEADER: Queue(), # Local key datagrams MESSAGE_DATAGRAM_HEADER: Queue(), # Message datagrams FILE_DATAGRAM_HEADER: Queue(), # File datagrams COMMAND_DATAGRAM_HEADER: Queue(), # Command datagrams EXIT_QUEUE: Queue() # EXIT/WIPE signal from `output_loop` to `main` } process_list = [Process(target=gateway_loop, args=(queues, gateway)), Process(target=receiver_loop, args=(queues, gateway)), Process(target=output_loop, args=(queues, gateway, settings, contact_list, key_list, group_list, master_key, sys.stdin.fileno()))] for p in process_list: p.start() monitor_processes(process_list, settings.software_operation, queues)
class TestKeyList(unittest.TestCase): def setUp(self): self.master_key = MasterKey() self.settings = Settings() self.keylist = KeyList(MasterKey(), Settings()) self.keylist.keysets = [ create_keyset(n, store_f=self.keylist.store_keys) for n in ['Alice', 'Bob', 'Charlie'] ] self.keylist.store_keys() def tearDown(self): cleanup() def test_storing_and_loading_of_keysets(self): # Test Store self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}ut_keys')) self.assertEqual( os.path.getsize(f'{DIR_USER_DATA}ut_keys'), XSALSA20_NONCE_LEN + self.settings.max_number_of_contacts * KEYSET_LENGTH + POLY1305_TAG_LEN) # Test load keylist2 = KeyList(MasterKey(), Settings()) self.assertEqual(len(keylist2.keysets), 3) def test_change_master_key(self): key = KEY_LENGTH * b'\x01' masterkey2 = MasterKey(master_key=key) self.keylist.change_master_key(masterkey2) self.assertEqual(self.keylist.master_key.master_key, key) def test_generate_dummy_keyset(self): dummy_keyset = self.keylist.generate_dummy_keyset() self.assertEqual(len(dummy_keyset.serialize_k()), KEYSET_LENGTH) self.assertIsInstance(dummy_keyset, KeySet) def test_get_keyset(self): keyset = self.keylist.get_keyset('*****@*****.**') self.assertIsInstance(keyset, KeySet) def test_has_local_key_and_add_keyset(self): self.assertFalse(self.keylist.has_local_key()) self.assertIsNone( self.keylist.add_keyset(LOCAL_ID, bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH))) self.assertIsNone( self.keylist.add_keyset(LOCAL_ID, bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH))) self.assertTrue(self.keylist.has_local_key()) def test_has_keyset_and_remove_keyset(self): self.assertTrue(self.keylist.has_keyset('*****@*****.**')) self.assertIsNone(self.keylist.remove_keyset('*****@*****.**')) self.assertFalse(self.keylist.has_keyset('*****@*****.**')) def test_has_rx_key(self): self.assertTrue(self.keylist.has_rx_key('*****@*****.**')) self.keylist.get_keyset('*****@*****.**').rx_key = bytes(KEY_LENGTH) self.keylist.get_keyset('*****@*****.**').rx_hek = bytes(KEY_LENGTH) self.assertFalse(self.keylist.has_rx_key('*****@*****.**')) def test_manage_keylist(self): self.assertFalse(self.keylist.has_keyset('*****@*****.**')) self.assertIsNone( self.keylist.manage(KDB_ADD_ENTRY_HEADER, '*****@*****.**', bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH), bytes(KEY_LENGTH))) self.assertTrue(self.keylist.has_keyset('*****@*****.**')) self.assertIsNone( self.keylist.manage(KDB_REMOVE_ENTRY_HEADER, '*****@*****.**')) self.assertFalse(self.keylist.has_keyset('*****@*****.**')) self.assertIsNone( self.keylist.manage(KDB_CHANGE_MASTER_KEY_HEADER, MasterKey(master_key=KEY_LENGTH * b'\x01'))) self.assertEqual(self.keylist.master_key.master_key, KEY_LENGTH * b'\x01') with self.assertRaises(SystemExit): self.keylist.manage('invalid_key', None)
def main() -> None: """Derive master key, decrypt databases and initialize processes.""" os.chdir(sys.path[0]) init_entropy() operation, local_test, dd_sockets = process_arguments() clear_screen() c_print("TFC", head=1, tail=1) master_key = MasterKey(operation, local_test) settings = Settings(master_key, operation, local_test, dd_sockets) contact_list = ContactList(master_key, settings) key_list = KeyList(master_key, settings) group_list = GroupList(master_key, settings, contact_list) gateway = Gateway(settings) process_list = [] if settings.software_operation == 'tx': queues = { MESSAGE_PACKET_QUEUE: Queue(), FILE_PACKET_QUEUE: Queue(), COMMAND_PACKET_QUEUE: Queue(), LOG_PACKET_QUEUE: Queue(), NOISE_PACKET_QUEUE: Queue(), NOISE_COMMAND_QUEUE: Queue(), KEY_MANAGEMENT_QUEUE: Queue(), WINDOW_SELECT_QUEUE: Queue() } if settings.session_trickle: np_filler = Process(target=noise_process, args=(P_N_HEADER, queues[NOISE_PACKET_QUEUE], contact_list)) nc_filler = Process(target=noise_process, args=(C_N_HEADER, queues[NOISE_COMMAND_QUEUE])) process_list.extend([np_filler, nc_filler]) for p in [np_filler, nc_filler]: p.start() while any([ q.qsize() < 1000 for q in [queues[NOISE_PACKET_QUEUE], queues[NOISE_COMMAND_QUEUE]] ]): time.sleep(0.1) sender_process = Process(target=sender_loop, args=(settings, queues, gateway, key_list)) input_process = Process(target=tx_loop, args=(settings, queues, gateway, contact_list, group_list, master_key, sys.stdin.fileno())) log_process = Process(target=log_writer, args=(queues[LOG_PACKET_QUEUE], )) process_list.extend([sender_process, input_process, log_process]) for p in [sender_process, input_process, log_process]: p.start() elif settings.software_operation == 'rx': queues = { LOCAL_KEY_PACKET_HEADER: Queue(), PUBLIC_KEY_PACKET_HEADER: Queue(), MESSAGE_PACKET_HEADER: Queue(), COMMAND_PACKET_HEADER: Queue(), IMPORTED_FILE_CT_HEADER: Queue(), GATEWAY_QUEUE: Queue() } gateway_process = Process(target=gw_incoming, args=(gateway, queues[GATEWAY_QUEUE])) receiver_process = Process(target=receiver_loop, args=(settings, queues)) output_process = Process(target=rx_loop, args=(settings, queues, contact_list, key_list, group_list, master_key, sys.stdin.fileno())) process_list.extend( [gateway_process, receiver_process, output_process]) for p in [gateway_process, receiver_process, output_process]: p.start() while True: try: time.sleep(0.1) if not all([p.is_alive() for p in process_list]): for p in process_list: p.terminate() exit() except (EOFError, KeyboardInterrupt): pass
class TestKeyList(unittest.TestCase): def setUp(self) -> None: """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_keys' self.keylist = KeyList(self.master_key, self.settings) self.full_contact_list = ['Alice', 'Bob', 'Charlie', LOCAL_ID] self.keylist.keysets = [ create_keyset(n, store_f=self.keylist.store_keys) for n in self.full_contact_list ] def tearDown(self) -> None: """Post-test actions.""" cleanup(self.unit_test_dir) def test_storing_and_loading_of_keysets(self) -> None: # Test store self.keylist.store_keys() self.assertEqual( os.path.getsize(self.file_name), XCHACHA20_NONCE_LENGTH + (self.settings.max_number_of_contacts + 1) * KEYSET_LENGTH + POLY1305_TAG_LENGTH) # Test load key_list2 = KeyList(MasterKey(), Settings()) self.assertEqual(len(key_list2.keysets), len(self.full_contact_list)) def test_load_of_modified_database_raises_critical_error(self) -> None: self.keylist.store_keys() # Test reading works normally self.assertIsInstance(KeyList(self.master_key, self.settings), KeyList) # Test loading of the tampered database raises CriticalError tamper_file(self.file_name, tamper_size=1) with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings) def test_invalid_content_raises_critical_error(self) -> None: # Setup invalid_data = b'a' pt_bytes = b''.join([ k.serialize_k() for k in self.keylist.keysets + self.keylist._dummy_keysets() ]) ct_bytes = encrypt_and_sign(pt_bytes + invalid_data, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes) # Test with self.assertRaises(SystemExit): KeyList(self.master_key, self.settings) def test_generate_dummy_keyset(self) -> None: dummy_keyset = self.keylist.generate_dummy_keyset() self.assertEqual(len(dummy_keyset.serialize_k()), KEYSET_LENGTH) self.assertIsInstance(dummy_keyset, KeySet) def test_dummy_keysets(self) -> None: dummies = self.keylist._dummy_keysets() self.assertEqual(len(dummies), (self.settings.max_number_of_contacts + 1) - len(self.full_contact_list)) for c in dummies: self.assertIsInstance(c, KeySet) def test_add_keyset(self) -> None: new_key = bytes(SYMMETRIC_KEY_LENGTH) self.keylist.keysets = [create_keyset(LOCAL_ID)] # Check that KeySet exists and that its keys are different from the new ones self.assertNotEqual(self.keylist.keysets[0].rx_hk, new_key) # Replace the existing KeySet self.assertIsNone( self.keylist.add_keyset(LOCAL_PUBKEY, new_key, new_key, new_key, new_key)) # Check that the new KeySet replaced the old one self.assertEqual(self.keylist.keysets[0].onion_pub_key, LOCAL_PUBKEY) self.assertEqual(self.keylist.keysets[0].rx_hk, new_key) def test_remove_keyset(self) -> None: # Test that the KeySet for Bob exists self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('Bob'))) # Remove the KeySet for Bob self.assertIsNone(self.keylist.remove_keyset(nick_to_pub_key('Bob'))) # Test that the KeySet was removed self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('Bob'))) @mock.patch('builtins.input', side_effect=['test_password']) def test_change_master_key(self, _: Any) -> None: # Setup key = SYMMETRIC_KEY_LENGTH * b'\x01' master_key2 = MasterKey(master_key=key) queues = gen_queue_dict() def queue_delayer() -> None: """Place packet to the key management queue after timer runs out.""" time.sleep(0.1) queues[KEY_MANAGEMENT_QUEUE].put(master_key2.master_key) threading.Thread(target=queue_delayer).start() # Test that the new key is different from the existing one self.assertNotEqual(key, self.master_key.master_key) # Change the master key self.assertIsNone(self.keylist.change_master_key(queues)) # Test that the master key was changed self.assertEqual(self.keylist.master_key.master_key, key) self.assertEqual(self.keylist.database.database_key, key) self.assertEqual(queues[KEY_MGMT_ACK_QUEUE].get(), KDB_HALT_ACK_HEADER) self.assertEqual(queues[KEY_MGMT_ACK_QUEUE].get(), key) def test_update_database(self) -> None: # Setup queues = gen_queue_dict() # Test self.assertEqual(os.path.getsize(self.file_name), 9016) self.assertIsNone( self.keylist.manage(queues, KDB_UPDATE_SIZE_HEADER, Settings(max_number_of_contacts=100))) self.assertEqual(os.path.getsize(self.file_name), 17816) self.assertEqual(self.keylist.settings.max_number_of_contacts, 100) def test_get_keyset(self) -> None: keyset = self.keylist.get_keyset(nick_to_pub_key('Alice')) self.assertIsInstance(keyset, KeySet) def test_get_list_of_pub_keys(self) -> None: self.assertEqual(self.keylist.get_list_of_pub_keys(), [ nick_to_pub_key("Alice"), nick_to_pub_key("Bob"), nick_to_pub_key("Charlie") ]) def test_has_keyset(self) -> None: self.keylist.keysets = [] self.assertFalse(self.keylist.has_keyset(nick_to_pub_key("Alice"))) self.keylist.keysets = [create_keyset('Alice')] self.assertTrue(self.keylist.has_keyset(nick_to_pub_key("Alice"))) def test_has_rx_mk(self) -> None: self.assertTrue(self.keylist.has_rx_mk(nick_to_pub_key('Bob'))) self.keylist.get_keyset( nick_to_pub_key('Bob')).rx_mk = bytes(SYMMETRIC_KEY_LENGTH) self.keylist.get_keyset( nick_to_pub_key('Bob')).rx_hk = bytes(SYMMETRIC_KEY_LENGTH) self.assertFalse(self.keylist.has_rx_mk(nick_to_pub_key('Bob'))) def test_has_local_keyset(self) -> None: self.keylist.keysets = [] self.assertFalse(self.keylist.has_local_keyset()) self.assertIsNone( self.keylist.add_keyset(LOCAL_PUBKEY, bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH))) self.assertTrue(self.keylist.has_local_keyset()) def test_manage(self) -> None: # Setup queues = gen_queue_dict() # Test that the KeySet for David does not exist self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test adding the KeySet for David self.assertIsNone( self.keylist.manage(queues, KDB_ADD_ENTRY_HEADER, nick_to_pub_key('David'), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH))) self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test removing David's KeySet self.assertIsNone( self.keylist.manage(queues, KDB_REMOVE_ENTRY_HEADER, nick_to_pub_key('David'))) self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David'))) # Test changing the master key new_key = SYMMETRIC_KEY_LENGTH * b'\x01' self.assertNotEqual(self.master_key.master_key, new_key) queues[KEY_MANAGEMENT_QUEUE].put(new_key) self.assertIsNone( self.keylist.manage(queues, KDB_M_KEY_CHANGE_HALT_HEADER)) self.assertEqual(self.keylist.master_key.master_key, new_key) self.assertEqual(self.keylist.database.database_key, new_key) # Test an invalid KeyList management command raises CriticalError with self.assertRaises(SystemExit): self.keylist.manage(queues, 'invalid_key', None)
def main() -> None: """Derive master key, decrypt databases and initialize processes.""" os.chdir(sys.path[0]) check_kernel_version() check_kernel_entropy() operation, local_test, dd_sockets = process_arguments() clear_screen() c_print(TFC, head=1, tail=1) master_key = MasterKey(operation, local_test) settings = Settings(master_key, operation, local_test, dd_sockets) contact_list = ContactList(master_key, settings) key_list = KeyList(master_key, settings) group_list = GroupList(master_key, settings, contact_list) gateway = Gateway(settings) if settings.software_operation == TX: queues = { MESSAGE_PACKET_QUEUE: Queue(), FILE_PACKET_QUEUE: Queue(), COMMAND_PACKET_QUEUE: Queue(), NH_PACKET_QUEUE: Queue(), LOG_PACKET_QUEUE: Queue(), EXIT_QUEUE: Queue(), NOISE_PACKET_QUEUE: Queue(), NOISE_COMMAND_QUEUE: Queue(), KEY_MANAGEMENT_QUEUE: Queue(), WINDOW_SELECT_QUEUE: Queue() } process_list = [ Process(target=input_loop, args=(queues, settings, gateway, contact_list, group_list, master_key, sys.stdin.fileno())), Process(target=sender_loop, args=(queues, settings, gateway, key_list)), Process(target=log_writer_loop, args=(queues, )) ] if settings.session_traffic_masking: process_list.extend([ Process(target=noise_loop, args=(P_N_HEADER, queues[NOISE_PACKET_QUEUE], contact_list)), Process(target=noise_loop, args=(C_N_HEADER, queues[NOISE_COMMAND_QUEUE])) ]) else: queues = { LOCAL_KEY_PACKET_HEADER: Queue(), PUBLIC_KEY_PACKET_HEADER: Queue(), MESSAGE_PACKET_HEADER: Queue(), COMMAND_PACKET_HEADER: Queue(), IMPORTED_FILE_HEADER: Queue(), EXIT_QUEUE: Queue(), GATEWAY_QUEUE: Queue() } process_list = [ Process(target=gateway_loop, args=(queues, gateway)), Process(target=receiver_loop, args=(queues, settings)), Process(target=output_loop, args=(queues, settings, contact_list, key_list, group_list, master_key, sys.stdin.fileno())) ] for p in process_list: p.start() while True: with ignored(EOFError, KeyboardInterrupt): time.sleep(0.1) if not all([p.is_alive() for p in process_list]): for p in process_list: p.terminate() sys.exit(1) if not queues[EXIT_QUEUE].empty(): command = queues[EXIT_QUEUE].get() for p in process_list: p.terminate() if command == WIPE: subprocess.Popen( f"find {DIR_USER_DATA} -name '{operation}*' -type f -exec shred -n 3 -z -u {{}} \;", shell=True).wait() os.system('poweroff') else: sys.exit(0)