예제 #1
0
def rxp_show_sys_win(
    user_input: 'UserInput',
    window: 'TxWindow',
    settings: 'Settings',
    queues: 'QueueDict',
) -> None:
    """\
    Display a system window on Receiver Program until the user presses
    Enter.

    Receiver Program has a dedicated window, WIN_UID_LOCAL, for system
    messages that shows information about received commands, status
    messages etc.

    Receiver Program also has another window, WIN_UID_FILE, that shows
    progress of file transmission from contacts that have traffic
    masking enabled.
    """
    cmd = user_input.plaintext.split()[0]
    win_uid = dict(cmd=WIN_UID_COMMAND, fw=WIN_UID_FILE)[cmd]

    command = WIN_SELECT + win_uid
    queue_command(command, settings, queues)

    try:
        m_print(f"<Enter> returns Receiver to {window.name}'s window",
                manual_proceed=True,
                box=True)
    except (EOFError, KeyboardInterrupt):
        pass

    print_on_previous_line(reps=4, flush=True)

    command = WIN_SELECT + window.uid
    queue_command(command, settings, queues)
예제 #2
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()
예제 #3
0
def rxp_load_psk(window:       'TxWindow',
                 contact_list: 'ContactList',
                 settings:     'Settings',
                 queues:       'QueueDict',
                 ) -> None:
    """Send command to Receiver Program to load PSK for active contact."""
    if settings.traffic_masking:
        raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)

    if window.type == WIN_TYPE_GROUP or window.contact is None:
        raise SoftError("Error: Group is selected.", head_clear=True)

    if not contact_list.get_contact_by_pub_key(window.uid).uses_psk():
        raise SoftError(f"Error: The current key was exchanged with {ECDHE}.", head_clear=True)

    c_code  = blake2b(window.uid, digest_size=CONFIRM_CODE_LENGTH)
    command = KEY_EX_PSK_RX + c_code + window.uid
    queue_command(command, settings, queues)

    while True:
        try:
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                window.contact.kex_status = KEX_STATUS_HAS_RX_PSK
                contact_list.store_contacts()
                raise SoftError(f"Removed PSK reminder for {window.name}.", tail_clear=True, delay=1)

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

        except (EOFError, KeyboardInterrupt):
            raise SoftError("PSK install verification aborted.", tail_clear=True, delay=1, head=2)
예제 #4
0
def change_nick(user_input: 'UserInput', window: 'TxWindow',
                contact_list: 'ContactList', group_list: 'GroupList',
                settings: 'Settings', queues: 'QueueDict') -> None:
    """Change nick of contact."""
    try:
        nick = user_input.plaintext.split()[1]
    except IndexError:
        raise SoftError("Error: No nick specified.", head_clear=True)

    if window.type == WIN_TYPE_GROUP:
        group_rename(nick, window, contact_list, group_list, settings, queues)

    if window.contact is None:
        raise SoftError("Error: Window does not have contact.")

    onion_pub_key = window.contact.onion_pub_key
    error_msg = validate_nick(nick, (contact_list, group_list, onion_pub_key))
    if error_msg:
        raise SoftError(error_msg, head_clear=True)

    window.contact.nick = nick
    window.name = nick
    contact_list.store_contacts()

    command = CH_NICKNAME + onion_pub_key + nick.encode()
    queue_command(command, settings, queues)
예제 #5
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])
예제 #6
0
def rename_group(
    new_name: str,
    window: 'TxWindow',
    contact_list: 'ContactList',
    group_list: 'GroupList',
    settings: 'Settings',
    queues: 'QueueDict',
) -> None:
    """Rename the active group."""
    if window.type == WIN_TYPE_CONTACT or window.group is None:
        raise FunctionReturn("Error: Selected window is not a group window.",
                             head_clear=True)

    error_msg = validate_group_name(new_name, contact_list, group_list)
    if error_msg:
        raise FunctionReturn(error_msg, head_clear=True)

    command = GROUP_RENAME + window.uid + new_name.encode()
    queue_command(command, settings, queues)

    old_name = window.group.name
    window.group.name = new_name
    group_list.store_groups()

    raise FunctionReturn(f"Renamed group '{old_name}' to '{new_name}'.",
                         delay=1,
                         tail_clear=True)
예제 #7
0
    def select_tx_window(
        self,
        settings: 'Settings',  # Settings object
        queues: 'QueueDict',  # Dictionary of Queues
        onion_service: 'OnionService',  # OnionService object
        gateway: 'Gateway',  # Gateway object
        selection: Optional[str] = None,  # Selector for window
        cmd: bool = False  # True when `/msg` command is used to switch window
    ) -> None:
        """Select specified window or ask the user to specify one."""
        if selection is None:
            selection = self.select_recipient()

        if selection in self.group_list.get_list_of_group_names():
            self.select_group(selection, cmd, settings)

        elif selection in self.contact_list.contact_selectors():
            self.select_contact(selection, cmd, queues, settings)

        elif selection.startswith("/"):
            self.window_selection_command(selection, settings, queues,
                                          onion_service, gateway)

        else:
            raise SoftError("Error: No contact/group was found.")

        if settings.traffic_masking:
            queues[WINDOW_SELECT_QUEUE].put(self.window_contacts)

        packet = WIN_SELECT + self.uid
        queue_command(packet, settings, queues)

        clear_screen()
예제 #8
0
def deliver_contact_data(
        header: bytes,  # Key type (x448, PSK)
        nick: str,  # Contact's nickname
        onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
        tx_mk: bytes,  # Message key for outgoing messages
        rx_mk: bytes,  # Message key for incoming messages
        tx_hk: bytes,  # Header key for outgoing messages
        rx_hk: bytes,  # Header key for incoming messages
        queues: 'QueueDict',  # Dictionary of multiprocessing queues
        settings: 'Settings',  # Settings object
) -> None:
    """Deliver contact data to Destination Computer."""
    c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
    command = (header + 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)
예제 #9
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])
예제 #10
0
def log_command(user_input: 'UserInput', window: 'TxWindow',
                contact_list: 'ContactList', group_list: 'GroupList',
                settings: 'Settings', queues: 'QueueDict',
                master_key: 'MasterKey') -> None:
    """Display message logs or export them to plaintext file on TCBs.

    Transmitter Program processes sent, Receiver Program sent and
    received, messages of all participants in the active window.

    Having the capability to export the log file from the encrypted
    database is a bad idea, but as it's required by the GDPR
    (https://gdpr-info.eu/art-20-gdpr/), it should be done as securely
    as possible.

    Therefore, before allowing export, TFC will ask for the master
    password to ensure no unauthorized user who gains momentary
    access to the system can the export logs from the database.
    """
    cmd = user_input.plaintext.split()[0]
    export, header = dict(export=(True, LOG_EXPORT),
                          history=(False, LOG_DISPLAY))[cmd]

    try:
        msg_to_load = int(user_input.plaintext.split()[1])
    except ValueError:
        raise SoftError("Error: Invalid number of messages.", head_clear=True)
    except IndexError:
        msg_to_load = 0

    try:
        command = header + int_to_bytes(msg_to_load) + window.uid
    except struct.error:
        raise SoftError("Error: Invalid number of messages.", head_clear=True)

    if export and not yes(f"Export logs for '{window.name}' in plaintext?",
                          abort=False):
        raise SoftError("Log file export aborted.",
                        tail_clear=True,
                        head=0,
                        delay=1)

    authenticated = master_key.authenticate_action(
    ) if settings.ask_password_for_log_access else True

    if authenticated:
        queue_command(command, settings, queues)
        access_logs(window,
                    contact_list,
                    group_list,
                    settings,
                    master_key,
                    msg_to_load,
                    export=export)

        if export:
            raise SoftError(
                f"Exported log file of {window.type} '{window.name}'.",
                head_clear=True)
예제 #11
0
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList',
                      group_list: 'GroupList', settings: 'Settings',
                      queues: 'QueueDict', master_key: 'MasterKey',
                      onion_service: 'OnionService') -> None:
    """Change the master key on Transmitter/Receiver Program."""
    try:
        if settings.traffic_masking:
            raise FunctionReturn(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

        try:
            device = user_input.plaintext.split()[1].lower()
        except IndexError:
            raise FunctionReturn(
                f"Error: No target-system ('{TX}' or '{RX}') specified.",
                head_clear=True)

        if device not in [TX, RX]:
            raise FunctionReturn(f"Error: Invalid target system '{device}'.",
                                 head_clear=True)

        if device == RX:
            queue_command(CH_MASTER_KEY, settings, queues)
            return None

        old_master_key = master_key.master_key[:]
        new_master_key = master_key.master_key = master_key.new_master_key()

        phase("Re-encrypting databases")

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_CHANGE_MASTER_KEY_HEADER, master_key))

        ensure_dir(DIR_USER_DATA)
        if os.path.isfile(
                f'{DIR_USER_DATA}{settings.software_operation}_logs'):
            change_log_db_key(old_master_key, new_master_key, settings)

        contact_list.store_contacts()
        group_list.store_groups()
        settings.store_settings()
        onion_service.store_onion_service_private_key()

        phase(DONE)
        m_print("Master key successfully changed.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Password change aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #12
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)
예제 #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 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)
예제 #14
0
파일: commands_g.py 프로젝트: todun/tfc
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)
예제 #15
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])
예제 #16
0
def contact_setting(user_input: 'UserInput', window: 'TxWindow',
                    contact_list: 'ContactList', group_list: 'GroupList',
                    settings: 'Settings', queues: 'QueueDict') -> None:
    """\
    Change logging, file reception, or notification setting of a group
    or (all) contact(s).
    """
    try:
        parameters = user_input.plaintext.split()
        cmd_key = parameters[0]
        cmd_header = {
            LOGGING: CH_LOGGING,
            STORE: CH_FILE_RECV,
            NOTIFY: CH_NOTIFY
        }[cmd_key]

        setting, b_value = dict(on=(ENABLE, True),
                                off=(DISABLE, False))[parameters[1]]

    except (IndexError, KeyError):
        raise SoftError("Error: Invalid command.", head_clear=True)

    # If second parameter 'all' is included, apply setting for all contacts and groups
    try:
        win_uid = b''
        if parameters[2] == ALL:
            cmd_value = setting.upper()
        else:
            raise SoftError("Error: Invalid command.", head_clear=True)
    except IndexError:
        win_uid = window.uid
        cmd_value = setting + win_uid

    if win_uid:
        change_setting_for_selected_contact(cmd_key, b_value, window,
                                            contact_list, group_list)
    else:
        change_setting_for_all_contacts(cmd_key, b_value, contact_list,
                                        group_list)

    command = cmd_header + cmd_value

    if settings.traffic_masking and cmd_key == LOGGING:
        # Send `log_writer_loop` the new logging setting that is loaded
        # when the next noise packet is loaded from `noise_packet_loop`.
        queues[LOG_SETTING_QUEUE].put(b_value)

    window.update_log_messages()

    queue_command(command, settings, queues)
예제 #17
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)
예제 #18
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])
예제 #19
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)
예제 #20
0
def log_command(user_input:   'UserInput',
                window:       'TxWindow',
                contact_list: 'ContactList',
                group_list:   'GroupList',
                settings:     'Settings',
                queues:       'QueueDict',
                master_key:   'MasterKey'
                ) -> None:
    """Display message logs or export them to plaintext file on TCBs.

    Transmitter Program processes sent, Receiver Program sent and
    received, messages of all participants in the active window.
    """
    cmd            = user_input.plaintext.split()[0]
    export, header = dict(export =(True,  LOG_EXPORT),
                          history=(False, LOG_DISPLAY))[cmd]

    try:
        msg_to_load = int(user_input.plaintext.split()[1])
    except ValueError:
        raise FunctionReturn("Error: Invalid number of messages.", head_clear=True)
    except IndexError:
        msg_to_load = 0

    try:
        command = header + int_to_bytes(msg_to_load) + window.uid
    except struct.error:
        raise FunctionReturn("Error: Invalid number of messages.", head_clear=True)

    if export:
        if not yes(f"Export logs for '{window.name}' in plaintext?", abort=False):
            raise FunctionReturn("Log file export aborted.", tail_clear=True, head=0, delay=1)

    queue_command(command, settings, queues)

    access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export)

    if export:
        raise FunctionReturn(f"Exported log file of {window.type} '{window.name}'.", head_clear=True)
예제 #21
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 SoftError("Error: No contact/group specified.", head_clear=True)

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

    selector = determine_selector(selection, contact_list, group_list)

    # 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)
예제 #22
0
 def test_queue_command(self):
     self.assertIsNone(queue_command(os.urandom(200), self.settings, self.queues))
     c_pt = self.queues[COMMAND_PACKET_QUEUE].get()
     self.assertEqual(len(c_pt), ASSEMBLY_PACKET_LENGTH)
예제 #23
0
def rxp_display_unread(settings: 'Settings', queues: 'QueueDict') -> None:
    """\
    Display the list of windows that contain unread messages on Receiver
    Program.
    """
    queue_command(WIN_ACTIVITY, settings, queues)
예제 #24
0
파일: key_exchanges.py 프로젝트: gtog/tfc
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)
예제 #25
0
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList',
                      group_list: 'GroupList', settings: 'Settings',
                      queues: 'QueueDict', master_key: 'MasterKey',
                      onion_service: 'OnionService') -> None:
    """Change the master key on Transmitter/Receiver Program."""
    if settings.traffic_masking:
        raise SoftError("Error: Command is disabled during traffic masking.",
                        head_clear=True)

    try:
        device = user_input.plaintext.split()[1].lower()
    except IndexError:
        raise SoftError(
            f"Error: No target-system ('{TX}' or '{RX}') specified.",
            head_clear=True)

    if device not in [TX, RX]:
        raise SoftError(f"Error: Invalid target system '{device}'.",
                        head_clear=True)

    if device == RX:
        queue_command(CH_MASTER_KEY, settings, queues)
        return None

    authenticated = master_key.authenticate_action()

    if authenticated:
        # Cache old master key to allow log file re-encryption.
        old_master_key = master_key.master_key[:]

        # Create new master key but do not store new master key data into any database.
        new_master_key = master_key.master_key = master_key.new_master_key(
            replace=False)
        phase("Re-encrypting databases")

        # Halt `sender_loop` for the duration of database re-encryption.
        queues[KEY_MANAGEMENT_QUEUE].put((KDB_M_KEY_CHANGE_HALT_HEADER, ))
        wait_for_key_db_halt(queues)

        # Load old key_list from database file as it's not used on input_loop side.
        key_list = KeyList(master_key, settings)

        # Update encryption keys for databases
        contact_list.database.database_key = new_master_key
        key_list.database.database_key = new_master_key
        group_list.database.database_key = new_master_key
        settings.database.database_key = new_master_key
        onion_service.database.database_key = new_master_key

        # Create temp databases for each database, do not replace original.
        with ignored(SoftError):
            change_log_db_key(old_master_key, new_master_key, settings)
        contact_list.store_contacts(replace=False)
        key_list.store_keys(replace=False)
        group_list.store_groups(replace=False)
        settings.store_settings(replace=False)
        onion_service.store_onion_service_private_key(replace=False)

        # At this point all temp files exist and they have been checked to be valid by the respective
        # temp file writing function. It's now time to create a temp file for the new master key
        # database. Once the temp master key database is created, the `replace_database_data()` method
        # will also run the atomic `os.replace()` command for the master key database.
        master_key.replace_database_data()

        # Next we do the atomic `os.replace()` for all other files too.
        replace_log_db(settings)
        contact_list.database.replace_database()
        key_list.database.replace_database()
        group_list.database.replace_database()
        settings.database.replace_database()
        onion_service.database.replace_database()

        # Now all databases have been updated. It's time to let
        # the key database know what the new master key is.
        queues[KEY_MANAGEMENT_QUEUE].put(new_master_key)

        wait_for_key_db_ack(new_master_key, queues)

        phase(DONE)
        m_print("Master key successfully changed.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)
예제 #26
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)
예제 #27
0
파일: key_exchanges.py 프로젝트: gtog/tfc
def create_pre_shared_key(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Nick of contact
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    onion_service: 'OnionService',  # OnionService object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Generate a new pre-shared key for manual key delivery.

    Pre-shared keys offer a low-tech solution against the slowly
    emerging threat of quantum computers. PSKs are less convenient and
    not usable in every scenario, but until a quantum-safe key exchange
    algorithm with reasonably short keys is standardized, TFC can't
    provide a better alternative against quantum computers.

    The generated keys are protected by a key encryption key, derived
    from a 256-bit salt and a password (that is to be shared with the
    recipient) using Argon2d key derivation function.

    The encrypted message and header keys are stored together with salt
    on a removable media. This media must be a never-before-used device
    from sealed packaging. Re-using an old device might infect Source
    Computer, and the malware could either copy sensitive data on that
    removable media, or Source Computer might start transmitting the
    sensitive data covertly over the serial interface to malware on
    Networked Computer.

    Once the key has been exported to the clean drive, contact data and
    keys are exported to the Receiver Program on Destination computer.
    The transmission is encrypted with the local key.
    """
    try:
        tx_mk = csprng()
        tx_hk = csprng()
        salt = csprng()

        password = MasterKey.new_password("password for PSK")

        phase("Deriving key encryption key", head=2)
        kek = argon2_kdf(password,
                         salt,
                         time_cost=ARGON2_PSK_TIME_COST,
                         memory_cost=ARGON2_PSK_MEMORY_COST)
        phase(DONE)

        ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek)

        while True:
            trunc_addr = pub_key_to_short_address(onion_pub_key)
            store_d = ask_path_gui(f"Select removable media for {nick}",
                                   settings)
            f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"
            try:
                with open(f_name, 'wb+') as f:
                    f.write(salt + ct_tag)
                break
            except PermissionError:
                m_print(
                    "Error: Did not have permission to write to the directory.",
                    delay=0.5)
                continue

        command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk +
                   csprng() + str_to_bytes(nick))

        queue_command(command, settings, queues)

        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)

        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):
        raise FunctionReturn("PSK generation aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #28
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 SoftError(
                "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(local_key_packet, kek, c_code, settings, queues)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK,
                                 blake2b(b58encode(kek).encode()),
                                 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 Program 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)
        reset_terminal()

    except (EOFError, KeyboardInterrupt):
        raise SoftError("Local key setup aborted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
예제 #29
0
파일: windows.py 프로젝트: gtog/tfc
    def select_tx_window(
        self,
        settings: 'Settings',  # Settings object
        queues: 'QueueDict',  # Dictionary of Queues
        onion_service: 'OnionService',  # OnionService object
        gateway: 'Gateway',  # Gateway object
        selection: Optional[str] = None,  # Selector for window
        cmd: bool = False  # True when `/msg` command is used to switch window
    ) -> None:
        """Select specified window or ask the user to specify one."""
        if selection is None:
            self.contact_list.print_contacts()
            self.group_list.print_groups()

            if self.contact_list.has_only_pending_contacts():
                print(
                    "\n'/connect'   sends Onion Service/contact data to Relay"
                    "\n'/add'       adds another contact."
                    "\n'/rm <Nick>' removes an existing contact.\n")

            selection = input("Select recipient: ").strip()

        if selection in self.group_list.get_list_of_group_names():
            if cmd and settings.traffic_masking and selection != self.name:
                raise FunctionReturn(
                    "Error: Can't change window during traffic masking.",
                    head_clear=True)

            self.contact = None
            self.group = self.group_list.get_group(selection)
            self.window_contacts = self.group.members
            self.name = self.group.name
            self.uid = self.group.group_id
            self.group_id = self.group.group_id
            self.log_messages = self.group.log_messages
            self.type = WIN_TYPE_GROUP
            self.type_print = 'group'

        elif selection in self.contact_list.contact_selectors():
            if cmd and settings.traffic_masking:
                contact = self.contact_list.get_contact_by_address_or_nick(
                    selection)
                if contact.onion_pub_key != self.uid:
                    raise FunctionReturn(
                        "Error: Can't change window during traffic masking.",
                        head_clear=True)

            self.contact = self.contact_list.get_contact_by_address_or_nick(
                selection)

            if self.contact.kex_status == KEX_STATUS_PENDING:
                start_key_exchange(self.contact.onion_pub_key,
                                   self.contact.nick, self.contact_list,
                                   settings, queues)

            self.group = None
            self.group_id = None
            self.window_contacts = [self.contact]
            self.name = self.contact.nick
            self.uid = self.contact.onion_pub_key
            self.log_messages = self.contact.log_messages
            self.type = WIN_TYPE_CONTACT
            self.type_print = 'contact'

        elif selection.startswith('/'):
            self.window_selection_command(selection, settings, queues,
                                          onion_service, gateway)

        else:
            raise FunctionReturn("Error: No contact/group was found.")

        if settings.traffic_masking:
            queues[WINDOW_SELECT_QUEUE].put(self.window_contacts)

        packet = WIN_SELECT + self.uid
        queue_command(packet, settings, queues)

        clear_screen()
예제 #30
0
파일: key_exchanges.py 프로젝트: gtog/tfc
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)