Example #1
0
def update_url_token(url_token_private_key: 'X448PrivateKey',
                     ut_pubkey_hex: str, cached_pk: str, onion_pub_key: bytes,
                     queues: 'QueueDict') -> Tuple[str, str]:
    """Update URL token for contact.

    When contact's URL token public key changes, update URL token.
    """
    if ut_pubkey_hex == cached_pk:
        raise SoftError("URL token public key has not changed.", output=False)

    try:
        public_key = bytes.fromhex(ut_pubkey_hex)

        if len(public_key) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes(
                TFC_PUBLIC_KEY_LENGTH):
            raise ValueError

        shared_secret = url_token_private_key.exchange(
            X448PublicKey.from_public_bytes(public_key))
        url_token = hashlib.blake2b(shared_secret,
                                    digest_size=URL_TOKEN_LENGTH).hexdigest()

        queues[URL_TOKEN_QUEUE].put(
            (onion_pub_key,
             url_token))  # Update Flask server's URL token for contact

        return url_token, ut_pubkey_hex

    except (TypeError, ValueError):
        raise SoftError("URL token derivation failed.", output=False)
Example #2
0
def change_setting(user_input: 'UserInput', window: 'TxWindow',
                   contact_list: 'ContactList', group_list: 'GroupList',
                   settings: 'Settings', queues: 'QueueDict',
                   master_key: 'MasterKey', gateway: 'Gateway') -> None:
    """Change setting on Transmitter and Receiver Program."""
    # Validate the KV-pair
    try:
        setting = user_input.plaintext.split()[1]
    except IndexError:
        raise SoftError("Error: No setting specified.", head_clear=True)

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

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

    relay_settings = dict(serial_error_correction=UNENCRYPTED_EC_RATIO,
                          serial_baudrate=UNENCRYPTED_BAUDRATE,
                          allow_contact_requests=UNENCRYPTED_MANAGE_CONTACT_REQ
                          )  # type: Dict[str, bytes]

    check_setting_change_conditions(setting, settings, relay_settings,
                                    master_key)

    change_setting_value(setting, value, relay_settings, queues, contact_list,
                         group_list, settings, gateway)

    propagate_setting_effects(setting, queues, contact_list, group_list,
                              settings, window)
Example #3
0
def group_rename(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList') -> None:
    """Rename the group."""
    group_id, new_name_bytes = separate_header(cmd_data, GROUP_ID_LENGTH)

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

    try:
        new_name = new_name_bytes.decode()
    except UnicodeError:
        raise SoftError(
            f"Error: New name for group '{group.name}' was invalid.")

    error_msg = validate_group_name(new_name, contact_list, group_list)
    if error_msg:
        raise SoftError(error_msg)

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

    window = window_list.get_window(group.group_id)
    window.name = new_name

    message = f"Renamed group '{old_name}' to '{new_name}'."
    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, message, output=True)
Example #4
0
def verify(window: 'TxWindow', contact_list: 'ContactList') -> None:
    """Verify fingerprints with contact."""
    if window.type == WIN_TYPE_GROUP or window.contact is None:
        raise SoftError("Error: A group is selected.", head_clear=True)

    if window.contact.uses_psk():
        raise SoftError("Pre-shared keys have no fingerprints.",
                        head_clear=True)

    try:
        verified = verify_fingerprints(window.contact.tx_fingerprint,
                                       window.contact.rx_fingerprint)
    except (EOFError, KeyboardInterrupt):
        raise SoftError("Fingerprint verification aborted.",
                        delay=1,
                        head=2,
                        tail_clear=True)

    status_hr, status = {
        True: ("Verified", KEX_STATUS_VERIFIED),
        False: ("Unverified", KEX_STATUS_UNVERIFIED)
    }[verified]

    window.contact.kex_status = status
    contact_list.store_contacts()
    m_print(f"Marked fingerprints with {window.name} as '{status_hr}'.",
            bold=True,
            tail_clear=True,
            delay=1,
            tail=1)
Example #5
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)
Example #6
0
    def detect_errors(self, packet: bytes) -> bytes:
        """Handle received packet error detection and/or correction."""
        if self.settings.qubes:
            try:
                packet = base64.b85decode(packet)
            except ValueError:
                raise SoftError(
                    "Error: Received packet had invalid Base85 encoding.")

        if self.settings.session_serial_error_correction and not self.settings.qubes:
            try:
                packet, _ = self.rs.decode(packet)
                return bytes(packet)
            except ReedSolomonError:
                raise SoftError(
                    "Error: Reed-Solomon failed to correct errors in the received packet.",
                    bold=True)
        else:
            packet, checksum = separate_trailer(packet, PACKET_CHECKSUM_LENGTH)

            if hashlib.blake2b(
                    packet,
                    digest_size=PACKET_CHECKSUM_LENGTH).digest() != checksum:
                raise SoftError(
                    "Warning! Received packet had an invalid checksum.",
                    bold=True)
            return packet
Example #7
0
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 SoftError("Error: Received file had an invalid structure.")

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

    if not file_name.isprintable() or not file_name or '/' in file_name:
        raise SoftError("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 SoftError("Error: Received file had an invalid key.")

    decrypt_and_store_file(ts, file_ct, file_key, file_name, onion_pub_key,
                           nick, window_list, settings)
Example #8
0
def validate_contact_public_key(tfc_public_key_contact: bytes) -> None:
    """This function validates the public key from contact.

    The validation takes into account key state and it will detect if
    the public key is zero, but it can't predict whether the shared key
    will be zero. Further validation of the public key is done by the
    `src.common.crypto` module.
    """
    if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH:
        m_print([
            "Warning!", "Received invalid size public key.",
            "Aborting key exchange for your safety."
        ],
                bold=True,
                tail=1)
        raise SoftError("Error: Invalid public key length", output=False)

    if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH):
        # The public key of contact is zero with negligible probability,
        # therefore we assume such key is malicious and attempts to set
        # the shared key to zero.
        m_print([
            "Warning!", "Received a malicious zero-public key.",
            "Aborting key exchange for your safety."
        ],
                bold=True,
                tail=1)
        raise SoftError("Error: Zero public key", output=False)
Example #9
0
def rxp_load_psk(window:       'TxWindow',
                 contact_list: 'ContactList',
                 settings:     'Settings',
                 queues:       'QueueDict',
                 ) -> None:
    """Send command to Receiver Program to load PSK for active contact."""
    if settings.traffic_masking:
        raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)

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

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

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

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

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

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

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

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

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

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

    command = CH_NICKNAME + onion_pub_key + nick.encode()
    queue_command(command, settings, queues)
Example #11
0
def ch_setting(cmd_data:     bytes,
               ts:           'datetime',
               window_list:  'WindowList',
               contact_list: 'ContactList',
               group_list:   'GroupList',
               key_list:     'KeyList',
               settings:     'Settings',
               gateway:      'Gateway'
               ) -> None:
    """Change TFC setting."""
    try:
        setting, value = [f.decode() for f in cmd_data.split(US_BYTE)]
    except ValueError:
        raise SoftError("Error: Received invalid setting data.")

    if setting in settings.key_list:
        settings.change_setting(setting, value, contact_list, group_list)
    elif setting in gateway.settings.key_list:
        gateway.settings.change_setting(setting, value)
    else:
        raise SoftError(f"Error: Invalid setting '{setting}'.")

    cmd_win = window_list.get_command_window()
    cmd_win.add_new(ts, f"Changed setting '{setting}' to '{value}'.", output=True)

    if setting == 'max_number_of_contacts':
        contact_list.store_contacts()
        key_list.store_keys()
    if setting in ['max_number_of_group_members', 'max_number_of_groups']:
        group_list.store_groups()
Example #12
0
def process_file_key_message(
    assembled: bytes,  # File decryption key
    onion_pub_key: bytes,  # Onion address of associated contact
    origin: bytes,  # Origin of file key packet (user / contact)
    contact_list: 'ContactList',  # ContactList object
    file_keys: Dict[
        bytes, bytes]  # Dictionary of file identifiers and decryption keys
) -> str:
    """Process received file key delivery message."""
    if origin == ORIGIN_USER_HEADER:
        raise SoftError("File key message from the user.", output=False)

    try:
        decoded = base64.b85decode(assembled)
    except ValueError:
        raise SoftError("Error: Received an invalid file key message.")

    ct_hash, file_key = separate_header(decoded, BLAKE2_DIGEST_LENGTH)

    if len(ct_hash) != BLAKE2_DIGEST_LENGTH or len(
            file_key) != SYMMETRIC_KEY_LENGTH:
        raise SoftError("Error: Received an invalid file key message.")

    file_keys[onion_pub_key + ct_hash] = file_key
    nick = contact_list.get_nick_by_pub_key(onion_pub_key)

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

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

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

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

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

    return selector
Example #14
0
File: files.py Project: dimwap/tfc
def new_file(
        ts: 'datetime',  # Timestamp of received packet
        packet: bytes,  # Sender of file and file ciphertext
        file_keys: Dict[bytes, bytes],  # Dictionary for file decryption keys
        file_buf: Dict[bytes,
                       Tuple['datetime',
                             bytes]],  # Dictionary for cached file ciphertexts
        contact_list: 'ContactList',  # ContactList object
        window_list: 'WindowList',  # WindowList object
        settings: 'Settings'  # Settings object
) -> None:
    """Validate received file and process or cache it."""
    onion_pub_key, _, file_ct = separate_headers(
        packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH])

    if not contact_list.has_pub_key(onion_pub_key):
        raise SoftError("File from an unknown account.", output=False)

    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    if not contact.file_reception:
        raise SoftError(
            f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.",
            bold=True)

    k = onion_pub_key + blake2b(file_ct)  # Dictionary key

    if k in file_keys:
        decryption_key = file_keys[k]
        process_file(ts, onion_pub_key, file_ct, decryption_key, contact_list,
                     window_list, settings)
        file_keys.pop(k)
    else:
        file_buf[k] = (ts, file_ct)
Example #15
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)
Example #16
0
def group_rename(
    new_name: str,
    window: 'TxWindow',
    contact_list: 'ContactList',
    group_list: 'GroupList',
    settings: 'Settings',
    queues: 'QueueDict',
) -> None:
    """Rename the active group."""
    if window.type == WIN_TYPE_CONTACT or window.group is None:
        raise SoftError("Error: Selected window is not a group window.",
                        head_clear=True)

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

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

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

    raise SoftError(f"Renamed group '{old_name}' to '{new_name}'.",
                    delay=1,
                    tail_clear=True)
Example #17
0
def decrypt_and_store_file(
        ts: 'datetime',  # Timestamp of received packet
        file_ct: bytes,  # File ciphertext
        file_key: bytes,  # File decryption key
        file_name: str,  # Name of the file
        onion_pub_key: bytes,  # Onion Service pubkey of sender
        nick: str,  # Nickname of sender
        window_list: 'WindowList',  # WindowList object
        settings: 'Settings'  # Settings object
) -> None:
    """Decrypt and store file."""
    try:
        file_pt = auth_and_decrypt(file_ct, file_key)
    except nacl.exceptions.CryptoError:
        raise SoftError("Error: Decryption of file data failed.")

    try:
        file_dc = decompress(file_pt, settings.max_decompress_size)
    except zlib.error:
        raise SoftError("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)
Example #18
0
def group_add_member(group_name: str,
                     purp_members: List['bytes'],
                     contact_list: 'ContactList',
                     group_list: 'GroupList',
                     settings: 'Settings',
                     queues: 'QueueDict',
                     master_key: 'MasterKey',
                     _: Optional[bytes] = None) -> None:
    """Add new member(s) to a specified group."""
    if group_name not in group_list.get_list_of_group_names():
        if not yes(f"Group {group_name} was not found. Create new group?",
                   abort=False,
                   head=1):
            raise SoftError("Group creation aborted.",
                            head=0,
                            delay=1,
                            tail_clear=True)
        group_create(group_name, purp_members, contact_list, group_list,
                     settings, queues, master_key)
        return None

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

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

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

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

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

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

    if new_in_group:
        if yes("Publish the list of new members to involved?", abort=False):
            add_packet = (GROUP_MSG_MEMBER_ADD_HEADER + group.group_id +
                          int_to_bytes(len(before_adding)) +
                          b''.join(before_adding) + b''.join(new_in_group))
            queue_to_nc(add_packet, queues[RELAY_PACKET_QUEUE])
Example #19
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)
Example #20
0
    def validate_serial_interface_value(self, key: str, json_dict: Any) -> None:
        """Validate the serial interface setting value."""
        if not isinstance(json_dict[key], str):
            self.invalid_setting(key, json_dict)
            raise SoftError("Invalid value", output=False)

        if not any(json_dict[key] == f for f in os.listdir('/sys/class/tty')):
            self.invalid_setting(key, json_dict)
            raise SoftError("Invalid value", output=False)
Example #21
0
    def check_long_packet(self) -> None:
        """Check if the long packet has permission to be extended."""
        if not self.long_active:
            self.add_masking_packet_to_log_file()
            raise SoftError("Missing start packet.", output=False)

        if self.type == FILE and not self.contact.file_reception:
            self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list) + 1)
            self.clear_assembly_packets()
            raise SoftError("Alert! File reception disabled mid-transfer.")
Example #22
0
def process_group_message(
        ts: 'datetime',  # Timestamp of group message
        assembled: bytes,  # Group message and its headers
        onion_pub_key: bytes,  # Onion address of associated contact
        origin: bytes,  # Origin of group message (user / contact)
        whisper: bool,  # When True, message is not logged.
        group_list: 'GroupList',  # GroupList object
        window_list: 'WindowList'  # WindowList object
) -> bool:
    """Process a group message."""
    group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH)
    if not group_list.has_group_id(group_id):
        raise SoftError("Error: Received message to an unknown group.",
                        output=False)

    group = group_list.get_group_by_id(group_id)
    if not group.has_member(onion_pub_key):
        raise SoftError("Error: Account is not a member of the group.",
                        output=False)

    group_msg_id, group_message = separate_header(assembled,
                                                  GROUP_MSG_ID_LENGTH)

    try:
        group_message_str = group_message.decode()
    except UnicodeError:
        raise SoftError("Error: Received an invalid group message.")

    window = window_list.get_window(group.group_id)

    # All copies of group messages the user sends to members contain
    # the same message ID. This allows the Receiver Program to ignore
    # duplicates of outgoing messages sent by the user to each member.
    if origin == ORIGIN_USER_HEADER:
        if window.group_msg_id != group_msg_id:
            window.group_msg_id = group_msg_id
            window.add_new(ts,
                           group_message_str,
                           onion_pub_key,
                           origin,
                           output=True,
                           whisper=whisper)

    elif origin == ORIGIN_CONTACT_HEADER:
        window.add_new(ts,
                       group_message_str,
                       onion_pub_key,
                       origin,
                       output=True,
                       whisper=whisper)

    # Return the group's logging setting because it might be different
    # from the logging setting of the contact who sent group message.
    return group.log_messages
Example #23
0
    def test_function_return(self) -> None:
        error = SoftError('test message')
        self.assertEqual(error.message, 'test message')

        error = SoftError('test message', head_clear=True)
        self.assertEqual(error.message, 'test message')

        error = SoftError('test message', tail_clear=True)
        self.assertEqual(error.message, 'test message')

        error = SoftError('test message', window=RxWindow())
        self.assertEqual(error.message, 'test message')
Example #24
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 SoftError("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 SoftError("File selection aborted.", head_clear=True)
        except (EOFError, KeyboardInterrupt):
            raise SoftError("File selection aborted.", head_clear=True)

    queue_assembly_packets(assembly_packets,
                           FILE,
                           settings,
                           queues,
                           window,
                           log_as_ph=True)
Example #25
0
def process_message_packet(
        ts: 'datetime',  # Timestamp of received message packet
        assembly_packet_ct: bytes,  # Encrypted assembly packet
        window_list: 'WindowList',  # WindowList object
        packet_list: 'PacketList',  # PacketList object
        contact_list: 'ContactList',  # ContactList object
        key_list: 'KeyList',  # KeyList object
        group_list: 'GroupList',  # GroupList object
        settings: 'Settings',  # Settings object
        file_keys: Dict[bytes, bytes],  # Dictionary of file decryption keys
        message_log: 'MessageLog',  # MessageLog object
) -> None:
    """Process received message packet."""
    command_window = window_list.get_command_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 SoftError("Warning! Received packet masqueraded as a command.",
                        window=command_window)

    if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
        raise SoftError("Error: Received packet had an invalid origin-header.",
                        window=command_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

    try:
        packet.add_packet(assembly_packet)
    except SoftError:
        log_masking_packets(onion_pub_key, origin, logging, settings, packet,
                            message_log)
        raise
    log_masking_packets(onion_pub_key, origin, logging, settings, packet,
                        message_log)

    if packet.is_complete:
        process_complete_message_packet(ts, onion_pub_key, p_type, origin,
                                        logging, packet, window_list,
                                        contact_list, group_list, settings,
                                        message_log, file_keys)
Example #26
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)
Example #27
0
    def assemble_command_packet(self) -> bytes:
        """Assemble command packet."""
        padded  = b''.join([p[ASSEMBLY_PACKET_HEADER_LENGTH:] for p in self.assembly_pt_list])
        payload = rm_padding_bytes(padded)

        if len(self.assembly_pt_list) > 1:
            payload, cmd_hash = separate_trailer(payload, BLAKE2_DIGEST_LENGTH)
            if blake2b(payload) != cmd_hash:
                raise SoftError("Error: Received an invalid command.")

        try:
            return decompress(payload, self.settings.max_decompress_size)
        except zlib.error:
            raise SoftError("Error: Decompression of command failed.")
Example #28
0
def contact_setting(user_input: 'UserInput', window: 'TxWindow',
                    contact_list: 'ContactList', group_list: 'GroupList',
                    settings: 'Settings', queues: 'QueueDict') -> None:
    """\
    Change logging, file reception, or notification setting of a group
    or (all) contact(s).
    """
    try:
        parameters = user_input.plaintext.split()
        cmd_key = parameters[0]
        cmd_header = {
            LOGGING: CH_LOGGING,
            STORE: CH_FILE_RECV,
            NOTIFY: CH_NOTIFY
        }[cmd_key]

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

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

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

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

    command = cmd_header + cmd_value

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

    window.update_log_messages()

    queue_command(command, settings, queues)
Example #29
0
def group_rm_group(group_name: str,
                   contact_list: 'ContactList',
                   group_list: 'GroupList',
                   settings: 'Settings',
                   queues: 'QueueDict',
                   master_key: 'MasterKey',
                   _: Optional[bytes] = None) -> None:
    """Remove the group with its members."""
    if not yes(f"Remove group '{group_name}'?", abort=False):
        raise SoftError("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 SoftError("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(SoftError):
            remove_logs(contact_list, group_list, settings, master_key,
                        group_id)
    else:
        raise SoftError(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 SoftError(f"Removed group '{group_name}'.",
                    head=0,
                    delay=1,
                    tail_clear=True,
                    bold=True)
Example #30
0
def validate_contact_fingerprint(tx_fp: bytes, rx_fp: bytes) -> bytes:
    """Validate or skip validation of contact fingerprint.

    This function prompts the user to verify the fingerprint of the contact.
    If the user issues Ctrl+{C,D} command, this function will set the key
    exchange status as unverified.
    """
    try:
        if not verify_fingerprints(tx_fp, rx_fp):
            m_print([
                "Warning!", "Possible man-in-the-middle attack detected.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise SoftError("Error: Fingerprint mismatch",
                            delay=2.5,
                            output=False)
        kex_status = KEX_STATUS_VERIFIED

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

    return kex_status