Beispiel #1
0
    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))
Beispiel #2
0
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()
Beispiel #3
0
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)
Beispiel #4
0
    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))
Beispiel #5
0
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))
Beispiel #6
0
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}'.")