Esempio n. 1
0
def determine_selector(selection: str, contact_list: 'ContactList',
                       group_list: 'GroupList') -> bytes:
    """Determine selector (group ID or Onion Service public key)."""
    if selection in contact_list.contact_selectors():
        selector = contact_list.get_contact_by_address_or_nick(
            selection).onion_pub_key

    elif selection in group_list.get_list_of_group_names():
        selector = group_list.get_group(selection).group_id

    elif len(selection) == ONION_ADDRESS_LENGTH:
        if validate_onion_addr(selection):
            raise SoftError("Error: Invalid account.", head_clear=True)
        selector = onion_address_to_pub_key(selection)

    elif len(selection) == GROUP_ID_ENC_LENGTH:
        try:
            selector = b58decode(selection)
        except ValueError:
            raise SoftError("Error: Invalid group ID.", head_clear=True)

    else:
        raise SoftError("Error: Unknown selector.", head_clear=True)

    return selector
Esempio n. 2
0
def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
    """Manage incoming contact requests."""
    existing_contacts = []  # type: List[bytes]
    contact_requests = []  # type: List[bytes]

    request_queue = queues[CONTACT_REQ_QUEUE]
    contact_queue = queues[C_REQ_MGMT_QUEUE]
    setting_queue = queues[C_REQ_STATE_QUEUE]
    show_requests = True

    while True:
        with ignored(EOFError, KeyboardInterrupt):
            while request_queue.qsize() == 0:
                time.sleep(0.1)
            purp_onion_address = request_queue.get()

            while setting_queue.qsize() != 0:
                show_requests = setting_queue.get()

            # Update list of existing contacts
            while contact_queue.qsize() > 0:
                command, ser_onion_pub_keys = contact_queue.get()
                onion_pub_key_list = split_byte_string(
                    ser_onion_pub_keys, ONION_SERVICE_PUBLIC_KEY_LENGTH)

                if command == RP_ADD_CONTACT_HEADER:
                    existing_contacts = list(
                        set(existing_contacts) | set(onion_pub_key_list))
                elif command == RP_REMOVE_CONTACT_HEADER:
                    existing_contacts = list(
                        set(existing_contacts) - set(onion_pub_key_list))

            if validate_onion_addr(purp_onion_address) == '':
                onion_pub_key = onion_address_to_pub_key(purp_onion_address)
                if onion_pub_key in existing_contacts:
                    continue
                if onion_pub_key in contact_requests:
                    continue

                if show_requests:
                    ts_fmt = datetime.now().strftime(
                        '%b %d - %H:%M:%S.%f')[:-4]
                    m_print([
                        f"{ts_fmt} - New contact request from an unknown TFC account:",
                        purp_onion_address
                    ],
                            box=True)
                contact_requests.append(onion_pub_key)

            if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0:
                break
Esempio n. 3
0
def remove_contact(user_input: 'UserInput', window: 'TxWindow',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   settings: 'Settings', queues: 'QueueDict',
                   master_key: 'MasterKey') -> None:
    """Remove contact from TFC."""
    if settings.traffic_masking:
        raise SoftError("Error: Command is disabled during traffic masking.",
                        head_clear=True)

    try:
        selection = user_input.plaintext.split()[1]
    except IndexError:
        raise SoftError("Error: No account specified.", head_clear=True)

    if not yes(f"Remove contact '{selection}'?", abort=False, head=1):
        raise SoftError("Removal of contact aborted.",
                        head=0,
                        delay=1,
                        tail_clear=True)

    if selection in contact_list.contact_selectors():
        onion_pub_key = contact_list.get_contact_by_address_or_nick(
            selection).onion_pub_key

    else:
        if validate_onion_addr(selection):
            raise SoftError("Error: Invalid selection.",
                            head=0,
                            delay=1,
                            tail_clear=True)
        onion_pub_key = onion_address_to_pub_key(selection)

    receiver_command = CONTACT_REM + onion_pub_key
    queue_command(receiver_command, settings, queues)

    with ignored(SoftError):
        remove_logs(contact_list, group_list, settings, master_key,
                    onion_pub_key)

    queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key))

    relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key
    queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])

    target = determine_target(selection, onion_pub_key, contact_list)

    if any([g.remove_members([onion_pub_key]) for g in group_list]):
        m_print(f"Removed {target} from group(s).", tail=1)

    check_for_window_deselection(onion_pub_key, window, group_list)
Esempio n. 4
0
    def generate_dummy_contact() -> Contact:
        """Generate a dummy Contact object.

        The dummy contact simplifies the code around the constant length
        serialization when the data is stored to, or read from the
        database.
        """
        return Contact(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT),
                       nick=DUMMY_NICK,
                       tx_fingerprint=bytes(FINGERPRINT_LENGTH),
                       rx_fingerprint=bytes(FINGERPRINT_LENGTH),
                       kex_status=KEX_STATUS_NONE,
                       log_messages=False,
                       file_reception=False,
                       notifications=False)
Esempio n. 5
0
    def generate_dummy_keyset() -> 'KeySet':
        """Generate a dummy KeySet object.

        The dummy KeySet simplifies the code around the constant length
        serialization when the data is stored to, or read from the
        database.

        In case the dummy keyset would ever be loaded accidentally, it
        uses a set of random keys to prevent decryption by eavesdropper.
        """
        return KeySet(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT),
                      tx_mk=csprng(),
                      rx_mk=csprng(),
                      tx_hk=csprng(),
                      rx_hk=csprng(),
                      tx_harac=INITIAL_HARAC,
                      rx_harac=INITIAL_HARAC,
                      store_keys=lambda: None)
Esempio n. 6
0
def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
    """Manage displayed contact requests."""
    existing_contacts = []  # type: List[bytes]
    displayed_requests = []  # type: List[bytes]

    request_queue = queues[CONTACT_REQ_QUEUE]
    contact_queue = queues[C_REQ_MGMT_QUEUE]
    setting_queue = queues[C_REQ_STATE_QUEUE]
    account_queue = queues[ACCOUNT_SEND_QUEUE]
    show_requests = True

    while True:
        with ignored(EOFError, KeyboardInterrupt):
            while request_queue.qsize() == 0:
                time.sleep(0.1)
            purp_onion_address = request_queue.get()

            while setting_queue.qsize() != 0:
                show_requests = setting_queue.get()

            existing_contacts = update_list_of_existing_contacts(
                contact_queue, existing_contacts)

            if validate_onion_addr(purp_onion_address) == '':
                onion_pub_key = onion_address_to_pub_key(purp_onion_address)
                if onion_pub_key in existing_contacts:
                    continue
                if onion_pub_key in displayed_requests:
                    continue

                if show_requests:
                    ts = datetime.now().strftime('%b %d - %H:%M:%S.%f')[:-4]
                    m_print([
                        f"{ts} - New contact request from an unknown TFC account:",
                        purp_onion_address
                    ],
                            box=True)
                    account_queue.put(purp_onion_address)

                displayed_requests.append(onion_pub_key)

            if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0:
                break
Esempio n. 7
0
def remove_log(user_input: 'UserInput', contact_list: 'ContactList',
               group_list: 'GroupList', settings: 'Settings',
               queues: 'QueueDict', master_key: 'MasterKey') -> None:
    """Remove log entries for contact or group."""
    try:
        selection = user_input.plaintext.split()[1]
    except IndexError:
        raise FunctionReturn("Error: No contact/group specified.",
                             head_clear=True)

    if not yes(f"Remove logs for {selection}?", abort=False, head=1):
        raise FunctionReturn("Log file removal aborted.",
                             tail_clear=True,
                             delay=1,
                             head=0)

    # Determine selector (group ID or Onion Service public key) from command parameters
    if selection in contact_list.contact_selectors():
        selector = contact_list.get_contact_by_address_or_nick(
            selection).onion_pub_key

    elif selection in group_list.get_list_of_group_names():
        selector = group_list.get_group(selection).group_id

    elif len(selection) == ONION_ADDRESS_LENGTH:
        if validate_onion_addr(selection):
            raise FunctionReturn("Error: Invalid account.", head_clear=True)
        selector = onion_address_to_pub_key(selection)

    elif len(selection) == GROUP_ID_ENC_LENGTH:
        try:
            selector = b58decode(selection)
        except ValueError:
            raise FunctionReturn("Error: Invalid group ID.", head_clear=True)

    else:
        raise FunctionReturn("Error: Unknown selector.", head_clear=True)

    # Remove logs that match the selector
    command = LOG_REMOVE + selector
    queue_command(command, settings, queues)

    remove_logs(contact_list, group_list, settings, master_key, selector)
Esempio n. 8
0
    def serialize_g(self) -> bytes:
        """Return group data as a constant length bytestring.

        This function serializes the group's data into a bytestring
        that always has a constant length. The exact length depends on
        the attribute `max_number_of_group_members` of TFC's Settings
        object. With the default setting of 50 members per group, the
        length of the serialized data is
            1024 + 4 + 2*1 + 50*32 = 2630 bytes
        The purpose of the constant length serialization is to hide any
        metadata the ciphertext length of the group database could
        reveal.
        """
        members = self.get_list_of_member_pub_keys()
        number_of_dummies = self.settings.max_number_of_group_members - len(
            self.members)
        members += number_of_dummies * [onion_address_to_pub_key(DUMMY_MEMBER)]
        member_bytes = b''.join(members)

        return (str_to_bytes(self.name) + self.group_id +
                bool_to_bytes(self.log_messages) +
                bool_to_bytes(self.notifications) + member_bytes)
Esempio n. 9
0
 def test_conversion_back_and_forth(self):
     pub_key = os.urandom(SYMMETRIC_KEY_LENGTH)
     self.assertEqual(
         onion_address_to_pub_key(pub_key_to_onion_address(pub_key)),
         pub_key)
Esempio n. 10
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()
Esempio n. 11
0
 def test_local_pubkey(self):
     """Test that local key's reserved public key is valid."""
     self.assertEqual(src.common.statics.LOCAL_PUBKEY,
                      onion_address_to_pub_key(src.common.statics.LOCAL_ID))
Esempio n. 12
0
def add_new_contact(contact_list: 'ContactList', group_list: 'GroupList',
                    settings: 'Settings', queues: 'QueueDict',
                    onion_service: 'OnionService') -> None:
    """Prompt for contact account details and initialize desired key exchange.

    This function requests the minimum amount of data about the
    recipient as possible. The TFC account of contact is the same as the
    Onion URL of contact's v3 Tor Onion Service. Since the accounts are
    random and hard to remember, the user has to choose a nickname for
    their contact. Finally, the user must select the key exchange method:
    ECDHE for convenience in a pre-quantum world, or PSK for situations
    where physical key exchange is possible, and ciphertext must remain
    secure even after sufficient QTMs are available to adversaries.

    Before starting the key exchange, Transmitter Program exports the
    public key of contact's Onion Service to Relay Program on their
    Networked Computer so that a connection to the contact can be
    established.
    """
    try:
        if settings.traffic_masking:
            raise SoftError(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

        if len(contact_list) >= settings.max_number_of_contacts:
            raise SoftError(
                f"Error: TFC settings only allow {settings.max_number_of_contacts} accounts.",
                head_clear=True)

        m_print("Add new contact", head=1, bold=True, head_clear=True)

        m_print([
            "Your TFC account is", onion_service.user_onion_address, '',
            "Warning!", "Anyone who knows this account",
            "can see when your TFC is online"
        ],
                box=True)

        contact_address = get_onion_address_from_user(
            onion_service.user_onion_address, queues)
        onion_pub_key = onion_address_to_pub_key(contact_address)

        contact_nick = box_input(
            "Contact nick",
            expected_len=
            ONION_ADDRESS_LENGTH,  # Limited to 255 but such long nick is unpractical.
            validator=validate_nick,
            validator_args=(contact_list, group_list, onion_pub_key)).strip()

        key_exchange = box_input(f"Key exchange ([{ECDHE}],PSK) ",
                                 default=ECDHE,
                                 expected_len=28,
                                 validator=validate_key_exchange).strip()

        relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_ADD_NEW_CONTACT + onion_pub_key
        queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])

        if key_exchange.upper() in ECDHE:
            start_key_exchange(onion_pub_key, contact_nick, contact_list,
                               settings, queues)

        elif key_exchange.upper() in PSK:
            create_pre_shared_key(onion_pub_key, contact_nick, contact_list,
                                  settings, onion_service, queues)

    except (EOFError, KeyboardInterrupt):
        raise SoftError("Contact creation aborted.",
                        head=2,
                        delay=1,
                        tail_clear=True)
Esempio n. 13
0
def remove_contact(user_input: 'UserInput', window: 'TxWindow',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   settings: 'Settings', queues: 'QueueDict',
                   master_key: 'MasterKey') -> None:
    """Remove contact from TFC."""
    if settings.traffic_masking:
        raise FunctionReturn(
            "Error: Command is disabled during traffic masking.",
            head_clear=True)

    try:
        selection = user_input.plaintext.split()[1]
    except IndexError:
        raise FunctionReturn("Error: No account specified.", head_clear=True)

    if not yes(f"Remove contact '{selection}'?", abort=False, head=1):
        raise FunctionReturn("Removal of contact aborted.",
                             head=0,
                             delay=1,
                             tail_clear=True)

    if selection in contact_list.contact_selectors():
        onion_pub_key = contact_list.get_contact_by_address_or_nick(
            selection).onion_pub_key

    else:
        if validate_onion_addr(selection):
            raise FunctionReturn("Error: Invalid selection.",
                                 head=0,
                                 delay=1,
                                 tail_clear=True)
        else:
            onion_pub_key = onion_address_to_pub_key(selection)

    receiver_command = CONTACT_REM + onion_pub_key
    queue_command(receiver_command, settings, queues)

    with ignored(FunctionReturn):
        remove_logs(contact_list, group_list, settings, master_key,
                    onion_pub_key)

    queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key))

    relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key
    queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])

    if onion_pub_key in contact_list.get_list_of_pub_keys():
        contact = contact_list.get_contact_by_pub_key(onion_pub_key)
        target = f"{contact.nick} ({contact.short_address})"
        contact_list.remove_contact_by_pub_key(onion_pub_key)
        m_print(f"Removed {target} from contacts.", head=1, tail=1)
    else:
        target = f"{selection[:TRUNC_ADDRESS_LENGTH]}"
        m_print(f"Transmitter has no {target} to remove.", head=1, tail=1)

    if any([g.remove_members([onion_pub_key]) for g in group_list]):
        m_print(f"Removed {target} from group(s).", tail=1)

    if window.type == WIN_TYPE_CONTACT:
        if onion_pub_key == window.uid:
            window.deselect()

    if window.type == WIN_TYPE_GROUP:
        for c in window:
            if c.onion_pub_key == onion_pub_key:
                window.update_window(group_list)

                # If the last member of the group is removed, deselect
                # the group. Deselection is not done in
                # update_group_win_members because it would prevent
                # selecting the empty group for group related commands
                # such as notifications.
                if not window.window_contacts:
                    window.deselect()