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)
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 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)
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)