Exemple #1
0
def wipe(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
    """Reset terminals, wipe all user data from TxM/RxM/NH and power off systems.

    No effective RAM overwriting tool currently exists, so as long as TxM/RxM
    use FDE and DDR3 memory, recovery of user 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?"):
        raise FunctionReturn("Wipe command aborted.")

    clear_screen()

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

    queue_command(WIPE_USER_DATA_HEADER, settings,
                  queues[COMMAND_PACKET_QUEUE])

    if not settings.session_traffic_masking:
        if settings.local_testing_mode:
            time.sleep(0.8)
            if settings.data_diode_sockets:
                time.sleep(2.2)
        else:
            time.sleep(settings.race_condition_delay)

    queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND, settings,
                queues[NH_PACKET_QUEUE])

    os.system('reset')
Exemple #2
0
    def setup(self) -> None:
        """Prompt the user to enter initial serial interface setting.

        Ensure that the serial interface is available before proceeding.
        """
        if not self.local_testing_mode and not self.qubes:
            name = {
                TX: TRANSMITTER,
                NC: RELAY,
                RX: RECEIVER
            }[self.software_operation]

            self.use_serial_usb_adapter = yes(
                f"Use USB-to-serial/TTL adapter for {name} Computer?",
                head=1,
                tail=1)

            if self.use_serial_usb_adapter:
                for f in sorted(os.listdir('/dev/')):
                    if f.startswith('ttyUSB'):
                        return None
                m_print("Error: USB-to-serial/TTL adapter not found.")
                self.setup()
            else:
                if self.built_in_serial_interface not in sorted(
                        os.listdir('/dev/')):
                    m_print(
                        f"Error: Serial interface /dev/{self.built_in_serial_interface} not found."
                    )
                    self.setup()
Exemple #3
0
def log_command(user_input: 'UserInput', window: 'TxWindow',
                contact_list: 'ContactList', group_list: 'GroupList',
                settings: 'Settings', c_queue: 'Queue',
                master_key: 'MasterKey') -> None:
    """Display message logs or export them to plaintext file on TxM/RxM.

    TxM processes sent messages, RxM processes sent and
    received messages for all participants in active window.
    """
    cmd = user_input.plaintext.split()[0]

    export, header = dict(export=(True, LOG_EXPORT_HEADER),
                          history=(False, LOG_DISPLAY_HEADER))[cmd]

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

    if export and not yes(
            f"Export logs for '{window.name}' in plaintext?", head=1, tail=1):
        raise FunctionReturn("Logfile export aborted.")

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

    queue_command(command, settings, c_queue)

    access_logs(window, contact_list, group_list, settings, master_key,
                msg_to_load, export)
Exemple #4
0
    def validate_traffic_masking_delay(key: str, value: 'SettingType',
                                       contact_list: 'ContactList') -> None:
        """Validate setting value for traffic masking delays."""
        if key in ["tm_static_delay", "tm_random_delay"]:

            for key_, name, min_setting in [
                ("tm_static_delay", "static",
                 TRAFFIC_MASKING_MIN_STATIC_DELAY),
                ("tm_random_delay", "random", TRAFFIC_MASKING_MIN_RANDOM_DELAY)
            ]:
                if key == key_ and value < min_setting:
                    raise SoftError(
                        f"Error: Can't set {name} delay lower than {min_setting}.",
                        head_clear=True)

            if contact_list.settings.software_operation == TX:
                m_print([
                    "WARNING!",
                    "Changing traffic masking delay can make your endpoint and traffic look unique!"
                ],
                        bold=True,
                        head=1,
                        tail=1)

                if not yes("Proceed anyway?"):
                    raise SoftError("Aborted traffic masking setting change.",
                                    head_clear=True)

            m_print("Traffic masking setting will change on restart.",
                    head=1,
                    tail=1)
Exemple #5
0
def send_onion_service_key(contact_list: 'ContactList', settings: 'Settings',
                           onion_service: 'OnionService',
                           gateway: 'Gateway') -> None:
    """Resend Onion Service key to Relay Program on Networked Computer.

    This command is used in cases where Relay Program had to be
    restarted for some reason (e.g. due to system updates).
    """
    try:
        if settings.traffic_masking:
            m_print([
                "Warning!",
                "Exporting Onion Service data to Networked Computer ",
                "during traffic masking can reveal to an adversary ",
                "TFC is being used at the moment. You should only do ",
                "this if you've had to restart the Relay Program."
            ],
                    bold=True,
                    head=1,
                    tail=1)
            if not yes("Proceed with the Onion Service data export?",
                       abort=False):
                raise SoftError("Onion Service data export canceled.",
                                tail_clear=True,
                                delay=1,
                                head=0)

        export_onion_service_data(contact_list, settings, onion_service,
                                  gateway)
    except (EOFError, KeyboardInterrupt):
        raise SoftError("Onion Service data export canceled.",
                        tail_clear=True,
                        delay=1,
                        head=2)
Exemple #6
0
def process_offset(
    offset: int,  # Number of dropped packets
    origin: bytes,  # "to/from" preposition
    direction: str,  # Direction of packet
    nick: str,  # Nickname of associated contact
    window: 'RxWindow'  # RxWindow object
) -> None:
    """Display warnings about increased offsets.

    If the offset has increased over the threshold, ask the user to
    confirm hash ratchet catch up.
    """
    if offset > HARAC_WARN_THRESHOLD and origin == ORIGIN_CONTACT_HEADER:
        m_print([
            f"Warning! {offset} packets from {nick} were not received.",
            f"This might indicate that {offset} most recent packets were ",
            f"lost during transmission, or that the contact is attempting ",
            f"a DoS attack. You can wait for TFC to attempt to decrypt the ",
            "packet, but it might take a very long time or even forever."
        ])

        if not yes("Proceed with the decryption?", abort=False, tail=1):
            raise FunctionReturn(f"Dropped packet from {nick}.", window=window)

    elif offset:
        m_print(
            f"Warning! {offset} packet{'s' if offset > 1 else ''} {direction} {nick} were not received."
        )
Exemple #7
0
def group_rm_group(group_name: str, contact_list: 'ContactList',
                   group_list: 'GroupList', settings: 'Settings',
                   queues: 'QueueDict', master_key: 'MasterKey') -> 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)
Exemple #8
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)
Exemple #9
0
    def test_yes(self, _: Any) -> None:
        self.assertTrue(yes('test prompt', head=1, tail=1))
        self.assertTrue(yes('test prompt'))

        self.assertFalse(yes('test prompt', head=1, tail=1))
        self.assertFalse(yes('test prompt'))

        self.assertTrue(yes('test prompt', head=1, tail=1, abort=True))
        self.assertFalse(yes('test prompt', abort=False))

        self.assertTrue(yes('test prompt', head=1, tail=1, abort=True))
        self.assertFalse(yes('test prompt', abort=False))

        with self.assertRaises(EOFError):
            self.assertFalse(yes('test prompt'))

        with self.assertRaises(KeyboardInterrupt):
            self.assertFalse(yes('test prompt'))
Exemple #10
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)
Exemple #11
0
    def test_yes(self, _):
        self.assertTrue(yes('test prompt', head=1, tail=1))
        self.assertTrue(yes('test prompt'))

        self.assertFalse(yes('test prompt', head=1, tail=1))
        self.assertFalse(yes('test prompt'))

        self.assertTrue(yes('test prompt', head=1, tail=1, abort=True))
        self.assertFalse(yes('test prompt', abort=False))

        self.assertTrue(yes('test prompt', head=1, tail=1, abort=True))
        self.assertFalse(yes('test prompt', abort=False))
Exemple #12
0
def queue_file(window: 'TxWindow', settings: 'Settings',
               queues: 'QueueDict') -> None:
    """Ask file path and load file data.

    In TFC there are two ways to send a file.

    For traffic masking, the file is loaded and sent inside normal
    messages using assembly packet headers dedicated for file
    transmission. This transmission is much slower, so the File object
    will determine metadata about the transmission's estimated transfer
    time, number of packets and the name and size of file. This
    information is inserted to the first assembly packet so that the
    recipient can observe the transmission progress from file transfer
    window.

    When traffic masking is disabled, file transmission is much faster
    as the file is only encrypted and transferred over serial once
    before the Relay Program multi-casts the ciphertext to each
    specified recipient. See the send_file docstring (below) for more
    details.
    """
    path = ask_path_gui("Select file to send...", settings, get_file=True)

    if path.endswith(
        ('tx_contacts', 'tx_groups', 'tx_keys', 'tx_login_data', 'tx_settings',
         'rx_contacts', 'rx_groups', 'rx_keys', 'rx_login_data', 'rx_settings',
         'tx_serial_settings.json', 'nc_serial_settings.json',
         'rx_serial_settings.json', 'tx_onion_db')):
        raise FunctionReturn("Error: Can't send TFC database.",
                             head_clear=True)

    if not settings.traffic_masking:
        send_file(path, settings, queues, window)
        return

    file = File(path, window, settings)
    assembly_packets = split_to_assembly_packets(file.plaintext, FILE)

    if settings.confirm_sent_files:
        try:
            if not yes(
                    f"Send {file.name.decode()} ({file.size_hr}) to {window.type_print} {window.name} "
                    f"({len(assembly_packets)} packets, time: {file.time_hr})?"
            ):
                raise FunctionReturn("File selection aborted.",
                                     head_clear=True)
        except (EOFError, KeyboardInterrupt):
            raise FunctionReturn("File selection aborted.", head_clear=True)

    queue_assembly_packets(assembly_packets,
                           FILE,
                           settings,
                           queues,
                           window,
                           log_as_ph=True)
Exemple #13
0
    def setup(self) -> None:
        """Prompt the user to enter initial serial interface setting.

        Ensure that the serial interface is available before proceeding.
        """
        if not self.local_testing_mode and not self.qubes:
            name = {
                TX: TRANSMITTER,
                NC: RELAY,
                RX: RECEIVER
            }[self.software_operation]

            self.use_serial_usb_adapter = yes(
                f"Use USB-to-serial/TTL adapter for {name} Computer?",
                head=1,
                tail=1)

            if self.use_serial_usb_adapter:
                for f in sorted(os.listdir('/dev/')):
                    if f.startswith('ttyUSB'):
                        return None
                m_print("Error: USB-to-serial/TTL adapter not found.")
                self.setup()
            else:
                if self.built_in_serial_interface not in sorted(
                        os.listdir('/dev/')):
                    m_print(
                        f"Error: Serial interface /dev/{self.built_in_serial_interface} not found."
                    )
                    self.setup()

        if self.qubes and self.software_operation != RX:

            # Check if IP address was stored by the installer.
            if os.path.isfile(QUBES_RX_IP_ADDR_FILE):
                cached_ip = open(QUBES_RX_IP_ADDR_FILE).read().strip()
                os.remove(QUBES_RX_IP_ADDR_FILE)

                if validate_ip_address(cached_ip) == '':
                    self.rx_udp_ip = cached_ip
                    return

            # If we reach this point, no cached IP was found, prompt for IP address from the user.
            rx_device, short = ('Networked',
                                'NET') if self.software_operation == TX else (
                                    'Destination', 'DST')
            m_print(f"Enter the IP address of the {rx_device} Computer",
                    head=1,
                    tail=1)
            self.rx_udp_ip = box_input(f"{short} IP-address",
                                       expected_len=15,
                                       validator=validate_ip_address,
                                       tail=1)
Exemple #14
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])
Exemple #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 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)
Exemple #16
0
def group_create(group_name:   str,
                 purp_members: List[str],
                 group_list:   'GroupList',
                 contact_list: 'ContactList',
                 settings:     'Settings',
                 queues:       Dict[bytes, 'Queue'],
                 _:            'MasterKey') -> None:
    """Create a new group.
    
    Validate group name and determine what members that can be added.
    """
    validate_group_name(group_name, contact_list, group_list)

    accounts      = set(contact_list.get_list_of_accounts())
    purp_accounts = set(purp_members)
    accepted      = list(accounts      & purp_accounts)
    rejected      = list(purp_accounts - accounts)

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

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

    group_list.add_group(group_name,
                         settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         members=[contact_list.get_contact(c) for c in accepted])

    fields  = [f.encode() for f in ([group_name] + accepted)]
    command = GROUP_CREATE_HEADER + US_BYTE.join(fields)
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    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 list of group members to participants?"):
            for member in accepted:
                m_list = [m for m in accepted if m != member]
                queue_message(user_input=UserInput(US_STR.join([group_name] + m_list), MESSAGE),
                              window    =MockWindow(member, [contact_list.get_contact(member)]),
                              settings  =settings,
                              m_queue   =queues[MESSAGE_PACKET_QUEUE],
                              header    =GROUP_MSG_INVITEJOIN_HEADER,
                              log_as_ph =True)
    else:
        box_print(f"Created an empty group '{group_name}'", head=1)
Exemple #17
0
def remove_contact(user_input: 'UserInput', window: 'Window',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
    """Remove contact on TxM/RxM."""
    if settings.session_trickle:
        raise FunctionReturn("Command disabled during trickle connection.")

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

    if not yes(f"Remove {selection} completely?", head=1):
        raise FunctionReturn("Removal of contact aborted.")

    # Load account if user enters nick
    if selection in contact_list.get_list_of_nicks():
        selection = contact_list.get_contact(selection).rx_account

    packet = CONTACT_REMOVE_HEADER + selection.encode()
    queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])

    if selection in contact_list.get_list_of_accounts():
        queues[KEY_MANAGEMENT_QUEUE].put(('REM', selection))
        contact_list.remove_contact(selection)
        box_print(f"Removed {selection} from contacts.", head=1, tail=1)
    else:
        box_print(f"TxM has no {selection} to remove.", head=1, tail=1)

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

    for c in window:
        if selection == c.rx_account:
            if window.type == 'contact':
                window.deselect()
            elif window.type == 'group':
                window.update_group_win_members(group_list)

                # If last member from group is removed, deselect group.
                # This 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()
Exemple #18
0
def group_rm_member(group_name:   str,
                    purp_members: List[str],
                    group_list:   'GroupList',
                    contact_list: 'ContactList',
                    settings:     'Settings',
                    queues:       Dict[bytes, 'Queue'],
                    master_key:   'MasterKey') -> None:
    """Remove member(s) from group or group itself."""
    if not purp_members:
        group_rm_group(group_name, 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.")

    purp_accounts   = set(purp_members)
    accounts        = set(contact_list.get_list_of_accounts())
    before_removal  = set(group_list.get_group(group_name).get_list_of_member_accounts())
    ok_accounts_set = set(purp_accounts  & accounts)
    removable_set   = set(before_removal & ok_accounts_set)

    end_assembly = list(before_removal  - removable_set)
    not_in_group = list(ok_accounts_set - before_removal)
    rejected     = list(purp_accounts   - accounts)
    removable    = list(removable_set)
    ok_accounts  = list(ok_accounts_set)

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

    fields  = [f.encode() for f in ([group_name] + ok_accounts)]
    command = GROUP_REMOVE_M_HEADER + US_BYTE.join(fields)
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    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 end_assembly and yes("Publish list of removed members to remaining members?"):
        for member in end_assembly:
            queue_message(user_input=UserInput(US_STR.join([group_name] + removable), MESSAGE),
                          window    =MockWindow(member, [contact_list.get_contact(member)]),
                          settings  =settings,
                          m_queue   =queues[MESSAGE_PACKET_QUEUE],
                          header    =GROUP_MSG_MEMBER_REM_HEADER,
                          log_as_ph =True)
Exemple #19
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)
Exemple #20
0
    def window_selection_command(self, selection: str, settings: 'Settings',
                                 queues: 'QueueDict',
                                 onion_service: 'OnionService',
                                 gateway: 'Gateway') -> None:
        """Commands for adding and removing contacts from contact selection menu.

        In situations where only pending contacts are available and
        those contacts are not online, these commands prevent the user
        from not being able to add new contacts.
        """
        if selection == '/add':
            add_new_contact(self.contact_list, self.group_list, settings,
                            queues, onion_service)
            raise FunctionReturn("New contact added.", output=False)

        elif selection == '/connect':
            export_onion_service_data(self.contact_list, settings,
                                      onion_service, gateway)

        elif selection.startswith('/rm'):
            try:
                selection = selection.split()[1]
            except IndexError:
                raise FunctionReturn("Error: No account specified.", delay=1)

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

            if selection in self.contact_list.contact_selectors():
                onion_pub_key = self.contact_list.get_contact_by_address_or_nick(
                    selection).onion_pub_key
                self.contact_list.remove_contact_by_pub_key(onion_pub_key)
                self.contact_list.store_contacts()
                raise FunctionReturn(f"Removed contact '{selection}'.",
                                     delay=1)
            else:
                raise FunctionReturn(f"Error: Unknown contact '{selection}'.",
                                     delay=1)

        else:
            raise FunctionReturn("Error: Invalid command.", delay=1)
Exemple #21
0
def queue_file(window:   'TxWindow',
               settings: 'Settings',
               f_queue:  'Queue',
               gateway:  'Gateway') -> None:
    """Ask file path and load file data."""
    path = ask_path_gui("Select file to send...", settings, get_file=True)
    file = File(path, window, settings, gateway)

    packet_list = split_to_assembly_packets(file.plaintext, FILE)

    if settings.confirm_sent_files:
        try:
            if not yes(f"Send {file.name.decode()} ({file.size_print}) to {window.type_print} {window.name} "
                       f"({len(packet_list)} packets, time: {file.time_print})?"):
                raise FunctionReturn("File selection aborted.")
        except KeyboardInterrupt:
            raise FunctionReturn("File selection aborted.", head=3)

    queue_packets(packet_list, FILE, settings, f_queue, window, log_as_ph=True)
Exemple #22
0
def queue_file(window: 'Window', settings: 'Settings', f_queue: 'Queue',
               gateway: 'Gateway') -> None:
    """Ask file path and load file data."""
    path = ask_path_gui("Select file to send...", settings, get_file=True)
    file = File(path, window, settings, gateway)
    name = file.name.decode()
    size = file.size.decode()
    payload = file.plaintext

    if len(payload) < 255:
        padded = byte_padding(payload)
        packet_list = [F_S_HEADER + padded]
    else:
        payload = bytes(8) + payload
        padded = byte_padding(payload)
        p_list = split_byte_string(padded, item_len=255)

        #                            <   number of packets   >
        packet_list = (
            [F_L_HEADER + int_to_bytes(len(p_list)) + p_list[0][8:]] +
            [F_A_HEADER + p for p in p_list[1:-1]] + [F_E_HEADER + p_list[-1]])

    for p in packet_list:
        assert len(p) == 256

    if settings.confirm_sent_files:
        if not yes(
                f"Send {name} ({size}) to {window.type} {window.name} "
                f"({len(packet_list)} packets, time: {file.time_s})?",
                tail=1):
            raise FunctionReturn("File selection aborted.")

    if settings.session_trickle:
        log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window)
        for p in packet_list:
            f_queue.put((p, log_m_dictionary))

    else:
        for c in window:
            for p in packet_list:
                f_queue.put((p, settings, c.rx_account, c.tx_account,
                             c.log_messages, window.uid))
Exemple #23
0
def remove_log(user_input: 'UserInput', contact_list: 'ContactList',
               settings: 'Settings', c_queue: 'Queue',
               master_key: 'MasterKey') -> None:
    """Remove log entries for contact."""
    try:
        selection = user_input.plaintext.split()[1]
    except IndexError:
        raise FunctionReturn("Error: No contact/group specified.")

    if not yes(f"Remove logs for {selection}?", head=1):
        raise FunctionReturn("Logfile removal aborted.")

    # Swap specified nick to rx_account
    if selection in contact_list.get_list_of_nicks():
        selection = contact_list.get_contact(selection).rx_account

    command = LOG_REMOVE_HEADER + selection.encode()
    queue_command(command, settings, c_queue)

    remove_logs(selection, settings, master_key)
Exemple #24
0
def verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool:
    """\
    Verify fingerprints over out-of-band channel to
    detect MITM attacks against TFC's key exchange.

    :param tx_fp: User's fingerprint
    :param rx_fp: Contact's fingerprint
    :return:      True if fingerprints match, else False
    """
    clear_screen()

    message_printer("To verify received public key was not replaced by attacker in network, "
                    "call the contact over end-to-end encrypted line, preferably Signal "
                    "(https://signal.org/). Make sure Signal's safety numbers have been "
                    "verified, and then verbally compare the key fingerprints below.", head=1, tail=1)

    print_fingerprint(tx_fp, "         Your fingerprint (you read)         ")
    print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)")

    return yes("Is the contact's fingerprint correct?")
Exemple #25
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)
Exemple #26
0
def verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool:
    """Verify fingerprints over off-band channel to detect MITM attacks between NHs.

    :param tx_fp: User's fingerprint
    :param rx_fp: Contact's fingerprint
    :return:      True if fingerprints match, else False
    """
    clear_screen()

    message_printer(
        "To verify the public key was not swapped during delivery, "
        "call your contact over end-to-end encrypted line, preferably "
        "Signal by Open Whisper Systems. Verify call's Short "
        "Authentication String and then compare fingerprints below.",
        head=1,
        tail=1)

    print_fingerprints(tx_fp, "         Your fingerprint (you read)         ")
    print_fingerprints(rx_fp, "Purported fingerprint for contact (they read)")

    return yes("Is the contact's fingerprint correct?")
Exemple #27
0
def validate_group_name(group_name: str, contact_list: 'ContactList', group_list: 'GroupList') -> None:
    """Check that group name is valid."""
    # Avoids collision with delimiters
    if not group_name.isprintable():
        raise FunctionReturn("Error: Group name must be printable.")

    # Length limited by database's unicode padding
    if len(group_name) >= PADDING_LEN:
        raise FunctionReturn("Error: Group name must be less than 255 chars long.")

    if group_name == DUMMY_GROUP:
        raise FunctionReturn("Error: Group name can't use name reserved for database padding.")

    if re.match(ACCOUNT_FORMAT, group_name):
        raise FunctionReturn("Error: Group name can't have format of an account.")

    if group_name in contact_list.get_list_of_nicks():
        raise FunctionReturn("Error: Group name can't be nick of contact.")

    if group_name in group_list.get_list_of_group_names():
        if not yes(f"Group with name '{group_name}' already exists. Overwrite?", head=1):
            raise FunctionReturn("Group creation aborted.")
Exemple #28
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)
Exemple #29
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)
Exemple #30
0
def verify_fingerprints(
    tx_fp: bytes,  # User's fingerprint
    rx_fp: bytes  # Contact's fingerprint
) -> bool:  # True if fingerprints match, else False
    """\
    Verify fingerprints over an authenticated out-of-band channel to
    detect MITM attacks against TFC's key exchange.

    MITM or man-in-the-middle attack is an attack against an inherent
    problem in cryptography:

    Cryptography is math, nothing more. During key exchange public keys
    are just very large numbers. There is no way to tell by looking if a
    number (received from an untrusted network / Networked Computer) is
    the same number the contact generated.

    Public key fingerprints are values designed to be compared by humans
    either visually or audibly (or sometimes by using semi-automatic
    means such as QR-codes). By comparing the fingerprint over an
    authenticated channel it's possible to verify that the correct key
    was received from the network.
    """
    m_print(
        "To verify received public key was not replaced by an attacker "
        "call the contact over an end-to-end encrypted line, preferably Signal "
        "(https://signal.org/). Make sure Signal's safety numbers have been "
        "verified, and then verbally compare the key fingerprints below.",
        head_clear=True,
        max_width=49,
        head=1,
        tail=1)

    print_fingerprint(tx_fp, "         Your fingerprint (you read)         ")
    print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)")

    return yes("Is the contact's fingerprint correct?")