Ejemplo n.º 1
0
    def process_long_header(self,
                            packet:    bytes,
                            packet_ct: Optional[bytes] = None
                            ) -> None:
        """Process first packet of long transmission."""
        if self.long_active:
            self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list))

        if self.type == FILE:
            self.new_file_packet()
            try:
                lh, no_p_bytes, time_bytes, size_bytes, packet \
                    = separate_headers(packet, [ASSEMBLY_PACKET_HEADER_LENGTH] + 3*[ENCODED_INTEGER_LENGTH])

                self.packets = bytes_to_int(no_p_bytes)  # added by transmitter.packet.split_to_assembly_packets
                self.time    = str(timedelta(seconds=bytes_to_int(time_bytes)))
                self.size    = readable_size(bytes_to_int(size_bytes))
                self.name    = packet.split(US_BYTE)[0].decode()
                packet       = lh + packet

                m_print([f'Receiving file from {self.contact.nick}:',
                         f'{self.name} ({self.size})',
                         f'ETA {self.time} ({self.packets} packets)'], bold=True)

            except (struct.error, UnicodeError, ValueError):
                self.add_masking_packet_to_log_file()
                raise FunctionReturn("Error: Received file packet had an invalid header.")

        self.assembly_pt_list = [packet]
        self.long_active      = True
        self.is_complete      = False

        if packet_ct is not None:
            self.log_ct_list = [packet_ct]
Ejemplo n.º 2
0
def receiver_loop(queues:   Dict[bytes, 'Queue'],
                  gateway:  'Gateway',
                  unittest: bool = False
                  ) -> None:
    """Decode received packets and forward them to packet queues."""
    gateway_queue = queues[GATEWAY_QUEUE]

    while True:
        with ignored(EOFError, KeyboardInterrupt):
            if gateway_queue.qsize() == 0:
                time.sleep(0.01)

            _, packet = gateway_queue.get()

            try:
                packet = gateway.detect_errors(packet)
            except FunctionReturn:
                continue

            header, ts_bytes, payload = separate_headers(packet, [DATAGRAM_HEADER_LENGTH, DATAGRAM_TIMESTAMP_LENGTH])

            try:
                ts = datetime.strptime(str(bytes_to_int(ts_bytes)), "%Y%m%d%H%M%S%f")
            except (ValueError, struct.error):
                m_print("Error: Failed to decode timestamp in the received packet.", head=1, tail=1)
                continue

            if header in [MESSAGE_DATAGRAM_HEADER, FILE_DATAGRAM_HEADER,
                          COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER]:
                queues[header].put((ts, payload))

            if unittest:
                break
Ejemplo n.º 3
0
def key_ex_psk_tx(packet: bytes, ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', key_list: 'KeyList',
                  settings: 'Settings') -> None:
    """Add contact and Tx-PSKs."""

    onion_pub_key, tx_mk, _, tx_hk, _, nick_bytes \
        = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH])

    try:
        nick = bytes_to_str(nick_bytes)
    except (struct.error, UnicodeError):
        raise SoftError("Error: Received invalid contact data")

    contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH),
                             bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK,
                             settings.log_messages_by_default,
                             settings.accept_files_by_default,
                             settings.show_notifications_by_default)

    # The Rx-side keys are set as null-byte strings to indicate they have not
    # been added yet. The zero-keys do not allow existential forgeries as
    # `decrypt_assembly_packet`does not allow the use of zero-keys for decryption.
    key_list.add_keyset(onion_pub_key=onion_pub_key,
                        tx_mk=tx_mk,
                        rx_mk=bytes(SYMMETRIC_KEY_LENGTH),
                        tx_hk=tx_hk,
                        rx_hk=bytes(SYMMETRIC_KEY_LENGTH))

    c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
    message = f"Added Tx-side PSK for {nick} ({pub_key_to_short_address(onion_pub_key)})."
    cmd_win = window_list.get_command_window()
    cmd_win.add_new(ts, message)

    m_print([message, f"Confirmation code (to Transmitter): {c_code.hex()}"],
            box=True)
Ejemplo n.º 4
0
def key_ex_ecdhe(packet: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', key_list: 'KeyList',
                 settings: 'Settings') -> None:
    """Add contact and symmetric keys derived from X448 shared key."""

    onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, nick_bytes \
        = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH])

    try:
        nick = bytes_to_str(nick_bytes)
    except (struct.error, UnicodeError):
        raise FunctionReturn("Error: Received invalid contact data")

    contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH),
                             bytes(FINGERPRINT_LENGTH), KEX_STATUS_NONE,
                             settings.log_messages_by_default,
                             settings.accept_files_by_default,
                             settings.show_notifications_by_default)

    key_list.add_keyset(onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk)

    message = f"Successfully added {nick}."
    local_win = window_list.get_local_window()
    local_win.add_new(ts, message)

    c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
    m_print([message, f"Confirmation code (to Transmitter): {c_code.hex()}"],
            box=True)
Ejemplo n.º 5
0
def add_complete_message_to_message_list(
        timestamp: bytes, onion_pub_key: bytes, group_msg_id: bytes,
        packet: 'Packet', message_list: List[MsgTuple],
        window: Union['TxWindow', 'RxWindow']) -> bytes:
    """Add complete log file message to `message_list`."""
    whisper_byte, header, message = separate_headers(
        packet.assemble_message_packet(),
        [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])

    whisper = bytes_to_bool(whisper_byte)

    if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT:
        message_list.append((bytes_to_timestamp(timestamp), message.decode(),
                             onion_pub_key, packet.origin, whisper, False))

    elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP:
        purp_group_id, message = separate_header(message, GROUP_ID_LENGTH)
        if window.group is not None and purp_group_id != window.group.group_id:
            return group_msg_id

        purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH)
        if packet.origin == ORIGIN_USER_HEADER:
            if purp_msg_id == group_msg_id:
                return group_msg_id
            group_msg_id = purp_msg_id

        message_list.append((bytes_to_timestamp(timestamp), message.decode(),
                             onion_pub_key, packet.origin, whisper, False))

    return group_msg_id
Ejemplo n.º 6
0
def key_ex_psk_tx(packet: bytes, ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', key_list: 'KeyList',
                  settings: 'Settings') -> None:
    """Add contact and Tx-PSKs."""

    onion_pub_key, tx_mk, _, tx_hk, _, nick_bytes \
        = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH])

    try:
        nick = bytes_to_str(nick_bytes)
    except (struct.error, UnicodeError):
        raise FunctionReturn("Error: Received invalid contact data")

    contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH),
                             bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK,
                             settings.log_messages_by_default,
                             settings.accept_files_by_default,
                             settings.show_notifications_by_default)

    # The Rx-side keys are set as null-byte strings to indicate they have not
    # been added yet. The zero-keys do not allow existential forgeries as
    # `decrypt_assembly_packet`does not allow the use of zero-keys for decryption.
    key_list.add_keyset(onion_pub_key=onion_pub_key,
                        tx_mk=tx_mk,
                        rx_mk=bytes(SYMMETRIC_KEY_LENGTH),
                        tx_hk=tx_hk,
                        rx_hk=bytes(SYMMETRIC_KEY_LENGTH))

    message = f"Added Tx-side PSK for {nick} ({pub_key_to_short_address(onion_pub_key)})."
    local_win = window_list.get_local_window()
    local_win.add_new(ts, message)

    m_print(message, bold=True, tail_clear=True, delay=1)
Ejemplo n.º 7
0
    def _load_keys(self) -> None:
        """Load KeySets from the encrypted database.

        This function first reads and decrypts the database content. It
        then splits the plaintext into a list of 176-byte blocks. Each
        block contains the serialized data of one KeySet. Next, the
        function will remove from the list all dummy KeySets (that start
        with the `dummy_id` byte string). The function will then
        populate the `self.keysets` list with KeySet objects, the data
        of which is sliced and decoded from the dummy-free blocks.
        """
        pt_bytes = self.database.load_database()
        blocks = split_byte_string(pt_bytes, item_len=KEYSET_LENGTH)
        df_blocks = [b for b in blocks if not b.startswith(self.dummy_id)]

        for block in df_blocks:
            if len(block) != KEYSET_LENGTH:
                raise CriticalError("Invalid data in key database.")

            onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, tx_harac_bytes, rx_harac_bytes \
                = separate_headers(block, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH] + [HARAC_LENGTH])

            self.keysets.append(
                KeySet(onion_pub_key=onion_pub_key,
                       tx_mk=tx_mk,
                       rx_mk=rx_mk,
                       tx_hk=tx_hk,
                       rx_hk=rx_hk,
                       tx_harac=bytes_to_int(tx_harac_bytes),
                       rx_harac=bytes_to_int(rx_harac_bytes),
                       store_keys=self.store_keys))
Ejemplo n.º 8
0
Archivo: files.py Proyecto: savg110/tfc
def new_file(
        ts: 'datetime',  # Timestamp of received packet
        packet: bytes,  # Sender of file and file ciphertext
        file_keys: Dict[bytes, bytes],  # Dictionary for file decryption keys
        file_buf: Dict[bytes,
                       Tuple['datetime',
                             bytes]],  # Dictionary for cached file ciphertexts
        contact_list: 'ContactList',  # ContactList object
        window_list: 'WindowList',  # WindowList object
        settings: 'Settings'  # Settings object
) -> None:
    """Validate received file and process or cache it."""
    onion_pub_key, _, file_ct = separate_headers(
        packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH])

    if not contact_list.has_pub_key(onion_pub_key):
        raise SoftError("File from an unknown account.", output=False)

    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    if not contact.file_reception:
        raise SoftError(
            f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.",
            bold=True)

    k = onion_pub_key + blake2b(file_ct)  # Dictionary key

    if k in file_keys:
        decryption_key = file_keys[k]
        process_file(ts, onion_pub_key, file_ct, decryption_key, contact_list,
                     window_list, settings)
        file_keys.pop(k)
    else:
        file_buf[k] = (ts, file_ct)
Ejemplo n.º 9
0
    def load_master_key(self) -> bytes:
        """Derive the master key from password and salt.

        Load the salt, hash, and key derivation settings from the login
        database. Derive the purported master key from the salt and
        entered password. If the BLAKE2b hash of derived master key
        matches the hash in the login database, accept the derived
        master key.
        """
        database_data = self.database.load_database()

        if len(database_data) != MASTERKEY_DB_SIZE:
            raise CriticalError(f"Invalid {self.file_name} database size.")

        salt, key_hash, time_bytes, memory_bytes, parallelism_bytes \
            = separate_headers(database_data, [ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH,
                                               ENCODED_INTEGER_LENGTH, ENCODED_INTEGER_LENGTH])

        time_cost   = bytes_to_int(time_bytes)
        memory_cost = bytes_to_int(memory_bytes)
        parallelism = bytes_to_int(parallelism_bytes)

        while True:
            password = MasterKey.get_password()
            phase("Deriving master key", head=2, offset=len("Password correct"))
            purp_key = argon2_kdf(password, salt, time_cost, memory_cost, parallelism)

            if blake2b(purp_key) == key_hash:
                phase("Password correct", done=True, delay=1)
                clear_screen()
                return purp_key

            phase("Invalid password", done=True, delay=1)
            print_on_previous_line(reps=5)
Ejemplo n.º 10
0
def add_onion_data(command: bytes, queues: 'QueueDict') -> None:
    """Add Onion Service data.

    Separate onion service private key and public keys for
    pending/existing contacts and add them as contacts.

    The ONION_KEY_QUEUE is read by
        relay.onion.onion_service()
    """
    os_private_key, confirmation_code, allow_req_byte, no_pending_bytes, ser_pub_keys \
        = separate_headers(command, [ONION_SERVICE_PRIVATE_KEY_LENGTH, CONFIRM_CODE_LENGTH,
                                     ENCODED_BOOLEAN_LENGTH, ENCODED_INTEGER_LENGTH])

    no_pending = bytes_to_int(no_pending_bytes)
    public_key_list = split_byte_string(ser_pub_keys,
                                        ONION_SERVICE_PUBLIC_KEY_LENGTH)
    pending_public_keys = public_key_list[:no_pending]
    existing_public_keys = public_key_list[no_pending:]

    for onion_pub_key in pending_public_keys:
        add_contact(onion_pub_key, False, queues)
    for onion_pub_key in existing_public_keys:
        add_contact(onion_pub_key, True, queues)

    manage_contact_req(allow_req_byte, queues, notify=False)
    queues[ONION_KEY_QUEUE].put((os_private_key, confirmation_code))
Ejemplo n.º 11
0
def process_message_packet(
        ts: 'datetime',  # Timestamp of received message packet
        assembly_packet_ct: bytes,  # Encrypted assembly packet
        window_list: 'WindowList',  # WindowList object
        packet_list: 'PacketList',  # PacketList object
        contact_list: 'ContactList',  # ContactList object
        key_list: 'KeyList',  # KeyList object
        group_list: 'GroupList',  # GroupList object
        settings: 'Settings',  # Settings object
        file_keys: Dict[bytes, bytes],  # Dictionary of file decryption keys
        message_log: 'MessageLog',  # MessageLog object
) -> None:
    """Process received message packet."""
    command_window = window_list.get_command_window()

    onion_pub_key, origin, assembly_packet_ct = separate_headers(
        assembly_packet_ct,
        [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH])

    if onion_pub_key == LOCAL_PUBKEY:
        raise SoftError("Warning! Received packet masqueraded as a command.",
                        window=command_window)

    if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
        raise SoftError("Error: Received packet had an invalid origin-header.",
                        window=command_window)

    assembly_packet = decrypt_assembly_packet(assembly_packet_ct,
                                              onion_pub_key, origin,
                                              window_list, contact_list,
                                              key_list)

    p_type = (FILE
              if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH].isupper() else
              MESSAGE)
    packet = packet_list.get_packet(onion_pub_key, origin, p_type)
    logging = contact_list.get_contact_by_pub_key(onion_pub_key).log_messages

    try:
        packet.add_packet(assembly_packet)
    except SoftError:
        log_masking_packets(onion_pub_key, origin, logging, settings, packet,
                            message_log)
        raise
    log_masking_packets(onion_pub_key, origin, logging, settings, packet,
                        message_log)

    if packet.is_complete:
        process_complete_message_packet(ts, onion_pub_key, p_type, origin,
                                        logging, packet, window_list,
                                        contact_list, group_list, settings,
                                        message_log, file_keys)
Ejemplo n.º 12
0
    def assemble_and_store_file(self, ts: 'datetime', onion_pub_key: bytes,
                                window_list: 'WindowList') -> None:
        """Assemble file packet and store it."""
        padded = b''.join(
            [p[ASSEMBLY_PACKET_HEADER_LENGTH:] for p in self.assembly_pt_list])
        payload = rm_padding_bytes(padded)

        no_fields = 3 if len(self.assembly_pt_list) > 1 else 2
        *_, payload = separate_headers(payload,
                                       no_fields * [ENCODED_INTEGER_LENGTH])

        process_assembled_file(ts, payload, onion_pub_key, self.contact.nick,
                               self.settings, window_list)
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
    def _load_contacts(self) -> None:
        """Load contacts from the encrypted database.

        This function first reads and decrypts the database content. It
        then splits the plaintext into a list of 1124-byte blocks: each
        block contains the serialized data of one contact. Next, the
        function will remove from the list all dummy contacts (that
        start with dummy contact's public key). The function will then
        populate the `self.contacts` list with Contact objects, the data
        of which is sliced and decoded from the dummy-free blocks.
        """
        with open(self.file_name, 'rb') as f:
            ct_bytes = f.read()

        pt_bytes = auth_and_decrypt(ct_bytes,
                                    self.master_key.master_key,
                                    database=self.file_name)
        blocks = split_byte_string(pt_bytes, item_len=CONTACT_LENGTH)
        df_blocks = [
            b for b in blocks
            if not b.startswith(self.dummy_contact.onion_pub_key)
        ]

        for block in df_blocks:
            if len(block) != CONTACT_LENGTH:
                raise CriticalError("Invalid data in contact database.")

            (onion_pub_key, tx_fingerprint, rx_fingerprint, kex_status_byte,
             log_messages_byte, file_reception_byte,
             notifications_byte, nick_bytes) = separate_headers(
                 block,
                 [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 2 * [FINGERPRINT_LENGTH] +
                 [KEX_STATUS_LENGTH] + 3 * [ENCODED_BOOLEAN_LENGTH])

            self.contacts.append(
                Contact(onion_pub_key=onion_pub_key,
                        tx_fingerprint=tx_fingerprint,
                        rx_fingerprint=rx_fingerprint,
                        kex_status=kex_status_byte,
                        log_messages=bytes_to_bool(log_messages_byte),
                        file_reception=bytes_to_bool(file_reception_byte),
                        notifications=bytes_to_bool(notifications_byte),
                        nick=bytes_to_str(nick_bytes)))
Ejemplo n.º 15
0
    def process_short_header(self,
                             packet:    bytes,
                             packet_ct: Optional[bytes] = None
                             ) -> None:
        """Process short packet."""
        if self.long_active:
            self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list))

        if self.type == FILE:
            self.new_file_packet()
            sh, _, packet = separate_headers(packet, [ASSEMBLY_PACKET_HEADER_LENGTH] + [2*ENCODED_INTEGER_LENGTH])
            packet        = sh + packet

        self.assembly_pt_list = [packet]
        self.long_active      = False
        self.is_complete      = True

        if packet_ct is not None:
            self.log_ct_list = [packet_ct]
Ejemplo n.º 16
0
def check_packet_fate(entries_to_keep: List[bytes], packet: 'Packet',
                      removed: bool, selector: bytes) -> bool:
    """Check whether the packet should be kept."""
    _, header, message = separate_headers(
        packet.assemble_message_packet(),
        [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])

    if header == PRIVATE_MESSAGE_HEADER:
        entries_to_keep.extend(packet.log_ct_list)
        packet.clear_assembly_packets()

    elif header == GROUP_MESSAGE_HEADER:
        group_id, _ = separate_header(message, GROUP_ID_LENGTH)
        if group_id == selector:
            removed = True
        else:
            entries_to_keep.extend(packet.log_ct_list)
            packet.clear_assembly_packets()

    return removed
Ejemplo n.º 17
0
def process_message(
    ts: 'datetime',  # Timestamp of received message packet
    onion_pub_key: bytes,  # Onion address of associated contact
    origin: bytes,  # Origin of message (user / contact)
    logging: bool,  # When True, message will be logged
    packet: 'Packet',  # Packet object
    window_list: 'WindowList',  # WindowList object
    contact_list: 'ContactList',  # ContactList object
    group_list: 'GroupList',  # GroupList object
    message_log: 'MessageLog',  # MessageLog object
    file_keys: Dict[bytes, bytes]  # Dictionary of file decryption keys
) -> None:
    """Process message packet.

    The received message might be a private or group message, or it
    might contain decryption key for file received earlier.

    Each received message contains a whisper header that allows the
    sender to request the message to not be logged. This request will
    be obeyed as long as the recipient does not edit the source code
    below. Thus, the sender should not trust a whisper message is
    never logged.
    """
    whisper_byte, header, assembled = separate_headers(
        packet.assemble_message_packet(),
        [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])
    if len(whisper_byte) != WHISPER_FIELD_LENGTH:
        raise SoftError(
            "Error: Message from contact had an invalid whisper header.")

    whisper = bytes_to_bool(whisper_byte)

    if header == GROUP_MESSAGE_HEADER:
        logging = process_group_message(ts, assembled, onion_pub_key, origin,
                                        whisper, group_list, window_list)

    elif header == PRIVATE_MESSAGE_HEADER:
        window = window_list.get_window(onion_pub_key)
        window.add_new(ts,
                       assembled.decode(),
                       onion_pub_key,
                       origin,
                       output=True,
                       whisper=whisper)

    elif header == FILE_KEY_HEADER:
        nick = process_file_key_message(assembled, onion_pub_key, origin,
                                        contact_list, file_keys)
        raise SoftError(f"Received file decryption key from {nick}",
                        window=window_list.get_command_window())

    else:
        raise SoftError("Error: Message from contact had an invalid header.")

    # Logging
    if whisper:
        raise SoftError("Whisper message complete.", output=False)

    if logging:
        for p in packet.assembly_pt_list:
            write_log_entry(p, onion_pub_key, message_log, origin)
Ejemplo n.º 18
0
    def _load_groups(self) -> None:
        """Load groups from the encrypted database.

        The function first reads, authenticates and decrypts the group
        database data. Next, it slices and decodes the header values
        that help the function to properly de-serialize the database
        content. The function then removes dummy groups based on header
        data. Next, the function updates the group database settings if
        necessary. It then splits group data based on header data into
        blocks, which are further sliced, and processed if necessary,
        to obtain data required to create Group objects. Finally, if
        needed, the function will update the group database content.
        """
        pt_bytes = self.database.load_database()

        # Slice and decode headers
        group_db_headers, pt_bytes = separate_header(pt_bytes,
                                                     GROUP_DB_HEADER_LENGTH)

        padding_for_group_db, padding_for_members, number_of_groups, members_in_largest_group \
            = list(map(bytes_to_int, split_byte_string(group_db_headers, ENCODED_INTEGER_LENGTH)))

        # Slice dummy groups
        bytes_per_group = GROUP_STATIC_LENGTH + padding_for_members * ONION_SERVICE_PUBLIC_KEY_LENGTH
        dummy_data_len = (padding_for_group_db -
                          number_of_groups) * bytes_per_group
        group_data = pt_bytes[:-dummy_data_len]

        update_db = self._check_db_settings(number_of_groups,
                                            members_in_largest_group)
        blocks = split_byte_string(group_data, item_len=bytes_per_group)

        all_pub_keys = self.contact_list.get_list_of_pub_keys()
        dummy_pub_key = onion_address_to_pub_key(DUMMY_MEMBER)

        # Deserialize group objects
        for block in blocks:
            if len(block) != bytes_per_group:
                raise CriticalError("Invalid data in group database.")

            name_bytes, group_id, log_messages_byte, notification_byte, ser_pub_keys \
                = separate_headers(block, [PADDED_UTF32_STR_LENGTH, GROUP_ID_LENGTH] + 2*[ENCODED_BOOLEAN_LENGTH])

            pub_key_list = split_byte_string(
                ser_pub_keys, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH)
            group_pub_keys = [k for k in pub_key_list if k != dummy_pub_key]
            group_members = [
                self.contact_list.get_contact_by_pub_key(k)
                for k in group_pub_keys if k in all_pub_keys
            ]

            self.groups.append(
                Group(name=bytes_to_str(name_bytes),
                      group_id=group_id,
                      log_messages=bytes_to_bool(log_messages_byte),
                      notifications=bytes_to_bool(notification_byte),
                      members=group_members,
                      settings=self.settings,
                      store_groups=self.store_groups))

            update_db |= set(all_pub_keys) > set(group_pub_keys)

        if update_db:
            self.store_groups()
Ejemplo n.º 19
0
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'
    log_file = get_logfile(file_name)
    packet_list = PacketList(settings, contact_list)
    message_log = []  # type: List[MsgTuple]
    group_msg_id = b''

    for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''):
        plaintext = auth_and_decrypt(ct,
                                     master_key.master_key,
                                     database=file_name)

        onion_pub_key, timestamp, origin, assembly_packet = separate_headers(
            plaintext, [
                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 FunctionReturn:
            continue
        if not packet.is_complete:
            continue

        whisper_byte, header, message = separate_headers(
            packet.assemble_message_packet(),
            [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])
        whisper = bytes_to_bool(whisper_byte)

        if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT:
            message_log.append(
                (bytes_to_timestamp(timestamp), message.decode(),
                 onion_pub_key, packet.origin, whisper, False))

        elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP:
            purp_group_id, message = separate_header(message, GROUP_ID_LENGTH)
            if window.group is not None and purp_group_id != window.group.group_id:
                continue

            purp_msg_id, message = separate_header(message,
                                                   GROUP_MSG_ID_LENGTH)
            if packet.origin == ORIGIN_USER_HEADER:
                if purp_msg_id == group_msg_id:
                    continue
                group_msg_id = purp_msg_id

            message_log.append(
                (bytes_to_timestamp(timestamp), message.decode(),
                 onion_pub_key, packet.origin, whisper, False))

    log_file.close()

    print_logs(message_log[-msg_to_load:], export, msg_to_load, window,
               contact_list, group_list, settings)
Ejemplo n.º 20
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 group determined by that group ID are removed.
    """
    ensure_dir(DIR_USER_DATA)
    file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs'
    temp_name = f'{file_name}_temp'
    log_file = get_logfile(file_name)
    packet_list = PacketList(settings, contact_list)
    ct_to_keep = []  # type: List[bytes]
    removed = False
    contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH

    for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''):
        plaintext = auth_and_decrypt(ct,
                                     master_key.master_key,
                                     database=file_name)

        onion_pub_key, _, origin, assembly_packet = separate_headers(
            plaintext, [
                ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH,
                ORIGIN_HEADER_LENGTH
            ])
        if contact:
            if onion_pub_key == selector:
                removed = True
            else:
                ct_to_keep.append(ct)

        else:  # Group
            packet = packet_list.get_packet(onion_pub_key,
                                            origin,
                                            MESSAGE,
                                            log_access=True)
            try:
                packet.add_packet(assembly_packet, ct)
            except FunctionReturn:
                continue
            if not packet.is_complete:
                continue

            _, header, message = separate_headers(
                packet.assemble_message_packet(),
                [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])

            if header == PRIVATE_MESSAGE_HEADER:
                ct_to_keep.extend(packet.log_ct_list)
                packet.clear_assembly_packets()

            elif header == GROUP_MESSAGE_HEADER:
                group_id, _ = separate_header(message, GROUP_ID_LENGTH)
                if group_id == selector:
                    removed = True
                else:
                    ct_to_keep.extend(packet.log_ct_list)
                    packet.clear_assembly_packets()

    log_file.close()

    if os.path.isfile(temp_name):
        os.remove(temp_name)

    with open(temp_name, 'wb+') as f:
        if ct_to_keep:
            f.write(b''.join(ct_to_keep))

    os.remove(file_name)
    os.rename(temp_name, file_name)

    try:
        name = contact_list.get_contact_by_pub_key(selector).nick \
            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 FunctionReturn(f"{action} log entries for {win_type} '{name}'.")
Ejemplo n.º 21
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings', kdk_hashes: List[bytes],
                      packet_hashes: List[bytes], l_queue: 'Queue') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    bootstrap = not key_list.has_local_keyset()
    plaintext = None

    try:
        packet_hash = blake2b(packet)

        # Check if the packet is an old one
        if packet_hash in packet_hashes:
            raise FunctionReturn("Error: Received old local key packet.",
                                 output=False)

        while True:
            m_print("Local key setup",
                    bold=True,
                    head_clear=True,
                    head=1,
                    tail=1)
            kdk = get_b58_key(B58_LOCAL_KEY, settings)
            kdk_hash = blake2b(kdk)

            try:
                plaintext = auth_and_decrypt(packet, kdk)
                break
            except nacl.exceptions.CryptoError:
                # Check if key was an old one
                if kdk_hash in kdk_hashes:
                    m_print("Error: Entered an old local key decryption key.",
                            delay=1)
                    continue

                # Check if the kdk was for a packet further ahead in the queue
                buffer = []  # type: List[Tuple[datetime, bytes]]
                while l_queue.qsize() > 0:
                    tup = l_queue.get()  # type: Tuple[datetime, bytes]
                    if tup not in buffer:
                        buffer.append(tup)

                for i, tup in enumerate(buffer):
                    try:
                        plaintext = auth_and_decrypt(tup[1], kdk)

                        # If we reach this point, decryption was successful.
                        for unexamined in buffer[i + 1:]:
                            l_queue.put(unexamined)
                        buffer = []
                        ts = tup[0]
                        break

                    except nacl.exceptions.CryptoError:
                        continue
                else:
                    # Finished the buffer without finding local key CT
                    # for the kdk. Maybe the kdk is from another session.
                    raise FunctionReturn(
                        "Error: Incorrect key decryption key.", delay=1)

            break

        # This catches PyCharm's weird claim that plaintext might be referenced before assignment
        if plaintext is None:  # pragma: no cover
            raise FunctionReturn("Error: Could not decrypt local key.")

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY,
                                 LOCAL_NICK, KEX_STATUS_LOCAL_KEY,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), False, False, True)

        tx_mk, tx_hk, c_code = separate_headers(plaintext,
                                                2 * [SYMMETRIC_KEY_LENGTH])

        # Add local keyset to keyset database
        key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY,
                            tx_mk=tx_mk,
                            rx_mk=csprng(),
                            tx_hk=tx_hk,
                            rx_hk=csprng())

        # Cache hashes needed to recognize reissued local key packets and key decryption keys.
        packet_hashes.append(packet_hash)
        kdk_hashes.append(kdk_hash)

        # Prevent leak of KDK via terminal history / clipboard
        readline.clear_history()
        os.system(RESET)
        root = tkinter.Tk()
        root.withdraw()
        try:
            if root.clipboard_get() == b58encode(kdk):
                root.clipboard_clear()
        except tkinter.TclError:
            pass
        root.destroy()

        m_print([
            "Local key successfully installed.",
            f"Confirmation code (to Transmitter): {c_code.hex()}"
        ],
                box=True,
                head=1)

        local_win = window_list.get_local_window()
        local_win.add_new(ts, "Added new local key.")

        if bootstrap:
            window_list.active_win = local_win

    except (EOFError, KeyboardInterrupt):
        m_print("Local key setup aborted.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=2)

        if window_list.active_win is not None and not bootstrap:
            window_list.active_win.redraw()

        raise FunctionReturn("Local key setup aborted.", output=False)
Ejemplo n.º 22
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}'.")
Ejemplo n.º 23
0
 def test_too_small_string(self):
     self.assertEqual(
         separate_headers(b"cypherpunk", header_length_list=[1, 2, 10]),
         [b"c", b"yp", b"herpunk", b""])
Ejemplo n.º 24
0
 def test_separate_headers(self):
     self.assertEqual(
         separate_headers(b"cypherpunk", header_length_list=[1, 2, 3]),
         [b"c", b"yp", b"her", b"punk"])
Ejemplo n.º 25
0
def process_message(ts: 'datetime', assembly_packet_ct: bytes,
                    window_list: 'WindowList', packet_list: 'PacketList',
                    contact_list: 'ContactList', key_list: 'KeyList',
                    group_list: 'GroupList', settings: 'Settings',
                    master_key: 'MasterKey', file_keys: Dict[bytes,
                                                             bytes]) -> None:
    """Process received private / group message."""
    local_window = window_list.get_local_window()

    onion_pub_key, origin, assembly_packet_ct = separate_headers(
        assembly_packet_ct,
        [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH])

    if onion_pub_key == LOCAL_PUBKEY:
        raise FunctionReturn(
            "Warning! Received packet masqueraded as a command.",
            window=local_window)
    if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
        raise FunctionReturn(
            "Error: Received packet had an invalid origin-header.",
            window=local_window)

    assembly_packet = decrypt_assembly_packet(assembly_packet_ct,
                                              onion_pub_key, origin,
                                              window_list, contact_list,
                                              key_list)

    p_type = FILE if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH].isupper(
    ) else MESSAGE
    packet = packet_list.get_packet(onion_pub_key, origin, p_type)
    logging = contact_list.get_contact_by_pub_key(onion_pub_key).log_messages

    def log_masking_packets(completed: bool = False) -> None:
        """Add masking packets to log file.

        If logging and log file masking are enabled, this function will
        in case of erroneous transmissions, store the correct number of
        placeholder data packets to log file to hide the quantity of
        communication that log file observation would otherwise reveal.
        """
        if logging and settings.log_file_masking and (packet.log_masking_ctr
                                                      or completed):
            no_masking_packets = len(packet.assembly_pt_list
                                     ) if completed else packet.log_masking_ctr
            for _ in range(no_masking_packets):
                write_log_entry(PLACEHOLDER_DATA, onion_pub_key, settings,
                                master_key, origin)
        packet.log_masking_ctr = 0

    try:
        packet.add_packet(assembly_packet)
    except FunctionReturn:
        log_masking_packets()
        raise
    log_masking_packets()

    if not packet.is_complete:
        return None

    try:
        if p_type == FILE:
            packet.assemble_and_store_file(ts, onion_pub_key, window_list)
            raise FunctionReturn(
                "File storage complete.",
                output=False)  # Raising allows calling log_masking_packets

        elif p_type == MESSAGE:
            whisper_byte, header, assembled = separate_headers(
                packet.assemble_message_packet(),
                [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])
            if len(whisper_byte) != WHISPER_FIELD_LENGTH:
                raise FunctionReturn(
                    "Error: Message from contact had an invalid whisper header."
                )

            whisper = bytes_to_bool(whisper_byte)

            if header == GROUP_MESSAGE_HEADER:
                logging = process_group_message(assembled, ts, onion_pub_key,
                                                origin, whisper, group_list,
                                                window_list)

            elif header == PRIVATE_MESSAGE_HEADER:
                window = window_list.get_window(onion_pub_key)
                window.add_new(ts,
                               assembled.decode(),
                               onion_pub_key,
                               origin,
                               output=True,
                               whisper=whisper)

            elif header == FILE_KEY_HEADER:
                nick = process_file_key_message(assembled, onion_pub_key,
                                                origin, contact_list,
                                                file_keys)
                raise FunctionReturn(
                    f"Received file decryption key from {nick}",
                    window=local_window)

            else:
                raise FunctionReturn(
                    "Error: Message from contact had an invalid header.")

            # Logging
            if whisper:
                raise FunctionReturn("Whisper message complete.", output=False)

            if logging:
                for p in packet.assembly_pt_list:
                    write_log_entry(p, onion_pub_key, settings, master_key,
                                    origin)

    except (FunctionReturn, UnicodeError):
        log_masking_packets(completed=True)
        raise
    finally:
        packet.clear_assembly_packets()
Ejemplo n.º 26
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings', kdk_hashes: List[bytes],
                      packet_hashes: List[bytes],
                      l_queue: 'Queue[Tuple[datetime, bytes]]') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    first_local_key = not key_list.has_local_keyset()

    try:
        if blake2b(packet) in packet_hashes:
            raise SoftError("Error: Received old local key packet.",
                            output=False)

        m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1)

        ts, plaintext = decrypt_local_key(ts, packet, kdk_hashes,
                                          packet_hashes, settings, l_queue)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY,
                                 LOCAL_NICK, KEX_STATUS_LOCAL_KEY,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), False, False, True)

        tx_mk, tx_hk, c_code = separate_headers(plaintext,
                                                2 * [SYMMETRIC_KEY_LENGTH])

        # Add local keyset to keyset database
        key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY,
                            tx_mk=tx_mk,
                            rx_mk=csprng(),
                            tx_hk=tx_hk,
                            rx_hk=csprng())

        m_print([
            "Local key successfully installed.",
            f"Confirmation code (to Transmitter): {c_code.hex()}"
        ],
                box=True,
                head=1)

        cmd_win = window_list.get_command_window()

        if first_local_key:
            window_list.active_win = cmd_win

        raise SoftError("Added new local key.",
                        window=cmd_win,
                        ts=ts,
                        output=False)

    except (EOFError, KeyboardInterrupt):
        m_print("Local key setup aborted.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=2)

        if window_list.active_win is not None and not first_local_key:
            window_list.active_win.redraw()

        raise SoftError("Local key setup aborted.", output=False)