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 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()
def write_log_entry( assembly_packet: bytes, # Assembly packet to log onion_pub_key: bytes, # Onion Service public key of the associated contact message_log: MessageLog, # MessageLog object origin: bytes = ORIGIN_USER_HEADER, # The direction of logged packet ) -> None: """Add an assembly packet to the encrypted log database. Logging assembly packets allows reconstruction of conversation while protecting metadata about the length of messages alternative log file formats could reveal to a physical attacker. Transmitter Program can only log sent messages. This is not useful for recalling conversations but it makes it possible to audit recipient's Destination Computer-side logs, where malware could have substituted content of the sent messages. Files are not produced or accessed by TFC. Thus, keeping a copy of file data in the log database is pointless and potentially dangerous, because the user should be right to assume deleting the file from `received_files` directory is enough. However, from the perspective of metadata, a difference between the number of logged packets and the number of output packets could reveal additional metadata about communication. Thus, during traffic masking, if `settings.log_file_masking` is enabled, instead of file data, TFC writes placeholder data to the log database. """ timestamp = struct.pack('<L', int(time.time())) log_entry = onion_pub_key + timestamp + origin + assembly_packet if len(log_entry) != LOG_ENTRY_LENGTH: raise CriticalError("Invalid log entry length.") ensure_dir(DIR_USER_DATA) message_log.insert_log_entry(log_entry)
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))
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}'.")