def access_logs(window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """\ Load 'msg_to_load' last messages from log database and display or export them. The default value of zero for `msg_to_load` means all messages for the window will be retrieved from the log database. """ file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' packet_list = PacketList(settings, contact_list) message_list = [] # type: List[MsgTuple] group_msg_id = b'' check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, timestamp, origin, assembly_packet \ = separate_headers(log_entry, [ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH]) if window.type == WIN_TYPE_CONTACT and onion_pub_key != window.uid: continue packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet) except SoftError: continue if not packet.is_complete: continue group_msg_id = add_complete_message_to_message_list( timestamp, onion_pub_key, group_msg_id, packet, message_list, window) message_log.close_database() print_logs(message_list[-msg_to_load:], export, msg_to_load, window, contact_list, group_list, settings)
def change_log_db_key(old_key: bytes, new_key: bytes, settings: 'Settings') -> None: """Re-encrypt the log database with a new master key.""" ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX if not os.path.isfile(file_name): raise SoftError("No log database available.") if os.path.isfile(temp_name): os.remove(temp_name) message_log_old = MessageLog(file_name, old_key) message_log_tmp = MessageLog(temp_name, new_key) for log_entry in message_log_old: message_log_tmp.insert_log_entry(log_entry) message_log_old.close_database() message_log_tmp.close_database()
class TestMessageLog(unittest.TestCase): def setUp(self) -> None: """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.file_name = f'{DIR_USER_DATA}ut_logs' self.temp_name = self.file_name + '_temp' self.settings = Settings() self.database_key = os.urandom(SYMMETRIC_KEY_LENGTH) self.message_log = MessageLog(self.file_name, self.database_key) def tearDown(self) -> None: """Post-test actions.""" cleanup(self.unit_test_dir) def test_empty_log_database_is_verified(self) -> None: self.assertTrue(self.message_log.verify_file(self.file_name)) def test_database_with_one_entry_is_verified(self) -> None: # Setup test_entry = b'test_log_entry' self.message_log.insert_log_entry(test_entry) # Test self.assertTrue(self.message_log.verify_file(self.file_name)) def test_invalid_database_returns_false(self) -> None: # Setup self.message_log.c.execute("DROP TABLE log_entries") self.message_log.conn.commit() # Test self.assertFalse(self.message_log.verify_file(self.file_name)) def test_invalid_entry_returns_false(self) -> None: # Setup params = (os.urandom(LOG_ENTRY_LENGTH),) self.message_log.c.execute(f"""INSERT INTO log_entries (log_entry) VALUES (?)""", params) self.message_log.conn.commit() # Test self.assertFalse(self.message_log.verify_file(self.file_name)) def test_table_creation(self) -> None: self.assertIsInstance(self.message_log, MessageLog) self.assertTrue(os.path.isfile(self.file_name)) def test_writing_to_log_database(self) -> None: data = os.urandom(LOG_ENTRY_LENGTH) self.assertIsNone(self.message_log.insert_log_entry(data)) def test_iterating_over_log_database(self) -> None: data = [os.urandom(LOG_ENTRY_LENGTH), os.urandom(LOG_ENTRY_LENGTH)] for entry in data: self.assertIsNone(self.message_log.insert_log_entry(entry)) for index, stored_entry in enumerate(self.message_log): self.assertEqual(stored_entry, data[index]) def test_invalid_temp_database_is_not_loaded(self) -> None: log_file = MessageLog(self.file_name, database_key=self.database_key) tmp_file = MessageLog(self.temp_name, database_key=self.database_key) log_file.insert_log_entry(b'a') log_file.insert_log_entry(b'b') log_file.insert_log_entry(b'c') log_file.insert_log_entry(b'd') log_file.insert_log_entry(b'e') tmp_file.insert_log_entry(b'a') tmp_file.insert_log_entry(b'b') tmp_file.c.execute(f"""INSERT INTO log_entries (log_entry) VALUES (?)""", (b'c',)) tmp_file.conn.commit() tmp_file.insert_log_entry(b'd') tmp_file.insert_log_entry(b'e') self.assertTrue(os.path.isfile(self.temp_name)) log_file = MessageLog(self.file_name, database_key=self.database_key) self.assertEqual(list(log_file), [b'a', b'b', b'c', b'd', b'e']) self.assertFalse(os.path.isfile(self.temp_name)) def test_valid_temp_database_is_loaded(self) -> None: log_file = MessageLog(self.file_name, database_key=self.database_key) tmp_file = MessageLog(self.temp_name, database_key=self.database_key) log_file.insert_log_entry(b'a') log_file.insert_log_entry(b'b') log_file.insert_log_entry(b'c') log_file.insert_log_entry(b'd') log_file.insert_log_entry(b'e') tmp_file.insert_log_entry(b'f') tmp_file.insert_log_entry(b'g') tmp_file.insert_log_entry(b'h') tmp_file.insert_log_entry(b'i') tmp_file.insert_log_entry(b'j') self.assertTrue(os.path.isfile(self.temp_name)) log_file = MessageLog(self.file_name, database_key=self.database_key) self.assertEqual(list(log_file), [b'f', b'g', b'h', b'i', b'j']) self.assertFalse(os.path.isfile(self.temp_name)) def test_database_closing(self) -> None: self.message_log.close_database() # Test insertion would fail at this point with self.assertRaises(sqlite3.ProgrammingError): self.message_log.c.execute(f"""INSERT INTO log_entries (log_entry) VALUES (?)""", (os.urandom(LOG_ENTRY_LENGTH),)) # Test closed database is re-opened during write data = os.urandom(LOG_ENTRY_LENGTH) self.assertIsNone(self.message_log.insert_log_entry(data))
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for the group matching that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX packet_list = PacketList(settings, contact_list) entries_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, _, origin, assembly_packet = separate_headers( log_entry, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: entries_to_keep.append(log_entry) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, log_entry) except SoftError: continue if not packet.is_complete: continue removed = check_packet_fate(entries_to_keep, packet, removed, selector) message_log.close_database() message_log_temp = MessageLog(temp_name, master_key.master_key) for log_entry in entries_to_keep: message_log_temp.insert_log_entry(log_entry) message_log_temp.close_database() os.replace(temp_name, file_name) try: name = contact_list.get_nick_by_pub_key( selector) if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) if contact else b58encode( selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise SoftError(f"{action} log entries for {win_type} '{name}'.")