コード例 #1
0
def add_complete_message_to_message_list(
        timestamp: bytes, onion_pub_key: bytes, group_msg_id: bytes,
        packet: 'Packet', message_list: List[MsgTuple],
        window: Union['TxWindow', 'RxWindow']) -> bytes:
    """Add complete log file message to `message_list`."""
    whisper_byte, header, message = separate_headers(
        packet.assemble_message_packet(),
        [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])

    whisper = bytes_to_bool(whisper_byte)

    if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT:
        message_list.append((bytes_to_timestamp(timestamp), message.decode(),
                             onion_pub_key, packet.origin, whisper, False))

    elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP:
        purp_group_id, message = separate_header(message, GROUP_ID_LENGTH)
        if window.group is not None and purp_group_id != window.group.group_id:
            return group_msg_id

        purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH)
        if packet.origin == ORIGIN_USER_HEADER:
            if purp_msg_id == group_msg_id:
                return group_msg_id
            group_msg_id = purp_msg_id

        message_list.append((bytes_to_timestamp(timestamp), message.decode(),
                             onion_pub_key, packet.origin, whisper, False))

    return group_msg_id
コード例 #2
0
ファイル: tcb.py プロジェクト: tannercollin/tfc
def src_incoming(queues:   'QueueDict',
                 gateway:  'Gateway',
                 unittest: bool = False
                 ) -> None:
    """\
    Redirect messages received from Source Computer to appropriate queues.
    """
    packets_from_sc   = queues[GATEWAY_QUEUE]
    packets_to_dc     = queues[DST_MESSAGE_QUEUE]
    commands_to_dc    = queues[DST_COMMAND_QUEUE]
    messages_to_flask = queues[M_TO_FLASK_QUEUE]
    files_to_flask    = queues[F_TO_FLASK_QUEUE]
    commands_to_relay = queues[SRC_TO_RELAY_QUEUE]

    while True:
        with ignored(EOFError, KeyboardInterrupt):
            while packets_from_sc.qsize() == 0:
                time.sleep(0.01)

            ts, packet = packets_from_sc.get()  # type: datetime, bytes
            ts_bytes   = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4]))

            try:
                packet = gateway.detect_errors(packet)
            except FunctionReturn:
                continue

            header, packet = separate_header(packet, DATAGRAM_HEADER_LENGTH)

            if header == UNENCRYPTED_DATAGRAM_HEADER:
                commands_to_relay.put(packet)

            elif header in [COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER]:
                commands_to_dc.put(header + ts_bytes + packet)
                p_type = 'Command  ' if header == COMMAND_DATAGRAM_HEADER else 'Local key'
                rp_print(f"{p_type} to local Receiver", ts)

            elif header in [MESSAGE_DATAGRAM_HEADER, PUBLIC_KEY_DATAGRAM_HEADER]:
                onion_pub_key, payload = separate_header(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
                packet_str             = header.decode() + b85encode(payload)
                queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header)
                if header == MESSAGE_DATAGRAM_HEADER:
                    packets_to_dc.put(header + ts_bytes + onion_pub_key + ORIGIN_USER_HEADER + payload)

            elif header == FILE_DATAGRAM_HEADER:
                no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH)
                no_contacts            = bytes_to_int(no_contacts_b)
                ser_accounts, file_ct  = separate_header(payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH)
                pub_keys               = split_byte_string(ser_accounts, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH)
                for onion_pub_key in pub_keys:
                    queue_to_flask(file_ct, onion_pub_key, files_to_flask, ts, header)

            elif header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER,
                            GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER,
                            GROUP_MSG_EXIT_GROUP_HEADER]:
                process_group_management_message(ts, packet, header, messages_to_flask)

            if unittest:
                break
コード例 #3
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
コード例 #4
0
def process_file_datagram(ts: 'datetime', packet: bytes, header: bytes,
                          queues: 'QueueDict') -> None:
    """Process file datagram."""
    files_to_flask = queues[F_TO_FLASK_QUEUE]
    no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH)
    no_contacts = bytes_to_int(no_contacts_b)
    ser_accounts, file_ct = separate_header(
        payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH)
    pub_keys = split_byte_string(ser_accounts,
                                 item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH)

    for onion_pub_key in pub_keys:
        queue_to_flask(file_ct, onion_pub_key, files_to_flask, ts, header)
コード例 #5
0
def process_group_management_message(
        ts: 'datetime', packet: bytes, header: bytes,
        messages_to_flask: 'Queue[Tuple[Union[bytes, str], bytes]]') -> None:
    """Parse and display group management message."""
    header_str = header.decode()
    group_id, packet = separate_header(packet, GROUP_ID_LENGTH)

    if header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER]:
        pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
        for onion_pub_key in pub_keys:
            others = [k for k in pub_keys if k != onion_pub_key]
            packet_str = header_str + b85encode(group_id + b''.join(others))
            queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts,
                           header)

    elif header in [GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER]:
        first_list_len_b, packet = separate_header(packet,
                                                   ENCODED_INTEGER_LENGTH)
        first_list_length = bytes_to_int(first_list_len_b)
        pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
        before_adding = remaining = pub_keys[:first_list_length]
        new_in_group = removable = pub_keys[first_list_length:]

        if header == GROUP_MSG_MEMBER_ADD_HEADER:

            packet_str = GROUP_MSG_MEMBER_ADD_HEADER.decode() + b85encode(
                group_id + b''.join(new_in_group))
            for onion_pub_key in before_adding:
                queue_to_flask(packet_str, onion_pub_key, messages_to_flask,
                               ts, header)

            for onion_pub_key in new_in_group:
                other_new = [k for k in new_in_group if k != onion_pub_key]
                packet_str = (
                    GROUP_MSG_INVITE_HEADER.decode() +
                    b85encode(group_id + b''.join(other_new + before_adding)))
                queue_to_flask(packet_str, onion_pub_key, messages_to_flask,
                               ts, header)

        elif header == GROUP_MSG_MEMBER_REM_HEADER:
            packet_str = header_str + b85encode(group_id + b''.join(removable))
            for onion_pub_key in remaining:
                queue_to_flask(packet_str, onion_pub_key, messages_to_flask,
                               ts, header)

    elif header == GROUP_MSG_EXIT_GROUP_HEADER:
        pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
        packet_str = header_str + b85encode(group_id)
        for onion_pub_key in pub_keys:
            queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts,
                           header)
コード例 #6
0
ファイル: commands.py プロジェクト: purplesparkle/tfc
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():
        status, specifier, w_type, w_name = change_setting_for_all_contacts(
            attr, file_cmd, b_value, contact_list, group_list)

    else:
        status, specifier, w_type, w_name = change_setting_for_one_contact(
            attr, file_cmd, b_value, win_uid, window_list, contact_list, group_list)

    message = f"{desc} {status} {action} for {specifier}{w_type}{w_name}"
    cmd_win = window_list.get_command_window()
    cmd_win.add_new(ts, message, output=True)
コード例 #7
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 FunctionReturn("File key message from the user.", output=False)

    try:
        decoded = base64.b85decode(assembled)
    except ValueError:
        raise FunctionReturn("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 FunctionReturn("Error: Received an invalid file key message.")

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

    return nick
コード例 #8
0
def ch_nick(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
            contact_list: 'ContactList') -> None:
    """Change nickname of contact."""
    onion_pub_key, nick_bytes = separate_header(
        cmd_data, header_length=ONION_SERVICE_PUBLIC_KEY_LENGTH)
    nick = nick_bytes.decode()
    short_addr = pub_key_to_short_address(onion_pub_key)

    try:
        contact = contact_list.get_contact_by_pub_key(onion_pub_key)
    except StopIteration:
        raise FunctionReturn(
            f"Error: Receiver has no contact '{short_addr}' to rename.")

    contact.nick = nick
    contact_list.store_contacts()

    window = window_list.get_window(onion_pub_key)
    window.name = nick
    window.handle_dict[onion_pub_key] = nick

    if window.type == WIN_TYPE_CONTACT:
        window.redraw()

    cmd_win = window_list.get_local_window()
    cmd_win.add_new(ts, f"Changed {short_addr} nick to '{nick}'.", output=True)
コード例 #9
0
def log_command(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                contact_list: 'ContactList', group_list: 'GroupList',
                settings: 'Settings', master_key: 'MasterKey') -> None:
    """Display or export log file for 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.
    """
    export = ts is not None
    ser_no_msg, uid = separate_header(cmd_data, ENCODED_INTEGER_LENGTH)
    no_messages = bytes_to_int(ser_no_msg)
    window = window_list.get_window(uid)

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

    if export:
        local_win = window_list.get_local_window()
        local_win.add_new(
            ts,
            f"Exported log file of {window.type} '{window.name}'.",
            output=True)
コード例 #10
0
def g_msg_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
    """Show group management messages according to contact list state.

    This process keeps track of existing contacts for whom there's a
    `client` process. When a group management message from a contact
    is received, existing contacts are displayed under "known contacts",
    and non-existing contacts are displayed under "unknown contacts".
    """
    existing_contacts = []  # type: List[bytes]
    group_management_queue = queues[GROUP_MGMT_QUEUE]

    while True:
        with ignored(EOFError, KeyboardInterrupt):
            while queues[GROUP_MSG_QUEUE].qsize() == 0:
                time.sleep(0.01)

            header, payload, trunc_addr = queues[GROUP_MSG_QUEUE].get()
            group_id, data = separate_header(payload, GROUP_ID_LENGTH)

            if len(group_id) != GROUP_ID_LENGTH:
                continue
            group_id_hr = b58encode(group_id)

            existing_contacts = update_list_of_existing_contacts(
                group_management_queue, existing_contacts)

            process_group_management_message(data, existing_contacts,
                                             group_id_hr, header, trunc_addr)

            if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0:
                break
コード例 #11
0
def process_file_datagram(
    ts: 'datetime',
    packet: bytes,
    header: bytes,
    buf_key: bytes,
) -> None:
    """Process file datagram."""
    no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH)
    no_contacts = bytes_to_int(no_contacts_b)
    ser_accounts, file_ct = separate_header(
        payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH)
    pub_keys = split_byte_string(ser_accounts,
                                 item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH)

    for onion_pub_key in pub_keys:
        buffer_to_flask(file_ct, onion_pub_key, ts, header, buf_key, file=True)
コード例 #12
0
ファイル: commands.py プロジェクト: tannercollin/tfc
def process_command(command:  bytes,
                    gateway:  'Gateway',
                    queues:   'QueueDict'
                    ) -> None:
    """Select function for received Relay Program command."""
    header, command = separate_header(command, UNENCRYPTED_COMMAND_HEADER_LENGTH)

    #             Keyword                            Function to run    (       Parameters        )
    #             ---------------------------------------------------------------------------------
    function_d = {UNENCRYPTED_SCREEN_CLEAR:         (clear_windows,               gateway,        ),
                  UNENCRYPTED_SCREEN_RESET:         (reset_windows,               gateway,        ),
                  UNENCRYPTED_EXIT_COMMAND:         (exit_tfc,                    gateway,  queues),
                  UNENCRYPTED_WIPE_COMMAND:         (wipe,                        gateway,  queues),
                  UNENCRYPTED_EC_RATIO:             (change_ec_ratio,    command, gateway,        ),
                  UNENCRYPTED_BAUDRATE:             (change_baudrate,    command, gateway,        ),
                  UNENCRYPTED_MANAGE_CONTACT_REQ:   (manage_contact_req, command,           queues),
                  UNENCRYPTED_ADD_NEW_CONTACT:      (add_contact,        command, False,    queues),
                  UNENCRYPTED_ADD_EXISTING_CONTACT: (add_contact,        command, True,     queues),
                  UNENCRYPTED_REM_CONTACT:          (remove_contact,     command,           queues),
                  UNENCRYPTED_ONION_SERVICE_DATA:   (add_onion_data,     command,           queues)
                  }  # type: Dict[bytes, Any]

    if header not in function_d:
        raise FunctionReturn("Error: Received an invalid command.")

    from_dict  = function_d[header]
    func       = from_dict[0]
    parameters = from_dict[1:]
    func(*parameters)
コード例 #13
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)
コード例 #14
0
def compare_pub_keys(command: bytes, queues: 'QueueDict') -> None:
    """\
    Compare incorrectly typed public key to what's available on Relay
    Program.
    """
    account, incorrect_pub_key = separate_header(command, ONION_SERVICE_PUBLIC_KEY_LENGTH)
    queues[PUB_KEY_CHECK_QUEUE].put((account, incorrect_pub_key))
コード例 #15
0
def process_group_message(assembled: bytes, ts: 'datetime',
                          onion_pub_key: bytes, origin: bytes, whisper: bool,
                          group_list: 'GroupList',
                          window_list: 'WindowList') -> bool:
    """Process a group message."""
    group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH)
    if not group_list.has_group_id(group_id):
        raise FunctionReturn("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 FunctionReturn("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 FunctionReturn("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 group.log_messages
コード例 #16
0
def process_group_management_message(
    ts: 'datetime',
    packet: bytes,
    header: bytes,
    buf_key: bytes,
) -> None:
    """Parse and display group management message."""
    header_str = header.decode()
    group_id, packet = separate_header(packet, GROUP_ID_LENGTH)

    if header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER]:
        pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
        for onion_pub_key in pub_keys:
            others = [k for k in pub_keys if k != onion_pub_key]
            packet_str = header_str + b85encode(group_id + b''.join(others))
            buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key)

    elif header in [GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER]:
        first_list_len_b, packet = separate_header(packet,
                                                   ENCODED_INTEGER_LENGTH)
        first_list_length = bytes_to_int(first_list_len_b)
        pub_keys = split_byte_string(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH)
        before_adding = remaining = pub_keys[:first_list_length]
        new_in_group = removable = pub_keys[first_list_length:]

        if header == GROUP_MSG_MEMBER_ADD_HEADER:

            process_add_or_group_remove_member(ts, header, buf_key, header_str,
                                               group_id, before_adding,
                                               new_in_group)

            for onion_pub_key in new_in_group:
                other_new = [k for k in new_in_group if k != onion_pub_key]
                packet_str = (
                    GROUP_MSG_INVITE_HEADER.decode() +
                    b85encode(group_id + b''.join(other_new + before_adding)))
                buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key)

        elif header == GROUP_MSG_MEMBER_REM_HEADER:
            process_add_or_group_remove_member(ts, header, buf_key, header_str,
                                               group_id, remaining, removable)

    elif header == GROUP_MSG_EXIT_GROUP_HEADER:
        process_group_exit_header(ts, packet, header, buf_key, header_str,
                                  group_id)
コード例 #17
0
ファイル: packet.py プロジェクト: tannercollin/tfc
def decrypt_assembly_packet(packet:        bytes,          # Assembly packet ciphertext
                            onion_pub_key: bytes,          # Onion Service pubkey of associated contact
                            origin:        bytes,          # Direction of packet
                            window_list:   'WindowList',   # WindowList object
                            contact_list:  'ContactList',  # ContactList object
                            key_list:      'KeyList'       # Keylist object
                            ) -> bytes:                    # Decrypted assembly packet
    """Decrypt assembly packet from contact/local Transmitter."""
    ct_harac, ct_assemby_packet = separate_header(packet, header_length=HARAC_CT_LENGTH)
    local_window                = window_list.get_local_window()
    command                     = onion_pub_key == LOCAL_PUBKEY

    p_type    = "command" if command                                      else "packet"
    direction = "from"    if command or (origin == ORIGIN_CONTACT_HEADER) else "sent to"
    nick      = contact_list.get_contact_by_pub_key(onion_pub_key).nick

    # Load keys
    keyset  = key_list.get_keyset(onion_pub_key)
    key_dir = TX if origin == ORIGIN_USER_HEADER else RX

    header_key  = getattr(keyset, f'{key_dir}_hk')  # type: bytes
    message_key = getattr(keyset, f'{key_dir}_mk')  # type: bytes

    if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [header_key, message_key]):
        raise FunctionReturn("Warning! Loaded zero-key for packet decryption.")

    # Decrypt hash ratchet counter
    try:
        harac_bytes = auth_and_decrypt(ct_harac, header_key)
    except nacl.exceptions.CryptoError:
        raise FunctionReturn(
            f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=local_window)

    # Catch up with hash ratchet offset
    purp_harac   = bytes_to_int(harac_bytes)
    stored_harac = getattr(keyset, f'{key_dir}_harac')
    offset       = purp_harac - stored_harac
    if offset < 0:
        raise FunctionReturn(
            f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=local_window)

    process_offset(offset, origin, direction, nick, local_window)
    for harac in range(stored_harac, stored_harac + offset):
        message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH)

    # Decrypt packet
    try:
        assembly_packet = auth_and_decrypt(ct_assemby_packet, message_key)
    except nacl.exceptions.CryptoError:
        raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=local_window)

    # Update message key and harac
    keyset.update_mk(key_dir,
                     blake2b(message_key + int_to_bytes(stored_harac + offset), digest_size=SYMMETRIC_KEY_LENGTH),
                     offset + 1)

    return assembly_packet
コード例 #18
0
def process_message_datagram(ts: 'datetime', packet: bytes, header: bytes,
                             buf_key: bytes, queues: 'QueueDict') -> None:
    """Process message or public key datagram."""
    packets_to_dst = queues[DST_MESSAGE_QUEUE]

    onion_pub_key, payload = separate_header(packet,
                                             ONION_SERVICE_PUBLIC_KEY_LENGTH)
    packet_str = header.decode() + b85encode(payload)
    ts_bytes = int_to_bytes(int(ts.strftime("%Y%m%d%H%M%S%f")[:-4]))

    buffer_to_flask(packet_str, onion_pub_key, ts, header, buf_key)

    if header == MESSAGE_DATAGRAM_HEADER:
        packets_to_dst.put(header + ts_bytes + onion_pub_key +
                           ORIGIN_USER_HEADER + payload)
コード例 #19
0
def group_add(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
              contact_list: 'ContactList', group_list: 'GroupList',
              settings: 'Settings') -> None:
    """Add member(s) to 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 SoftError(
            f"Error: No group with ID '{b58encode(group_id)}' found.")

    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_accounts = set(pub_keys & purp_pub_keys)
    new_in_group_set = set(ok_accounts - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    already_in_g = list(purp_pub_keys & before_adding)
    rejected = list(purp_pub_keys - pub_keys)
    new_in_group = list(new_in_group_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."
        )

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

    window = window_list.get_window(group.group_id)
    window.add_contacts(new_in_group)
    window.create_handle_dict()

    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)

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Added members to group {group_name}.")
コード例 #20
0
def src_incoming(queues: 'QueueDict',
                 gateway: 'Gateway',
                 unit_test: bool = False) -> None:
    """\
    Redirect datagrams received from Source Computer to appropriate queues.
    """
    commands_to_relay = queues[SRC_TO_RELAY_QUEUE]
    buf_key_queue = queues[TX_BUF_KEY_QUEUE]

    buf_key = None

    while True:
        with ignored(EOFError, KeyboardInterrupt, SoftError):

            if buf_key is None and buf_key_queue.qsize() > 0:
                buf_key = buf_key_queue.get()

            ts, packet = load_packet_from_queue(queues, gateway)
            header, packet = separate_header(packet, DATAGRAM_HEADER_LENGTH)

            if header == UNENCRYPTED_DATAGRAM_HEADER:
                commands_to_relay.put(packet)

            elif header in [
                    COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER
            ]:
                process_command_datagram(ts, packet, header, queues)

            elif header in [
                    MESSAGE_DATAGRAM_HEADER, PUBLIC_KEY_DATAGRAM_HEADER
            ] and buf_key is not None:
                process_message_datagram(ts, packet, header, buf_key, queues)

            elif header == FILE_DATAGRAM_HEADER and buf_key is not None:
                process_file_datagram(ts, packet, header, buf_key)

            elif header in [
                    GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER,
                    GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER,
                    GROUP_MSG_EXIT_GROUP_HEADER
            ] and buf_key is not None:
                process_group_management_message(ts, packet, header, buf_key)

            if unit_test:
                break
コード例 #21
0
def group_create(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList',
                 settings: 'Settings') -> None:
    """Create a new group."""
    group_id, variable_len_data = separate_header(cmd_data, GROUP_ID_LENGTH)
    group_name_bytes, ser_members = variable_len_data.split(US_BYTE, 1)
    group_name = group_name_bytes.decode()

    purp_pub_keys = set(
        split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH))
    pub_keys = set(contact_list.get_list_of_pub_keys())
    accepted = list(purp_pub_keys & pub_keys)
    rejected = list(purp_pub_keys - pub_keys)

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

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

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

    group = group_list.get_group(group_name)
    window = window_list.get_window(group.group_id)
    window.window_contacts = accepted_contacts
    window.message_log = []
    window.unread_messages = 0
    window.create_handle_dict()

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

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Created new group {group_name}.")
コード例 #22
0
def check_packet_fate(entries_to_keep: List[bytes], packet: 'Packet',
                      removed: bool, selector: bytes) -> bool:
    """Check whether the packet should be kept."""
    _, header, message = separate_headers(
        packet.assemble_message_packet(),
        [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])

    if header == PRIVATE_MESSAGE_HEADER:
        entries_to_keep.extend(packet.log_ct_list)
        packet.clear_assembly_packets()

    elif header == GROUP_MESSAGE_HEADER:
        group_id, _ = separate_header(message, GROUP_ID_LENGTH)
        if group_id == selector:
            removed = True
        else:
            entries_to_keep.extend(packet.log_ct_list)
            packet.clear_assembly_packets()

    return removed
コード例 #23
0
ファイル: commands.py プロジェクト: tannercollin/tfc
def log_command(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                contact_list: 'ContactList', group_list: 'GroupList',
                settings: 'Settings', master_key: 'MasterKey') -> None:
    """Display or export log file for the active window."""
    export = ts is not None
    ser_no_msg, uid = separate_header(cmd_data, ENCODED_INTEGER_LENGTH)
    no_messages = bytes_to_int(ser_no_msg)
    window = window_list.get_window(uid)
    access_logs(window,
                contact_list,
                group_list,
                settings,
                master_key,
                msg_to_load=no_messages,
                export=export)

    if export:
        local_win = window_list.get_local_window()
        local_win.add_new(
            ts,
            f"Exported log file of {window.type} '{window.name}'.",
            output=True)
コード例 #24
0
def process_file_key_message(assembled: bytes, onion_pub_key: bytes,
                             origin: bytes, contact_list: 'ContactList',
                             file_keys: Dict[bytes, bytes]) -> str:
    """Process received file key delivery message."""
    if origin == ORIGIN_USER_HEADER:
        raise FunctionReturn("File key message from the user.", output=False)

    try:
        decoded = base64.b85decode(assembled)
    except ValueError:
        raise FunctionReturn("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 FunctionReturn("Error: Received an invalid file key message.")

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

    return nick
コード例 #25
0
ファイル: crypto.py プロジェクト: gtog/tfc
def auth_and_decrypt(
    nonce_ct_tag: bytes,  # Nonce + ciphertext + tag
    key: bytes,  # 32-byte symmetric key
    database:
    str = '',  # When provided, gracefully exists TFC when the tag is invalid
    ad: bytes = b''  # Associated data
) -> bytes:  # Plaintext
    """Authenticate and decrypt XChaCha20-Poly1305 ciphertext.

    The Poly1305 tag is checked using constant time `sodium_memcmp`:
        https://download.libsodium.org/doc/helpers#constant-time-test-for-equality

    When TFC decrypts ciphertext from an untrusted source (i.e., a
    contact), no `database` parameter is provided. In such situation, if
    the tag of the untrusted ciphertext is invalid, TFC discards the
    ciphertext and recovers appropriately.

    When TFC decrypts ciphertext from a trusted source (i.e., a
    database), the `database` parameter is provided, so the function
    knows which database is in question. In case the authentication
    fails due to invalid tag, the data is assumed to be either tampered
    or corrupted. TFC will in such case gracefully exit to avoid
    processing the unsafe data and warn the user in which database the
    issue was detected.
    """
    if len(key) != SYMMETRIC_KEY_LENGTH:
        raise CriticalError("Invalid key length.")

    nonce, ct_tag = separate_header(nonce_ct_tag, XCHACHA20_NONCE_LENGTH)

    try:
        plaintext = nacl.bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(
            ct_tag, ad, nonce, key)  # type: bytes
        return plaintext
    except nacl.exceptions.CryptoError:
        if database:
            raise CriticalError(
                f"Authentication of data in database '{database}' failed.")
        raise
コード例 #26
0
def get_data_loop(onion_addr: str, url_token: str, short_addr: str,
                  onion_pub_key: bytes, queues: 'QueueDict',
                  session: 'Session', gateway: 'Gateway') -> None:
    """Load TFC data from contact's Onion Service using valid URL token."""
    while True:
        try:
            check_for_files(url_token, onion_pub_key, onion_addr, short_addr,
                            session, queues)

            try:
                r = session.get(
                    f'http://{onion_addr}.onion/{url_token}/messages',
                    stream=True)
            except requests.exceptions.RequestException:
                return None

            for line in r.iter_lines(
            ):  # Iterate over newline-separated datagrams

                if not line:
                    continue

                try:
                    header, payload = separate_header(
                        line, DATAGRAM_HEADER_LENGTH)  # type: bytes, bytes
                    payload_bytes = base64.b85decode(payload)
                except (UnicodeError, ValueError):
                    continue

                ts = datetime.now()
                ts_bytes = int_to_bytes(int(
                    ts.strftime('%Y%m%d%H%M%S%f')[:-4]))

                process_received_packet(ts, ts_bytes, header, payload_bytes,
                                        onion_pub_key, short_addr, queues,
                                        gateway)

        except requests.exceptions.RequestException:
            break
コード例 #27
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 SoftError(
            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)

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Removed members from group {group_name}.")
コード例 #28
0
    def _load_groups(self) -> None:
        """Load groups from the encrypted database.

        The function first reads, authenticates and decrypts the group
        database data. Next, it slices and decodes the header values
        that help the function to properly de-serialize the database
        content. The function then removes dummy groups based on header
        data. Next, the function updates the group database settings if
        necessary. It then splits group data based on header data into
        blocks, which are further sliced, and processed if necessary,
        to obtain data required to create Group objects. Finally, if
        needed, the function will update the group database content.
        """
        pt_bytes = self.database.load_database()

        # Slice and decode headers
        group_db_headers, pt_bytes = separate_header(pt_bytes,
                                                     GROUP_DB_HEADER_LENGTH)

        padding_for_group_db, padding_for_members, number_of_groups, members_in_largest_group \
            = list(map(bytes_to_int, split_byte_string(group_db_headers, ENCODED_INTEGER_LENGTH)))

        # Slice dummy groups
        bytes_per_group = GROUP_STATIC_LENGTH + padding_for_members * ONION_SERVICE_PUBLIC_KEY_LENGTH
        dummy_data_len = (padding_for_group_db -
                          number_of_groups) * bytes_per_group
        group_data = pt_bytes[:-dummy_data_len]

        update_db = self._check_db_settings(number_of_groups,
                                            members_in_largest_group)
        blocks = split_byte_string(group_data, item_len=bytes_per_group)

        all_pub_keys = self.contact_list.get_list_of_pub_keys()
        dummy_pub_key = onion_address_to_pub_key(DUMMY_MEMBER)

        # Deserialize group objects
        for block in blocks:
            if len(block) != bytes_per_group:
                raise CriticalError("Invalid data in group database.")

            name_bytes, group_id, log_messages_byte, notification_byte, ser_pub_keys \
                = separate_headers(block, [PADDED_UTF32_STR_LENGTH, GROUP_ID_LENGTH] + 2*[ENCODED_BOOLEAN_LENGTH])

            pub_key_list = split_byte_string(
                ser_pub_keys, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH)
            group_pub_keys = [k for k in pub_key_list if k != dummy_pub_key]
            group_members = [
                self.contact_list.get_contact_by_pub_key(k)
                for k in group_pub_keys if k in all_pub_keys
            ]

            self.groups.append(
                Group(name=bytes_to_str(name_bytes),
                      group_id=group_id,
                      log_messages=bytes_to_bool(log_messages_byte),
                      notifications=bytes_to_bool(notification_byte),
                      members=group_members,
                      settings=self.settings,
                      store_groups=self.store_groups))

            update_db |= set(all_pub_keys) > set(group_pub_keys)

        if update_db:
            self.store_groups()
コード例 #29
0
def decrypt_assembly_packet(
    packet: bytes,  # Assembly packet ciphertext
    onion_pub_key: bytes,  # Onion Service pubkey of associated contact
    origin: bytes,  # Direction of packet
    window_list: 'WindowList',  # WindowList object
    contact_list: 'ContactList',  # ContactList object
    key_list: 'KeyList'  # Keylist object
) -> bytes:  # Decrypted assembly packet
    """Decrypt assembly packet from contact/local Transmitter.

    This function authenticates and decrypts incoming message and
    command datagrams. This function does not authenticate/decrypt
    incoming file and/or local key datagrams.

    While all message datagrams have been implicitly assumed to have
    originated from some contact until this point, to prevent the
    possibility of existential forgeries, the origin of the message will
    be validated at this point with the cryptographic Poly1305-tag.

    As per the cryptographic doom principle, the message won't be even
    decrypted unless the Poly1305 tag of the ciphertext is valid.

    This function also authenticates packets that handle control flow of
    the Receiver program. Like messages, command datagrams have been
    implicitly assumed to be commands until this point. However, unless
    the Poly1305-tag of the purported command is found to be valid with
    the forward secret local key, it will not be even decrypted, let
    alone processed.
    """
    ct_harac, ct_assembly_packet = separate_header(
        packet, header_length=HARAC_CT_LENGTH)
    cmd_win = window_list.get_command_window()
    command = onion_pub_key == LOCAL_PUBKEY

    p_type = "command" if command else "packet"
    direction = "from" if command or (origin
                                      == ORIGIN_CONTACT_HEADER) else "sent to"
    nick = contact_list.get_nick_by_pub_key(onion_pub_key)

    # Load keys
    keyset = key_list.get_keyset(onion_pub_key)
    key_dir = TX if origin == ORIGIN_USER_HEADER else RX

    header_key = getattr(keyset, f'{key_dir}_hk')  # type: bytes
    message_key = getattr(keyset, f'{key_dir}_mk')  # type: bytes

    if any(k == bytes(SYMMETRIC_KEY_LENGTH)
           for k in [header_key, message_key]):
        raise SoftError("Warning! Loaded zero-key for packet decryption.")

    # Decrypt hash ratchet counter
    try:
        harac_bytes = auth_and_decrypt(ct_harac, header_key)
    except nacl.exceptions.CryptoError:
        raise SoftError(
            f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.",
            window=cmd_win)

    # Catch up with hash ratchet offset
    purp_harac = bytes_to_int(harac_bytes)
    stored_harac = getattr(keyset, f'{key_dir}_harac')
    offset = purp_harac - stored_harac
    if offset < 0:
        raise SoftError(
            f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.",
            window=cmd_win)

    process_offset(offset, origin, direction, nick, cmd_win)
    for harac in range(stored_harac, stored_harac + offset):
        message_key = blake2b(message_key + int_to_bytes(harac),
                              digest_size=SYMMETRIC_KEY_LENGTH)

    # Decrypt packet
    try:
        assembly_packet = auth_and_decrypt(ct_assembly_packet, message_key)
    except nacl.exceptions.CryptoError:
        raise SoftError(
            f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.",
            window=cmd_win)

    # Update message key and harac
    new_key = blake2b(message_key + int_to_bytes(stored_harac + offset),
                      digest_size=SYMMETRIC_KEY_LENGTH)
    keyset.update_mk(key_dir, new_key, offset + 1)

    return assembly_packet
コード例 #30
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)