Пример #1
0
def clear_screens(user_input: 'UserInput', window: 'TxWindow',
                  settings: 'Settings', queues: 'QueueDict') -> None:
    """Clear/reset screen of Source, Destination, and Networked Computer.

    Only send an unencrypted command to Networked Computer if traffic
    masking is disabled.

    With clear command, sending only the command header is enough.
    However, as reset command removes the ephemeral message log on
    Receiver Program, Transmitter Program must define the window to
    reset (in case, e.g., previous window selection command packet
    dropped, and active window state is inconsistent between the
    TCB programs).
    """
    clear = user_input.plaintext.split()[0] == CLEAR

    command = CLEAR_SCREEN if clear else RESET_SCREEN + window.uid
    queue_command(command, settings, queues)

    clear_screen()

    if not settings.traffic_masking:
        pt_cmd = UNENCRYPTED_SCREEN_CLEAR if clear else UNENCRYPTED_SCREEN_RESET
        packet = UNENCRYPTED_DATAGRAM_HEADER + pt_cmd
        queue_to_nc(packet, queues[RELAY_PACKET_QUEUE])

    if not clear:
        readline.clear_history()
        reset_terminal()
Пример #2
0
def exit_tfc(settings: 'Settings', queues: 'QueueDict',
             gateway: 'Gateway') -> None:
    """Exit TFC on all three computers.

    To exit TFC as fast as possible, this function starts by clearing
    all command queues before sending the exit command to Receiver
    Program. It then sends an unencrypted exit command to Relay Program
    on Networked Computer. As the `sender_loop` process loads the
    unencrypted exit command from queue, it detects the user's
    intention, and after outputting the packet, sends the EXIT signal to
    Transmitter Program's main() method that's running the
    `monitor_processes` loop. Upon receiving the EXIT signal,
    `monitor_processes` kills all Transmitter Program's processes and
    exits the program.

    During local testing, this function adds some delays to prevent TFC
    programs from dying when sockets disconnect.
    """
    for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]:
        while queues[q].qsize() > 0:
            queues[q].get()

    queue_command(EXIT_PROGRAM, settings, queues)

    if not settings.traffic_masking:
        if settings.local_testing_mode:
            time.sleep(LOCAL_TESTING_PACKET_DELAY)
            time.sleep(gateway.settings.data_diode_sockets * 1.5)
        else:
            time.sleep(gateway.settings.race_condition_delay)

    relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_EXIT_COMMAND
    queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
Пример #3
0
def get_onion_address_from_user(onion_address_user: str,
                                queues: 'QueueDict') -> str:
    """Get contact's Onion Address from user."""
    while True:
        onion_address_contact = box_input("Contact account",
                                          expected_len=ONION_ADDRESS_LENGTH)
        error_msg = validate_onion_addr(onion_address_contact,
                                        onion_address_user)

        if error_msg:
            m_print(error_msg, head=1)
            print_on_previous_line(reps=5, delay=1)

            if error_msg not in [
                    "Error: Invalid account length.",
                    "Error: Account must be in lower case.",
                    "Error: Can not add reserved account.",
                    "Error: Can not add own account."
            ]:
                relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_ACCOUNT_CHECK + onion_address_contact.encode(
                )
                queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
            continue

        return onion_address_contact
Пример #4
0
def deliver_local_key(local_key_packet: bytes, kek: bytes, c_code: bytes,
                      settings: 'Settings', queues: 'QueueDict') -> None:
    """Deliver encrypted local key to Destination Computer."""
    nc_bypass_msg(NC_BYPASS_START, settings)
    queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])

    while True:
        print_key("Local key decryption key (to Receiver)", kek, settings)
        purp_code = ask_confirmation_code("Receiver")
        if purp_code == c_code.hex():
            nc_bypass_msg(NC_BYPASS_STOP, settings)
            break
        elif purp_code == "":
            phase("Resending local key", head=2)
            queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
            phase(DONE)
            print_on_previous_line(
                reps=(9 if settings.local_testing_mode else 10))
        else:
            m_print([
                "Incorrect confirmation code. If Receiver did not receive",
                "the encrypted local key, resend it by pressing <Enter>."
            ],
                    head=1)
            print_on_previous_line(
                reps=(9 if settings.local_testing_mode else 10), delay=2)
Пример #5
0
def group_add_member(group_name: str,
                     purp_members: List['bytes'],
                     contact_list: 'ContactList',
                     group_list: 'GroupList',
                     settings: 'Settings',
                     queues: 'QueueDict',
                     master_key: 'MasterKey',
                     _: Optional[bytes] = None) -> None:
    """Add new member(s) to a specified group."""
    if group_name not in group_list.get_list_of_group_names():
        if not yes(f"Group {group_name} was not found. Create new group?",
                   abort=False,
                   head=1):
            raise SoftError("Group creation aborted.",
                            head=0,
                            delay=1,
                            tail_clear=True)
        group_create(group_name, purp_members, contact_list, group_list,
                     settings, queues, master_key)
        return None

    purp_pub_keys = set(purp_members)
    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_adding = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_pub_keys_set = set(pub_keys & purp_pub_keys)
    new_in_group_set = set(ok_pub_keys_set - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    rejected = list(purp_pub_keys - pub_keys)
    already_in_g = list(before_adding & purp_pub_keys)
    new_in_group = list(new_in_group_set)
    ok_pub_keys = list(ok_pub_keys_set)

    if len(end_assembly) > settings.max_number_of_group_members:
        raise SoftError(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.",
            head_clear=True)

    group = group_list.get_group(group_name)
    group.add_members(
        [contact_list.get_contact_by_pub_key(k) for k in new_in_group])

    command = GROUP_ADD + group.group_id + b''.join(ok_pub_keys)
    queue_command(command, settings, queues)

    group_management_print(ADDED_MEMBERS, new_in_group, contact_list,
                           group_name)
    group_management_print(ALREADY_MEMBER, already_in_g, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    if new_in_group:
        if yes("Publish the list of new members to involved?", abort=False):
            add_packet = (GROUP_MSG_MEMBER_ADD_HEADER + group.group_id +
                          int_to_bytes(len(before_adding)) +
                          b''.join(before_adding) + b''.join(new_in_group))
            queue_to_nc(add_packet, queues[RELAY_PACKET_QUEUE])
Пример #6
0
def group_create(group_name:   str,
                 purp_members: List[bytes],
                 contact_list: 'ContactList',
                 group_list:   'GroupList',
                 settings:     'Settings',
                 queues:       'QueueDict',
                 _:            'MasterKey',
                 group_id:     Optional[bytes] = None
                 ) -> None:
    """Create a new group.
    
    Validate the group name and determine what members can be added.
    """
    error_msg = validate_group_name(group_name, contact_list, group_list)
    if error_msg:
        raise FunctionReturn(error_msg, head_clear=True)

    public_keys   = set(contact_list.get_list_of_pub_keys())
    purp_pub_keys = set(purp_members)
    accepted      = list(purp_pub_keys & public_keys)
    rejected      = list(purp_pub_keys - public_keys)

    if len(accepted) > settings.max_number_of_group_members:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} "
                             f"members per group.", head_clear=True)

    if len(group_list) == settings.max_number_of_groups:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_groups} groups.", head_clear=True)

    header = GROUP_MSG_INVITE_HEADER if group_id is None else GROUP_MSG_JOIN_HEADER

    if group_id is None:
        while True:
            group_id = os.urandom(GROUP_ID_LENGTH)
            if group_id not in group_list.get_list_of_group_ids():
                break

    group_list.add_group(group_name,
                         group_id,
                         settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         members=[contact_list.get_contact_by_pub_key(k) for k in accepted])

    command = GROUP_CREATE + group_id + group_name.encode() + US_BYTE + b''.join(accepted)
    queue_command(command, settings, queues)

    group_management_print(NEW_GROUP,        accepted, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name)

    if accepted:
        if yes("Publish the list of group members to participants?", abort=False):
            create_packet = header + group_id + b''.join(accepted)
            queue_to_nc(create_packet, queues[RELAY_PACKET_QUEUE])

    else:
        m_print(f"Created an empty group '{group_name}'.", bold=True, head=1)
Пример #7
0
 def queue_delayer():
     """Place packets to queue after delay."""
     time.sleep(0.01)
     queues[WINDOW_SELECT_QUEUE].put(window.window_contacts)
     time.sleep(0.01)
     queue_command(b'test',            settings, queues)                                              # 1
     queue_message(user_input, window, settings, queues)                                              # 2
     queue_message(user_input, window, settings, queues)                                              # 3
     queue_command(b'test',            settings, queues)                                              # 4
     queues[TM_NOISE_COMMAND_QUEUE].put((C_N_HEADER + bytes(PADDING_LENGTH)))                         # 5
     queue_to_nc(UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_EXIT_COMMAND, queues[RELAY_PACKET_QUEUE])  # 6
     queue_to_nc(UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND, queues[RELAY_PACKET_QUEUE])  # 7
     queues[SENDER_MODE_QUEUE].put(settings)
Пример #8
0
def group_rm_member(group_name: str,
                    purp_members: List[bytes],
                    contact_list: 'ContactList',
                    group_list: 'GroupList',
                    settings: 'Settings',
                    queues: 'QueueDict',
                    master_key: 'MasterKey',
                    _: Optional[bytes] = None) -> None:
    """Remove member(s) from the specified group or remove the group itself."""
    if not purp_members:
        group_rm_group(group_name, contact_list, group_list, settings, queues,
                       master_key)

    if group_name not in group_list.get_list_of_group_names():
        raise FunctionReturn(f"Group '{group_name}' does not exist.",
                             head_clear=True)

    purp_pub_keys = set(purp_members)
    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_removal = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_pub_keys_set = set(purp_pub_keys & pub_keys)
    removable_set = set(before_removal & ok_pub_keys_set)

    remaining = list(before_removal - removable_set)
    not_in_group = list(ok_pub_keys_set - before_removal)
    rejected = list(purp_pub_keys - pub_keys)
    removable = list(removable_set)
    ok_pub_keys = list(ok_pub_keys_set)

    group = group_list.get_group(group_name)
    group.remove_members(removable)

    command = GROUP_REMOVE + group.group_id + b''.join(ok_pub_keys)
    queue_command(command, settings, queues)

    group_management_print(REMOVED_MEMBERS, removable, contact_list,
                           group_name)
    group_management_print(NOT_IN_GROUP, not_in_group, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    if removable and remaining and yes(
            "Publish the list of removed members to remaining members?",
            abort=False):
        rem_packet = (GROUP_MSG_MEMBER_REM_HEADER + group.group_id +
                      int_to_bytes(len(remaining)) + b''.join(remaining) +
                      b''.join(removable))
        queue_to_nc(rem_packet, queues[RELAY_PACKET_QUEUE])
Пример #9
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)
Пример #10
0
def group_rm_group(group_name: str,
                   contact_list: 'ContactList',
                   group_list: 'GroupList',
                   settings: 'Settings',
                   queues: 'QueueDict',
                   master_key: 'MasterKey',
                   _: Optional[bytes] = None) -> None:
    """Remove the group with its members."""
    if not yes(f"Remove group '{group_name}'?", abort=False):
        raise FunctionReturn("Group removal aborted.",
                             head=0,
                             delay=1,
                             tail_clear=True)

    if group_name in group_list.get_list_of_group_names():
        group_id = group_list.get_group(group_name).group_id
    else:
        try:
            group_id = b58decode(group_name)
        except ValueError:
            raise FunctionReturn("Error: Invalid group name/ID.",
                                 head_clear=True)

    command = LOG_REMOVE + group_id
    queue_command(command, settings, queues)

    command = GROUP_DELETE + group_id
    queue_command(command, settings, queues)

    if group_list.has_group(group_name):
        with ignored(FunctionReturn):
            remove_logs(contact_list, group_list, settings, master_key,
                        group_id)
    else:
        raise FunctionReturn(
            f"Transmitter has no group '{group_name}' to remove.")

    group = group_list.get_group(group_name)
    if not group.empty() and yes("Notify members about leaving the group?",
                                 abort=False):
        exit_packet = (GROUP_MSG_EXIT_GROUP_HEADER + group.group_id +
                       b''.join(group.get_list_of_member_pub_keys()))
        queue_to_nc(exit_packet, queues[RELAY_PACKET_QUEUE])

    group_list.remove_group_by_name(group_name)
    raise FunctionReturn(f"Removed group '{group_name}'.",
                         head=0,
                         delay=1,
                         tail_clear=True,
                         bold=True)
Пример #11
0
def change_setting_value(setting: str, value: str,
                         relay_settings: Dict[str, bytes], queues: 'QueueDict',
                         contact_list: 'ContactList', group_list: 'GroupList',
                         settings: 'Settings', gateway: 'Gateway') -> None:
    """Change setting value in setting databases."""
    if setting in gateway.settings.key_list:
        gateway.settings.change_setting(setting, value)
    else:
        settings.change_setting(setting, value, contact_list, group_list)

    receiver_command = CH_SETTING + setting.encode() + US_BYTE + value.encode()

    queue_command(receiver_command, settings, queues)

    if setting in relay_settings:
        if setting == 'allow_contact_requests':
            value = bool_to_bytes(settings.allow_contact_requests).decode()
        relay_command = UNENCRYPTED_DATAGRAM_HEADER + relay_settings[
            setting] + value.encode()
        queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
Пример #12
0
def wipe(settings: 'Settings',
         queues:   'QueueDict',
         gateway:  'Gateway'
         ) -> None:
    """\
    Reset terminals, wipe all TFC user data from Source, Networked, and
    Destination Computer, and power all three systems off.

    The purpose of the wipe command is to provide additional protection
    against physical attackers, e.g. in situation where a dissident gets
    a knock on their door. By overwriting and deleting user data the
    program prevents access to encrypted databases. Additional security
    should be sought with full disk encryption (FDE).

    Unfortunately, no effective tool for overwriting RAM currently exists.
    However, as long as Source and Destination Computers use FDE and
    DDR3 memory, recovery of sensitive data becomes impossible very fast:
        https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf
    """
    if not yes("Wipe all user data and power off systems?", abort=False):
        raise FunctionReturn("Wipe command aborted.", head_clear=True)

    clear_screen()

    for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]:
        while queues[q].qsize() != 0:
            queues[q].get()

    queue_command(WIPE_USR_DATA, settings, queues)

    if not settings.traffic_masking:
        if settings.local_testing_mode:
            time.sleep(0.8)
            time.sleep(gateway.settings.data_diode_sockets * 2.2)
        else:
            time.sleep(gateway.settings.race_condition_delay)

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

    os.system(RESET)
Пример #13
0
        def queue_delayer():
            """Place datagrams into queue after delay."""
            time.sleep(delay)
            queue_command(b'test', settings, queues)

            time.sleep(delay)
            queue_to_nc(
                PUBLIC_KEY_DATAGRAM_HEADER + TFC_PUBLIC_KEY_LENGTH * b'a' +
                nick_to_pub_key('Alice'),  # 1
                queues[RELAY_PACKET_QUEUE])

            time.sleep(delay)
            queue_to_nc(UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND,
                        queues[RELAY_PACKET_QUEUE])  # 2

            time.sleep(delay)
            queue_to_nc(UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_EXIT_COMMAND,
                        queues[RELAY_PACKET_QUEUE])  # 3

            time.sleep(delay)
            queues[KEY_MANAGEMENT_QUEUE].put((
                KDB_ADD_ENTRY_HEADER,
                LOCAL_PUBKEY,  # 4
                SYMMETRIC_KEY_LENGTH * b'a',
                SYMMETRIC_KEY_LENGTH * b'a',
                SYMMETRIC_KEY_LENGTH * b'a',
                SYMMETRIC_KEY_LENGTH * b'a'))

            time.sleep(delay)
            queue_message(user_input, window, settings, queues)  # 5

            time.sleep(delay)
            queue_message(user_input, window, settings, queues)  # 6

            time.sleep(delay)
            queues[KEY_MANAGEMENT_QUEUE].put(
                (KDB_ADD_ENTRY_HEADER, nick_to_pub_key('Alice'),
                 SYMMETRIC_KEY_LENGTH * b'a', SYMMETRIC_KEY_LENGTH * b'a',
                 SYMMETRIC_KEY_LENGTH * b'a', SYMMETRIC_KEY_LENGTH * b'a'))

            time.sleep(delay)
            queue_message(user_input, window, settings, queues)  # 7

            time.sleep(delay)
            queue_message(user_input, window, settings, queues)  # 8

            time.sleep(delay)
            queues[SENDER_MODE_QUEUE].put(settings)
Пример #14
0
def exchange_public_keys(
    onion_pub_key: bytes,
    tfc_public_key_user: bytes,
    kdk_hash: bytes,
    contact: 'Contact',
    settings: 'Settings',
    queues: 'QueueDict',
) -> bytes:
    """Exchange public keys with contact.

    This function outputs the user's public key and waits for user to
    enter the public key of the contact. If the User presses <Enter>,
    the function will resend the users' public key to contact.
    """
    public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
    queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])

    while True:
        try:
            tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings,
                                                 contact.short_address)
        except ValueError as invalid_pub_key:
            invalid_key = str(invalid_pub_key).encode()

            # Do not send packet to Relay Program if the user has for some reason
            # managed to embed the local key decryption key inside the public key.
            substrings = split_to_substrings(invalid_key,
                                             ENCODED_B58_KDK_LENGTH)
            safe_string = not any(
                blake2b(substring) == kdk_hash for substring in substrings)

            if safe_string:
                public_key_packet = (UNENCRYPTED_DATAGRAM_HEADER +
                                     UNENCRYPTED_PUBKEY_CHECK + onion_pub_key +
                                     invalid_key)
                queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])
            continue

        if tfc_public_key_contact == b'':
            public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
            queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])
            continue

        return tfc_public_key_contact
Пример #15
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()
Пример #16
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)
Пример #17
0
def start_key_exchange(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Contact's nickname
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Start X448 key exchange with the recipient.

    This function first creates the X448 key pair. It then outputs the
    public key to Relay Program on Networked Computer, that passes the
    public key to contact's Relay Program. When contact's public key
    reaches the user's Relay Program, the user will manually copy the
    key into their Transmitter Program.

    The X448 shared secret is used to create unidirectional message and
    header keys, that will be used in forward secret communication. This
    is followed by the fingerprint verification where the user manually
    authenticates the public key.

    Once the fingerprint has been accepted, this function will add the
    contact/key data to contact/key databases, and export that data to
    the Receiver Program on Destination Computer. The transmission is
    encrypted with the local key.

    ---

    TFC provides proactive security by making fingerprint verification
    part of the key exchange. This prevents the situation where the
    users don't know about the feature, and thus helps minimize the risk
    of MITM attack.

    The fingerprints can be skipped by pressing Ctrl+C. This feature is
    not advertised however, because verifying fingerprints the only
    strong way to be sure TFC is not under MITM attack. When
    verification is skipped, TFC marks the contact's X448 keys as
    "Unverified". The fingerprints can later be verified with the
    `/verify` command: answering `yes` to the question on whether the
    fingerprints match, marks the X448 keys as "Verified".

    Variable naming:
        tx = user's key     rx = contact's key    fp = fingerprint
        mk = message key    hk = header key
    """
    if not contact_list.has_pub_key(onion_pub_key):
        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)
    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    # Generate new private key or load cached private key
    if contact.tfc_private_key is None:
        tfc_private_key_user = X448.generate_private_key()
    else:
        tfc_private_key_user = contact.tfc_private_key

    try:
        tfc_public_key_user = X448.derive_public_key(tfc_private_key_user)

        # Import public key of contact
        while True:
            public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
            queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])

            tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings,
                                                 contact.short_address)
            if tfc_public_key_contact != b'':
                break

        # Validate public key of contact
        if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH:
            m_print([
                "Warning!", "Received invalid size public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Invalid public key length",
                                 output=False)

        if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH):
            # The public key of contact is zero with negligible probability,
            # therefore we assume such key is malicious and attempts to set
            # the shared key to zero.
            m_print([
                "Warning!", "Received a malicious zero-public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Zero public key", output=False)

        # Derive shared key
        dh_shared_key = X448.shared_key(tfc_private_key_user,
                                        tfc_public_key_contact)

        # Domain separate unidirectional keys from shared key by using public
        # keys as message and the context variable as personalization string.
        tx_mk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_mk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        tx_hk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_hk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)

        # Domain separate fingerprints of public keys by using the
        # shared secret as key and the context variable as
        # personalization string. This way entities who might monitor
        # fingerprint verification channel are unable to correlate
        # spoken values with public keys that they might see on RAM or
        # screen of Networked Computer: Public keys can not be derived
        # from the fingerprints due to preimage resistance of BLAKE2b,
        # and fingerprints can not be derived from public key without
        # the X448 shared secret. Using the context variable ensures
        # fingerprints are distinct from derived message and header keys.
        tx_fp = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)
        rx_fp = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)

        # Verify fingerprints
        try:
            if not verify_fingerprints(tx_fp, rx_fp):
                m_print([
                    "Warning!", "Possible man-in-the-middle attack detected.",
                    "Aborting key exchange for your safety."
                ],
                        bold=True,
                        tail=1)
                raise FunctionReturn("Error: Fingerprint mismatch",
                                     delay=2.5,
                                     output=False)
            kex_status = KEX_STATUS_VERIFIED

        except (EOFError, KeyboardInterrupt):
            m_print([
                "Skipping fingerprint verification.", '', "Warning!",
                "Man-in-the-middle attacks can not be detected",
                "unless fingerprints are verified! To re-verify",
                "the contact, use the command '/verify'.", '',
                "Press <enter> to continue."
            ],
                    manual_proceed=True,
                    box=True,
                    head=2)
            kex_status = KEX_STATUS_UNVERIFIED

        # Send keys to the Receiver Program
        c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
        command = (KEY_EX_ECDHE + onion_pub_key + tx_mk + rx_mk + tx_hk +
                   rx_hk + str_to_bytes(nick))

        queue_command(command, settings, queues)

        while True:
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                break

            elif purp_code == '':
                phase("Resending contact data", head=2)
                queue_command(command, settings, queues)
                phase(DONE)
                print_on_previous_line(reps=5)

            else:
                m_print("Incorrect confirmation code.", head=1)
                print_on_previous_line(reps=4, delay=2)

        # Store contact data into databases
        contact.tfc_private_key = None
        contact.tx_fingerprint = tx_fp
        contact.rx_fingerprint = rx_fp
        contact.kex_status = kex_status
        contact_list.store_contacts()

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        contact.tfc_private_key = tfc_private_key_user
        raise FunctionReturn("Key exchange interrupted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
Пример #18
0
def new_local_key(contact_list: 'ContactList', settings: 'Settings',
                  queues: 'QueueDict') -> None:
    """Run local key exchange protocol.

    Local key encrypts commands and data sent from Source Computer to
    user's Destination Computer. The key is delivered to Destination
    Computer in packet encrypted with an ephemeral, symmetric, key
    encryption key.

    The check-summed Base58 format key decryption key is typed to
    Receiver Program manually. This prevents local key leak in following
    scenarios:

        1. CT is intercepted by an adversary on compromised Networked
           Computer, but no visual eavesdropping takes place.

        2. CT is not intercepted by an adversary on Networked Computer,
           but visual eavesdropping records key decryption key.

        3. CT is delivered from Source Computer to Destination Computer
           directly (bypassing compromised Networked Computer), and
           visual eavesdropping records key decryption key.

    Once the correct key decryption key is entered to Receiver Program,
    it will display the 2-hexadecimal confirmation code generated by
    the Transmitter Program. The code will be entered back to
    Transmitter Program to confirm the user has successfully delivered
    the key decryption key.

    The protocol is completed with Transmitter Program sending
    LOCAL_KEY_RDY signal to the Receiver Program, that then moves to
    wait for public keys from contact.
    """
    try:
        if settings.traffic_masking and contact_list.has_local_contact():
            raise FunctionReturn(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

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

        if not contact_list.has_local_contact():
            time.sleep(0.5)

        key = csprng()
        hek = csprng()
        kek = csprng()
        c_code = os.urandom(CONFIRM_CODE_LENGTH)

        local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign(
            plaintext=key + hek + c_code, key=kek)

        # Deliver local key to Destination computer
        nc_bypass_msg(NC_BYPASS_START, settings)
        queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
        while True:
            print_key("Local key decryption key (to Receiver)", kek, settings)
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                nc_bypass_msg(NC_BYPASS_STOP, settings)
                break
            elif purp_code == '':
                phase("Resending local key", head=2)
                queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
                phase(DONE)
                print_on_previous_line(
                    reps=(9 if settings.local_testing_mode else 10))
            else:
                m_print([
                    "Incorrect confirmation code. If Receiver did not receive",
                    "the encrypted local key, resend it by pressing <Enter>."
                ],
                        head=1)
                print_on_previous_line(
                    reps=(9 if settings.local_testing_mode else 10), delay=2)

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

        # Add local contact to keyset database
        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng()))

        # Notify Receiver that confirmation code was successfully entered
        queue_command(LOCAL_KEY_RDY, settings, queues)

        m_print("Successfully completed the local key exchange.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)
        os.system(RESET)

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Local key setup aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
Пример #19
0
def change_setting(user_input: 'UserInput', window: 'TxWindow',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   settings: 'Settings', queues: 'QueueDict',
                   master_key: 'MasterKey', gateway: 'Gateway') -> None:
    """Change setting on Transmitter and Receiver Program."""
    # Validate the KV-pair
    try:
        setting = user_input.plaintext.split()[1]
    except IndexError:
        raise FunctionReturn("Error: No setting specified.", head_clear=True)

    if setting not in (settings.key_list + gateway.settings.key_list):
        raise FunctionReturn(f"Error: Invalid setting '{setting}'.",
                             head_clear=True)

    try:
        value = user_input.plaintext.split()[2]
    except IndexError:
        raise FunctionReturn("Error: No value for setting specified.",
                             head_clear=True)

    # Check if the setting can be changed
    relay_settings = dict(
        serial_error_correction=UNENCRYPTED_EC_RATIO,
        serial_baudrate=UNENCRYPTED_BAUDRATE,
        allow_contact_requests=UNENCRYPTED_MANAGE_CONTACT_REQ)
    if settings.traffic_masking and (setting in relay_settings
                                     or setting == 'max_number_of_contacts'):
        raise FunctionReturn(
            "Error: Can't change this setting during traffic masking.",
            head_clear=True)

    if setting in ['use_serial_usb_adapter', 'built_in_serial_interface']:
        raise FunctionReturn(
            "Error: Serial interface setting can only be changed manually.",
            head_clear=True)

    if setting == 'ask_password_for_log_access':
        try:
            authenticated = master_key.load_master_key(
            ) == master_key.master_key
        except (EOFError, KeyboardInterrupt):
            raise FunctionReturn(f"Setting change aborted.",
                                 tail_clear=True,
                                 head=2,
                                 delay=1)

        if not authenticated:
            raise FunctionReturn("Error: No permission to change setting.",
                                 head_clear=True)

    # Change the setting
    if setting in gateway.settings.key_list:
        gateway.settings.change_setting(setting, value)
    else:
        settings.change_setting(setting, value, contact_list, group_list)

    receiver_command = CH_SETTING + setting.encode() + US_BYTE + value.encode()
    queue_command(receiver_command, settings, queues)

    if setting in relay_settings:
        if setting == 'allow_contact_requests':
            value = bool_to_bytes(settings.allow_contact_requests).decode()
        relay_command = UNENCRYPTED_DATAGRAM_HEADER + relay_settings[
            setting] + value.encode()
        queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])

    # Propagate the effects of the setting
    if setting == 'max_number_of_contacts':
        contact_list.store_contacts()
        queues[KEY_MANAGEMENT_QUEUE].put((KDB_UPDATE_SIZE_HEADER, settings))

    if setting in ['max_number_of_group_members', 'max_number_of_groups']:
        group_list.store_groups()

    if setting == 'traffic_masking':
        queues[SENDER_MODE_QUEUE].put(settings)
        queues[TRAFFIC_MASKING_QUEUE].put(settings.traffic_masking)
        window.deselect()

    if setting == 'log_file_masking':
        queues[LOGFILE_MASKING_QUEUE].put(settings.log_file_masking)