Ejemplo n.º 1
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)
Ejemplo n.º 2
0
Archivo: path.py Proyecto: gtog/tfc
def cli_get_file(prompt_msg: str) -> str:
    """Ask the user to specify file to load."""
    while True:
        try:
            path_to_file = input(prompt_msg + ": ")

            if not path_to_file:
                print_on_previous_line()
                raise KeyboardInterrupt

            if os.path.isfile(path_to_file):
                if path_to_file.startswith('./'):
                    path_to_file = path_to_file[len('./'):]
                print('')
                return path_to_file

            m_print("File selection error.", head=1, tail=1)
            print_on_previous_line(reps=4, delay=1)

        except (EOFError, KeyboardInterrupt):
            print_on_previous_line()
            raise FunctionReturn("File selection aborted.", head_clear=True)
Ejemplo n.º 3
0
Archivo: gateway.py Proyecto: todun/tfc
    def change_setting(self, key: str, value_str: str) -> None:
        """Parse, update and store new setting value."""
        attribute = self.__getattribute__(key)
        try:
            if isinstance(attribute, bool):
                value = dict(true=True, false=False)[value_str.lower()]  # type: Union[bool, int]

            elif isinstance(attribute, int):
                value = int(value_str)
                if value < 0 or value > MAX_INT:
                    raise ValueError

            else:
                raise CriticalError("Invalid attribute type in settings.")

        except (KeyError, ValueError):
            raise FunctionReturn(f"Error: Invalid setting value '{value_str}'.", delay=1, tail_clear=True)

        self.validate_key_value_pair(key, value)

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

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

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

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

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

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

        if packet_ct is not None:
            self.log_ct_list = [packet_ct]
Ejemplo n.º 5
0
Archivo: path.py Proyecto: gtog/tfc
def cli_get_path(prompt_msg: str) -> str:
    """Ask the user to specify path for file."""
    while True:
        try:
            directory = input(prompt_msg + ": ")

            if directory.startswith('./'):
                directory = directory[len('./'):]

            if not directory.endswith(os.sep):
                directory += os.sep

            if not os.path.isdir(directory):
                m_print("Error: Invalid directory.", head=1, tail=1)
                print_on_previous_line(reps=4, delay=1)
                continue

            return directory

        except (EOFError, KeyboardInterrupt):
            print_on_previous_line()
            raise FunctionReturn("File path selection aborted.", head_clear=True)
Ejemplo n.º 6
0
Archivo: files.py Proyecto: gtog/tfc
def process_assembled_file(
        ts: 'datetime',  # Timestamp last received packet
        payload: bytes,  # File name and content
        onion_pub_key: bytes,  # Onion Service pubkey of sender
        nick: str,  # Nickname of sender
        settings: 'Settings',  # Settings object
        window_list: 'WindowList',  # WindowList object
) -> None:
    """Process received file assembly packets."""
    try:
        file_name_b, file_data = payload.split(US_BYTE, 1)
    except ValueError:
        raise FunctionReturn("Error: Received file had an invalid structure.")

    try:
        file_name = file_name_b.decode()
    except UnicodeError:
        raise FunctionReturn("Error: Received file name had invalid encoding.")

    if not file_name.isprintable() or not file_name or '/' in file_name:
        raise FunctionReturn("Error: Received file had an invalid name.")

    file_ct, file_key = separate_trailer(file_data, SYMMETRIC_KEY_LENGTH)

    if len(file_key) != SYMMETRIC_KEY_LENGTH:
        raise FunctionReturn("Error: Received file had an invalid key.")

    try:
        file_pt = auth_and_decrypt(file_ct, file_key)
    except nacl.exceptions.CryptoError:
        raise FunctionReturn("Error: Decryption of file data failed.")

    try:
        file_dc = decompress(file_pt, settings.max_decompress_size)
    except zlib.error:
        raise FunctionReturn("Error: Decompression of file data failed.")

    file_dir = f'{DIR_RECV_FILES}{nick}/'
    final_name = store_unique(file_dc, file_dir, file_name)

    message = f"Stored file from {nick} as '{final_name}'."
    if settings.traffic_masking and window_list.active_win is not None:
        window = window_list.active_win
    else:
        window = window_list.get_window(onion_pub_key)
    window.add_new(ts, message, onion_pub_key, output=True, event_msg=True)
Ejemplo n.º 7
0
def print_logs(message_list: List[MsgTuple], export: bool, msg_to_load: int,
               window: Union['TxWindow',
                             'RxWindow'], contact_list: 'ContactList',
               group_list: 'GroupList', settings: 'Settings') -> None:
    """Print list of logged messages to screen or export them to file."""
    terminal_width = get_terminal_width()
    system, m_dir = {
        TX: ("Transmitter", "sent to"),
        RX: ("Receiver", "to/from")
    }[settings.software_operation]

    f_name = open(f"{system} - Plaintext log ({window.name})",
                  'w+') if export else sys.stdout
    subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent "
    title = textwrap.fill(
        f"Log file of {subset}message(s) {m_dir} {window.type} {window.name}",
        terminal_width)

    packet_list = PacketList(settings, contact_list)
    log_window = RxWindow(window.uid, contact_list, group_list, settings,
                          packet_list)
    log_window.is_active = True
    log_window.message_log = message_list

    if message_list:
        if not export:
            clear_screen()
        print(title, file=f_name)
        print(terminal_width * '═', file=f_name)
        log_window.redraw(file=f_name)
        print("<End of log file>\n", file=f_name)
    else:
        raise FunctionReturn(
            f"No logged messages for {window.type} '{window.name}'.",
            head_clear=True)

    if export:
        f_name.close()
Ejemplo n.º 8
0
def group_remove(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList') -> None:
    """Remove member(s) from the group."""
    group_id, ser_members = separate_header(cmd_data, GROUP_ID_LENGTH)
    purp_pub_keys = set(
        split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH))

    try:
        group_name = group_list.get_group_by_id(group_id).name
    except StopIteration:
        raise FunctionReturn(
            f"Error: No group with ID '{b58encode(group_id)}' found.")

    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_accounts_set = set(purp_pub_keys & pub_keys)
    removable_set = set(before_removal & ok_accounts_set)

    not_in_group = list(ok_accounts_set - before_removal)
    rejected = list(purp_pub_keys - pub_keys)
    removable = list(removable_set)

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

    window = window_list.get_window(group.group_id)
    window.remove_contacts(removable)

    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)

    local_win = window_list.get_window(WIN_UID_LOCAL)
    local_win.add_new(ts, f"Removed members from group {group_name}.")
Ejemplo n.º 9
0
def remove_contact(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   key_list: 'KeyList') -> None:
    """Remove contact from RxM."""
    rx_account = cmd_data.decode()

    key_list.remove_keyset(rx_account)
    window_list.remove_window(rx_account)

    if not contact_list.has_contact(rx_account):
        raise FunctionReturn(f"RxM has no account '{rx_account}' to remove.")

    nick = contact_list.get_contact(rx_account).nick
    contact_list.remove_contact(rx_account)

    message = f"Removed {nick} from contacts."
    box_print(message, head=1, tail=1)

    local_win = window_list.get_local_window()
    local_win.add_new(ts, message)

    if any([g.remove_members([rx_account]) for g in group_list]):
        box_print(f"Removed {rx_account} from group(s).", tail=1)
Ejemplo n.º 10
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.")
Ejemplo n.º 11
0
def whisper(user_input: 'UserInput',
            window:     'TxWindow',
            settings:   'Settings',
            queues:     'QueueDict',
            ) -> None:
    """\
    Send a message to the contact that overrides their enabled logging
    setting for that message.

    The functionality of this feature is impossible to enforce, but if
    the recipient can be trusted and they do not modify their client,
    this feature can be used to send the message off-the-record.
    """
    try:
        message = user_input.plaintext.strip().split(' ', 1)[1]
    except IndexError:
        raise FunctionReturn("Error: No whisper message specified.", head_clear=True)

    queue_message(user_input=UserInput(message, MESSAGE),
                  window=window,
                  settings=settings,
                  queues=queues,
                  whisper=True,
                  log_as_ph=True)
Ejemplo n.º 12
0
    def change_setting(
            self,
            key: str,  # Name of the setting
            value_str: str,  # Value of the setting
            contact_list: 'ContactList',
            group_list: 'GroupList') -> None:
        """Parse, update and store new setting value."""
        attribute = self.__getattribute__(key)

        try:
            if isinstance(attribute, bool):
                value = dict(true=True, false=False)[
                    value_str.lower()]  # type: Union[bool, int, float]

            elif isinstance(attribute, int):
                value = int(value_str)
                if value < 0 or value > MAX_INT:
                    raise ValueError

            elif isinstance(attribute, float):
                value = float(value_str)
                if value < 0.0:
                    raise ValueError

            else:
                raise CriticalError("Invalid attribute type in settings.")

        except (KeyError, ValueError):
            raise FunctionReturn(
                f"Error: Invalid setting value '{value_str}'.",
                head_clear=True)

        self.validate_key_value_pair(key, value, contact_list, group_list)

        setattr(self, key, value)
        self.store_settings()
Ejemplo n.º 13
0
def ch_master_key(ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', group_list: 'GroupList',
                  key_list: 'KeyList', settings: 'Settings',
                  master_key: 'MasterKey') -> None:
    """Prompt the user for a new master password and derive a new master key from that."""
    try:
        old_master_key = master_key.master_key[:]
        master_key.master_key = master_key.new_master_key()

        phase("Re-encrypting databases")

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

        key_list.store_keys()
        settings.store_settings()
        contact_list.store_contacts()
        group_list.store_groups()

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

        local_win = window_list.get_local_window()
        local_win.add_new(ts, "Changed Receiver master password.")

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Password change aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
Ejemplo n.º 14
0
Archivo: windows.py Proyecto: gtog/tfc
    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)
Ejemplo n.º 15
0
def process_received_file(payload: bytes, nick: str) -> None:
    """Process received file assembly packets."""
    try:
        f_name_b, f_data = payload.split(US_BYTE)
    except ValueError:
        raise FunctionReturn("Error: Received file had invalid structure.")

    try:
        f_name = f_name_b.decode()
    except UnicodeError:
        raise FunctionReturn("Error: Received file name had invalid encoding.")

    if not f_name.isprintable() or not f_name:
        raise FunctionReturn("Error: Received file had an invalid name.")

    try:
        f_data = base64.b85decode(f_data)
    except (binascii.Error, ValueError):
        raise FunctionReturn("Error: Received file had invalid encoding.")

    file_ct = f_data[:-KEY_LENGTH]
    file_key = f_data[-KEY_LENGTH:]
    if len(file_key) != KEY_LENGTH:
        raise FunctionReturn("Error: Received file had an invalid key.")

    try:
        file_pt = auth_and_decrypt(file_ct, file_key, soft_e=True)
    except nacl.exceptions.CryptoError:
        raise FunctionReturn("Error: Decryption of file data failed.")

    try:
        file_dc = zlib.decompress(file_pt)
    except zlib.error:
        raise FunctionReturn("Error: Decompression of file data failed.")

    file_dir = f'{DIR_RX_FILES}{nick}/'
    final_name = store_unique(file_dc, file_dir, f_name)
    box_print(f"Stored file from {nick} as '{final_name}'")
Ejemplo n.º 16
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.")
Ejemplo n.º 17
0
    def validate_key_value_pair(
            key: str,  # Name of the setting
            value: Union[int, float, bool],  # Value of the setting
            contact_list: 'ContactList',
            group_list: 'GroupList') -> None:
        """Evaluate values for settings that have further restrictions."""
        if key in [
                'max_number_of_group_members', 'max_number_of_groups',
                'max_number_of_contacts'
        ]:
            if value % 10 != 0 or value == 0:
                raise FunctionReturn(
                    "Error: Database padding settings must be divisible by 10.",
                    head_clear=True)

        if key == 'max_number_of_group_members':
            min_size = round_up(group_list.largest_group())
            if value < min_size:
                raise FunctionReturn(
                    f"Error: Can't set the max number of members lower than {min_size}.",
                    head_clear=True)

        if key == 'max_number_of_groups':
            min_size = round_up(len(group_list))
            if value < min_size:
                raise FunctionReturn(
                    f"Error: Can't set the max number of groups lower than {min_size}.",
                    head_clear=True)

        if key == 'max_number_of_contacts':
            min_size = round_up(len(contact_list))
            if value < min_size:
                raise FunctionReturn(
                    f"Error: Can't set the max number of contacts lower than {min_size}.",
                    head_clear=True)

        if key == 'new_message_notify_duration' and value < 0.05:
            raise FunctionReturn(
                "Error: Too small value for message notify duration.",
                head_clear=True)

        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 FunctionReturn(
                        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 FunctionReturn(
                        "Aborted traffic masking setting change.",
                        head_clear=True)

            m_print("Traffic masking setting will change on restart.",
                    head=1,
                    tail=1)
Ejemplo n.º 18
0
def process_group_command(user_input: 'UserInput', contact_list: 'ContactList',
                          group_list: 'GroupList', settings: 'Settings',
                          queues: 'QueueDict',
                          master_key: 'MasterKey') -> None:
    """Parse a group command and process it accordingly."""
    if settings.traffic_masking:
        raise FunctionReturn(
            "Error: Command is disabled during traffic masking.",
            head_clear=True)

    input_parameters = user_input.plaintext.split()  # type: List[str]

    try:
        command_type = input_parameters[1]
    except IndexError:
        raise FunctionReturn("Error: Invalid group command.", head_clear=True)

    if command_type not in ['create', 'join', 'add', 'rm']:
        raise FunctionReturn("Error: Invalid group command.")

    group_id = None  # type: Optional[bytes]
    if command_type == 'join':
        try:
            group_id_s = input_parameters[2]
        except IndexError:
            raise FunctionReturn("Error: No group ID specified.",
                                 head_clear=True)
        try:
            group_id = b58decode(group_id_s)
        except ValueError:
            raise FunctionReturn("Error: Invalid group ID.", head_clear=True)

        if group_id in group_list.get_list_of_group_ids():
            raise FunctionReturn(
                "Error: Group with matching ID already exists.",
                head_clear=True)

    try:
        name_index = 3 if command_type == 'join' else 2
        group_name = input_parameters[name_index]
    except IndexError:
        raise FunctionReturn("Error: No group name specified.",
                             head_clear=True)

    member_index = 4 if command_type == 'join' else 3
    purp_members = input_parameters[member_index:]

    # Swap specified strings to public keys
    selectors = contact_list.contact_selectors()
    pub_keys = [
        contact_list.get_contact_by_address_or_nick(m).onion_pub_key
        for m in purp_members if m in selectors
    ]

    func = dict(create=group_create,
                join=group_create,
                add=group_add_member,
                rm=group_rm_member)[command_type]  # type: Callable

    func(group_name, pub_keys, contact_list, group_list, settings, queues,
         master_key, group_id)
    print('')
Ejemplo n.º 19
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 yes(f"Group {group_name} was not found. Create new group?",
               abort=False,
               head=1):
            group_create(group_name, purp_members, contact_list, group_list,
                         settings, queues, master_key)
            return None
        else:
            raise FunctionReturn("Group creation aborted.",
                                 head=0,
                                 delay=1,
                                 tail_clear=True)

    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 FunctionReturn(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} "
            f"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])
Ejemplo n.º 20
0
def cancel_packet(user_input: 'UserInput',
                  window:     'TxWindow',
                  settings:   'Settings',
                  queues:     'QueueDict'
                  ) -> None:
    """Cancel sent message/file to contact/group.

    In cases where the assembly packets have not yet been encrypted or
    output to Networked Computer, the queued messages or files to active
    window can be cancelled. Any single-packet message and file this
    function removes from the queue/transfer buffer are unavailable to
    recipient. However, in the case of multi-packet transmissions, if
    only the last assembly packet is cancelled, the recipient might
    obtain large enough section of the key that protects the inner
    encryption layer to allow them to brute force the rest of the key,
    and thus, decryption of the packet. There is simply no way to
    prevent this kind of attack without making TFC proprietary and
    re-writing it in a compiled language (which is very bad for users'
    rights).
    """
    header, p_type = dict(cm=(M_C_HEADER, 'messages'),
                          cf=(F_C_HEADER, 'files'   ))[user_input.plaintext]

    if settings.traffic_masking:
        queue = queues[TM_MESSAGE_PACKET_QUEUE] if header == M_C_HEADER else queues[TM_FILE_PACKET_QUEUE]
    else:
        if header == F_C_HEADER:
            raise FunctionReturn("Files are only queued during traffic masking.", head_clear=True)
        queue = queues[MESSAGE_PACKET_QUEUE]

    cancel_pt = header + bytes(PADDING_LENGTH)
    log_as_ph = False  # Never log cancel assembly packets as placeholder data

    cancel = False
    if settings.traffic_masking:
        if queue.qsize() != 0:
            cancel = True

            # Get most recent log_messages setting status in queue
            log_messages = False
            while queue.qsize() != 0:
                log_messages = queue.get()[1]

            queue.put((cancel_pt, log_messages, log_as_ph))

        m_print(f"Cancelled queues {p_type}." if cancel else f"No {p_type} to cancel.", head=1, tail=1)

    else:
        p_buffer = []
        while queue.qsize() != 0:
            queue_data = queue.get()
            window_uid = queue_data[4]

            # Put messages unrelated to the active window into the buffer
            if window_uid != window.uid:
                p_buffer.append(queue_data)
            else:
                cancel = True

        # Put cancel packets for each window contact to queue first
        if cancel:
            for c in window:
                queue.put((cancel_pt, c.onion_pub_key, c.log_messages, log_as_ph, window.uid))

        # Put buffered tuples back to the queue
        for p in p_buffer:
            queue.put(p)

        if cancel:
            message = f"Cancelled queued {p_type} to {window.type_print} {window.name}."
        else:
            message = f"No {p_type} queued for {window.type_print} {window.name}."

        raise FunctionReturn(message, head_clear=True)
Ejemplo n.º 21
0
def key_ex_psk_rx(packet: bytes, ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', key_list: 'KeyList',
                  settings: 'Settings') -> None:
    """Import Rx-PSK of contact."""
    c_code, onion_pub_key = separate_header(packet, CONFIRM_CODE_LENGTH)
    short_addr = pub_key_to_short_address(onion_pub_key)

    if not contact_list.has_pub_key(onion_pub_key):
        raise FunctionReturn(f"Error: Unknown account '{short_addr}'.",
                             head_clear=True)

    contact = contact_list.get_contact_by_pub_key(onion_pub_key)
    psk_file = ask_path_gui(f"Select PSK for {contact.nick} ({short_addr})",
                            settings,
                            get_file=True)

    try:
        with open(psk_file, 'rb') as f:
            psk_data = f.read()
    except PermissionError:
        raise FunctionReturn("Error: No read permission for the PSK file.")

    if len(psk_data) != PSK_FILE_SIZE:
        raise FunctionReturn("Error: The PSK data in the file was invalid.",
                             head_clear=True)

    salt, ct_tag = separate_header(psk_data, ARGON2_SALT_LENGTH)

    while True:
        try:
            password = MasterKey.get_password("PSK password")
            phase("Deriving the key decryption key", head=2)
            kdk = argon2_kdf(password,
                             salt,
                             time_cost=ARGON2_PSK_TIME_COST,
                             memory_cost=ARGON2_PSK_MEMORY_COST)
            psk = auth_and_decrypt(ct_tag, kdk)
            phase(DONE)
            break

        except nacl.exceptions.CryptoError:
            print_on_previous_line()
            m_print("Invalid password. Try again.", head=1)
            print_on_previous_line(reps=5, delay=1)
        except (EOFError, KeyboardInterrupt):
            raise FunctionReturn("PSK import aborted.",
                                 head=2,
                                 delay=1,
                                 tail_clear=True)

    rx_mk, rx_hk = separate_header(psk, SYMMETRIC_KEY_LENGTH)

    if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [rx_mk, rx_hk]):
        raise FunctionReturn("Error: Received invalid keys from contact.",
                             head_clear=True)

    keyset = key_list.get_keyset(onion_pub_key)
    keyset.rx_mk = rx_mk
    keyset.rx_hk = rx_hk
    key_list.store_keys()

    contact.kex_status = KEX_STATUS_HAS_RX_PSK
    contact_list.store_contacts()

    # Pipes protects against shell injection. Source of command's parameter is
    # the program itself, and therefore trusted, but it's still good practice.
    subprocess.Popen(f"shred -n 3 -z -u {pipes.quote(psk_file)}",
                     shell=True).wait()
    if os.path.isfile(psk_file):
        m_print(
            f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.",
            manual_proceed=True,
            box=True)

    message = f"Added Rx-side PSK for {contact.nick} ({short_addr})."
    local_win = window_list.get_local_window()
    local_win.add_new(ts, message)

    m_print([
        message, '', "Warning!",
        "Physically destroy the keyfile transmission media ",
        "to ensure it does not steal data from this computer!", '',
        f"Confirmation code (to Transmitter): {c_code.hex()}"
    ],
            box=True,
            head=1,
            tail=1)
Ejemplo n.º 22
0
def process_message(ts: 'datetime', assembly_packet_ct: bytes,
                    window_list: 'WindowList', packet_list: 'PacketList',
                    contact_list: 'ContactList', key_list: 'KeyList',
                    group_list: 'GroupList', settings: 'Settings',
                    master_key: 'MasterKey', file_keys: Dict[bytes,
                                                             bytes]) -> None:
    """Process received private / group message."""
    local_window = window_list.get_local_window()

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

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

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

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

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

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

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

    if not packet.is_complete:
        return None

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

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

            whisper = bytes_to_bool(whisper_byte)

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

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

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

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

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

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

    except (FunctionReturn, UnicodeError):
        log_masking_packets(completed=True)
        raise
    finally:
        packet.clear_assembly_packets()
Ejemplo n.º 23
0
def ch_contact_s(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList',
                 header: bytes) -> None:
    """Change contact/group related setting."""
    setting, win_uid = separate_header(cmd_data, CONTACT_SETTING_HEADER_LENGTH)
    attr, desc, file_cmd = {
        CH_LOGGING: ('log_messages', "Logging of messages", False),
        CH_FILE_RECV: ('file_reception', "Reception of files", True),
        CH_NOTIFY: ('notifications', "Message notifications", False)
    }[header]

    action, b_value = {
        ENABLE: ('enabled', True),
        DISABLE: ('disabled', False)
    }[setting.lower()]

    if setting.isupper():
        # Change settings for all contacts (and groups)
        enabled = [
            getattr(c, attr) for c in contact_list.get_list_of_contacts()
        ]
        enabled += [getattr(g, attr)
                    for g in group_list] if not file_cmd else []
        status = "was already" if (
            (all(enabled) and b_value) or
            (not any(enabled) and not b_value)) else "has been"
        specifier = "every "
        w_type = "contact"
        w_name = "." if file_cmd else " and group."

        # Set values
        for c in contact_list.get_list_of_contacts():
            setattr(c, attr, b_value)
        contact_list.store_contacts()

        if not file_cmd:
            for g in group_list:
                setattr(g, attr, b_value)
            group_list.store_groups()

    else:
        # Change setting for contacts in specified window
        if not window_list.has_window(win_uid):
            raise FunctionReturn(
                f"Error: Found no window for '{pub_key_to_short_address(win_uid)}'."
            )

        window = window_list.get_window(win_uid)
        group_window = window.type == WIN_TYPE_GROUP
        contact_window = window.type == WIN_TYPE_CONTACT

        if contact_window:
            target = contact_list.get_contact_by_pub_key(
                win_uid)  # type: Union[Contact, Group]
        else:
            target = group_list.get_group_by_id(win_uid)

        if file_cmd:
            enabled = [getattr(m, attr) for m in window.window_contacts]
            changed = not all(enabled) if b_value else any(enabled)
        else:
            changed = getattr(target, attr) != b_value

        status = "has been" if changed else "was already"
        specifier = "members in " if (file_cmd and group_window) else ''
        w_type = window.type
        w_name = f" {window.name}."

        # Set values
        if contact_window or (group_window and file_cmd):
            for c in window.window_contacts:
                setattr(c, attr, b_value)
            contact_list.store_contacts()

        elif group_window:
            setattr(group_list.get_group_by_id(win_uid), attr, b_value)
            group_list.store_groups()

    message = f"{desc} {status} {action} for {specifier}{w_type}{w_name}"
    local_win = window_list.get_local_window()
    local_win.add_new(ts, message, output=True)
Ejemplo n.º 24
0
Archivo: windows.py Proyecto: 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()
Ejemplo n.º 25
0
    def print_settings(self) -> None:
        """\
        Print list of settings, their current and
        default values, and setting descriptions.
        """
        desc_d = {
            # Common settings
            "disable_gui_dialog":
            "True replaces GUI dialogs with CLI prompts",
            "max_number_of_group_members":
            "Maximum number of members in a group",
            "max_number_of_groups":
            "Maximum number of groups",
            "max_number_of_contacts":
            "Maximum number of contacts",
            "log_messages_by_default":
            "Default logging setting for new contacts/groups",
            "accept_files_by_default":
            "Default file reception setting for new contacts",
            "show_notifications_by_default":
            "Default message notification setting for new contacts/groups",
            "log_file_masking":
            "True hides real size of log file during traffic masking",
            "ask_password_for_log_access":
            "False disables password prompt when viewing/exporting logs",

            # Transmitter settings
            "nc_bypass_messages":
            "False removes Networked Computer bypass interrupt messages",
            "confirm_sent_files":
            "False sends files without asking for confirmation",
            "double_space_exits":
            "True exits, False clears screen with double space command",
            "traffic_masking":
            "True enables traffic masking to hide metadata",
            "tm_static_delay":
            "The static delay between traffic masking packets",
            "tm_random_delay":
            "Max random delay for traffic masking timing obfuscation",

            # Relay settings
            "allow_contact_requests":
            "When False, does not show TFC contact requests",

            # Receiver settings
            "new_message_notify_preview":
            "When True, shows a preview of the received message",
            "new_message_notify_duration":
            "Number of seconds new message notification appears",
            "max_decompress_size":
            "Max size Receiver accepts when decompressing file"
        }

        # Columns
        c1 = ['Setting name']
        c2 = ['Current value']
        c3 = ['Default value']
        c4 = ['Description']

        terminal_width = get_terminal_width()
        description_indent = 64

        if terminal_width < description_indent + 1:
            raise FunctionReturn("Error: Screen width is too small.",
                                 head_clear=True)

        # Populate columns with setting data
        for key in self.defaults:
            c1.append(key)
            c2.append(str(self.__getattribute__(key)))
            c3.append(str(self.defaults[key]))

            description = desc_d[key]
            wrapper = textwrap.TextWrapper(
                width=max(1, (terminal_width - description_indent)))
            desc_lines = wrapper.fill(description).split('\n')
            desc_string = desc_lines[0]

            for line in desc_lines[1:]:
                desc_string += '\n' + description_indent * ' ' + line

            if len(desc_lines) > 1:
                desc_string += '\n'

            c4.append(desc_string)

        # Calculate column widths
        c1w, c2w, c3w = [
            max(len(v) for v in column) + SETTINGS_INDENT
            for column in [c1, c2, c3]
        ]

        # Align columns by adding whitespace between fields of each line
        lines = [
            f'{f1:{c1w}} {f2:{c2w}} {f3:{c3w}} {f4}'
            for f1, f2, f3, f4 in zip(c1, c2, c3, c4)
        ]

        # Add a terminal-wide line between the column names and the data
        lines.insert(1, get_terminal_width() * '─')

        # Print the settings
        clear_screen()
        print('\n' + '\n'.join(lines))
Ejemplo n.º 26
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings', kdk_hashes: List[bytes],
                      packet_hashes: List[bytes], l_queue: 'Queue') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    bootstrap = not key_list.has_local_keyset()
    plaintext = None

    try:
        packet_hash = blake2b(packet)

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

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

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

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

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

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

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

            break

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

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

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

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

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

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

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

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

        if bootstrap:
            window_list.active_win = local_win

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

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

        raise FunctionReturn("Local key setup aborted.", output=False)
Ejemplo n.º 27
0
def send_file(path:     str,
              settings: 'Settings',
              queues:   'QueueDict',
              window:   'TxWindow'
              ) -> None:
    """Send file to window members in a single transmission.

    This is the default mode for file transmission, used when traffic
    masking is not enabled. The file is loaded and compressed before it
    is encrypted. The encrypted file is then exported to Networked
    Computer along with a list of Onion Service public keys (members in
    window) of all recipients to whom the Relay Program will multi-cast
    the file to.

    Once the file ciphertext has been exported, this function will
    multi-cast the file decryption key to each recipient inside an
    automated key delivery message that uses a special FILE_KEY_HEADER
    in place of standard PRIVATE_MESSAGE_HEADER. To know for which file
    ciphertext the key is for, an identifier must be added to the key
    delivery message. The identifier in this case is the BLAKE2b digest
    of the ciphertext itself. The reason of using the digest as the
    identifier is, it authenticates both the ciphertext and its origin.
    To understand this, consider the following attack scenario:

    Let the file ciphertext identifier be just a random 32-byte value "ID".

    1) Alice sends Bob and Chuck (a malicious common peer) a file
       ciphertext and identifier CT|ID (where | denotes concatenation).

    2) Chuck who has compromised Bob's Networked Computer interdicts the
       CT|ID from Alice.

    3) Chuck decrypts CT in his end, makes edits to the plaintext PT to
       create PT'.

    4) Chuck re-encrypts PT' with the same symmetric key to produce CT'.

    5) Chuck re-uses the ID and produces CT'|ID.

    6) Chuck uploads the CT'|ID to Bob's Networked Computer and replaces
       the interdicted CT|ID with it.

    7) When Bob' Receiver Program receives the automated key delivery
       message from Alice, his Receiver program uses the bundled ID to
       identify the key is for CT'.

    8) Bob's Receiver decrypts CT' using the newly received key and
       obtains Chuck's PT', that appears to come from Alice.

    Now, consider a situation where the ID is instead calculated
    ID = BLAKE2b(CT), if Chuck edits the PT, the CT' will by definition
    be different from CT, and the BLAKE2b digest will also be different.
    In order to make Bob decrypt CT', Chuck needs to also change the
    hash in Alice's key delivery message, which means Chuck needs to
    create an existential forgery of the TFC message. Since the Poly1305
    tag prevents this, the calculated ID is enough to authenticate the
    ciphertext.

    If Chuck attempts to send their own key delivery message, Chuck's
    own Onion Service public key used to identify the TFC message key
    (decryption key for the key delivery message) will be permanently
    associated with the file hash, so if they inject a file CT, and Bob
    has decided to enable file reception for Chuck, the file CT will
    appear to come from Chuck, and not from Alice. From the perspective
    of Bob, it's as if Chuck had dropped Alice's file and sent him
    another file instead.
    """
    from src.transmitter.windows import MockWindow  # Avoid circular import

    if settings.traffic_masking:
        raise FunctionReturn("Error: Command is disabled during traffic masking.", head_clear=True)

    name = path.split('/')[-1]
    data = bytearray()
    data.extend(str_to_bytes(name))

    if not os.path.isfile(path):
        raise FunctionReturn("Error: File not found.", head_clear=True)

    if os.path.getsize(path) == 0:
        raise FunctionReturn("Error: Target file is empty.", head_clear=True)

    phase("Reading data")
    with open(path, 'rb') as f:
        data.extend(f.read())
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Compressing data")
    comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL))
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Encrypting data")
    file_key = csprng()
    file_ct  = encrypt_and_sign(comp, file_key)
    ct_hash  = blake2b(file_ct)
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Exporting data")
    no_contacts  = int_to_bytes(len(window))
    ser_contacts = b''.join([c.onion_pub_key for c in window])
    file_packet  = FILE_DATAGRAM_HEADER + no_contacts + ser_contacts + file_ct
    queue_to_nc(file_packet, queues[RELAY_PACKET_QUEUE])

    key_delivery_msg = base64.b85encode(ct_hash + file_key).decode()
    for contact in window:
        queue_message(user_input=UserInput(key_delivery_msg, MESSAGE),
                      window    =MockWindow(contact.onion_pub_key, [contact]),
                      settings  =settings,
                      queues    =queues,
                      header    =FILE_KEY_HEADER,
                      log_as_ph =True)
    phase(DONE)
    print_on_previous_line(flush=True)
    m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
Ejemplo n.º 28
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)
Ejemplo n.º 29
0
def process_command(ts: 'datetime', assembly_ct: bytes,
                    window_list: 'WindowList', packet_list: 'PacketList',
                    contact_list: 'ContactList', key_list: 'KeyList',
                    group_list: 'GroupList', settings: 'Settings',
                    master_key: 'MasterKey', gateway: 'Gateway',
                    exit_queue: 'Queue[bytes]') -> None:
    """Decrypt command assembly packet and process command."""
    assembly_packet = decrypt_assembly_packet(assembly_ct, LOCAL_PUBKEY,
                                              ORIGIN_USER_HEADER, window_list,
                                              contact_list, key_list)

    cmd_packet = packet_list.get_packet(LOCAL_PUBKEY, ORIGIN_USER_HEADER,
                                        COMMAND)
    cmd_packet.add_packet(assembly_packet)

    if not cmd_packet.is_complete:
        raise FunctionReturn("Incomplete command.", output=False)

    header, cmd = separate_header(cmd_packet.assemble_command_packet(),
                                  ENCRYPTED_COMMAND_HEADER_LENGTH)
    no = None

    #    Keyword        Function to run (                                 Parameters                                  )
    #    --------------------------------------------------------------------------------------------------------------
    d = {
        LOCAL_KEY_RDY: (local_key_rdy, ts, window_list, contact_list),
        WIN_ACTIVITY: (win_activity, window_list),
        WIN_SELECT: (win_select, cmd, window_list),
        CLEAR_SCREEN: (clear_screen, ),
        RESET_SCREEN: (reset_screen, cmd, window_list),
        EXIT_PROGRAM: (exit_tfc, exit_queue),
        LOG_DISPLAY: (log_command, cmd, no, window_list, contact_list,
                      group_list, settings, master_key),
        LOG_EXPORT: (log_command, cmd, ts, window_list, contact_list,
                     group_list, settings, master_key),
        LOG_REMOVE:
        (remove_log, cmd, contact_list, group_list, settings, master_key),
        CH_MASTER_KEY: (ch_master_key, ts, window_list, contact_list,
                        group_list, key_list, settings, master_key),
        CH_NICKNAME: (
            ch_nick,
            cmd,
            ts,
            window_list,
            contact_list,
        ),
        CH_SETTING: (ch_setting, cmd, ts, window_list, contact_list,
                     group_list, key_list, settings, gateway),
        CH_LOGGING:
        (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header),
        CH_FILE_RECV:
        (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header),
        CH_NOTIFY:
        (ch_contact_s, cmd, ts, window_list, contact_list, group_list, header),
        GROUP_CREATE: (group_create, cmd, ts, window_list, contact_list,
                       group_list, settings),
        GROUP_ADD: (group_add, cmd, ts, window_list, contact_list, group_list,
                    settings),
        GROUP_REMOVE: (group_remove, cmd, ts, window_list, contact_list,
                       group_list),
        GROUP_DELETE: (group_delete, cmd, ts, window_list, group_list),
        GROUP_RENAME: (group_rename, cmd, ts, window_list, contact_list,
                       group_list),
        KEY_EX_ECDHE: (key_ex_ecdhe, cmd, ts, window_list, contact_list,
                       key_list, settings),
        KEY_EX_PSK_TX: (key_ex_psk_tx, cmd, ts, window_list, contact_list,
                        key_list, settings),
        KEY_EX_PSK_RX: (key_ex_psk_rx, cmd, ts, window_list, contact_list,
                        key_list, settings),
        CONTACT_REM: (contact_rem, cmd, ts, window_list, contact_list,
                      group_list, key_list, settings, master_key),
        WIPE_USR_DATA: (wipe, exit_queue)
    }  # type: Dict[bytes, Any]

    try:
        from_dict = d[header]
    except KeyError:
        raise FunctionReturn("Error: Received an invalid command.")

    func = from_dict[0]
    parameters = from_dict[1:]
    func(*parameters)
Ejemplo n.º 30
0
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)