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

    No effective RAM overwriting tool currently exists, so as long as TxM/RxM
    use FDE and DDR3 memory, recovery of user data becomes impossible very fast:

        https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf
    """
    if not yes("Wipe all user data and power off systems?"):
        raise FunctionReturn("Wipe command aborted.")

    clear_screen()

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

    queue_command(WIPE_USER_DATA_HEADER, settings,
                  queues[COMMAND_PACKET_QUEUE])

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

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

    os.system('reset')
Ejemplo n.º 2
0
def change_setting(user_input: 'UserInput', contact_list: 'ContactList',
                   group_list: 'GroupList', settings: 'Settings',
                   queues: Dict[bytes, 'Queue']) -> None:
    """Change setting on TxM / RxM."""
    try:
        setting = user_input.plaintext.split()[1]
    except IndexError:
        raise FunctionReturn("Error: No setting specified.")

    if setting not in settings.key_list:
        raise FunctionReturn(f"Error: Invalid setting '{setting}'")

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

    pt_cmd = dict(serial_error_correction=UNENCRYPTED_EC_RATIO,
                  serial_baudrate=UNENCRYPTED_BAUDRATE,
                  disable_gui_dialog=UNENCRYPTED_GUI_DIALOG)

    if setting in pt_cmd:
        if settings.session_traffic_masking:
            raise FunctionReturn(
                "Error: Can't change this setting during traffic masking.")

    settings.change_setting(setting, value, contact_list, group_list)

    command = CHANGE_SETTING_HEADER + setting.encode(
    ) + US_BYTE + value.encode()
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    if setting in pt_cmd:
        packet = UNENCRYPTED_PACKET_HEADER + pt_cmd[setting] + value.encode()
        queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE])
Ejemplo n.º 3
0
def clear_screens(user_input: 'UserInput', window: 'TxWindow',
                  settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
    """Clear/reset TxM, RxM and NH screens.

    Only send unencrypted command to NH if traffic masking is disabled and
    if some related IM account can be bound to active window.

    Since reset command removes ephemeral message log on RxM, TxM decides
    the window to reset (in case e.g. previous window selection command
    packet dropped and active window state is inconsistent between TxM/RxM).
    """
    cmd = user_input.plaintext.split()[0]

    command = CLEAR_SCREEN_HEADER if cmd == CLEAR else RESET_SCREEN_HEADER + window.uid.encode(
    )
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    clear_screen()

    if not settings.session_traffic_masking and window.imc_name is not None:
        im_window = window.imc_name.encode()
        pt_cmd = UNENCRYPTED_SCREEN_CLEAR if cmd == CLEAR else UNENCRYPTED_SCREEN_RESET
        packet = UNENCRYPTED_PACKET_HEADER + pt_cmd + im_window
        queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE])

    if cmd == RESET:
        os.system('reset')
Ejemplo n.º 4
0
def import_file(settings: 'Settings', nh_queue: 'Queue') -> None:
    """\
    Send unencrypted command to NH that tells it to open
    RxM upload prompt for received (exported) file.
    """
    if settings.session_traffic_masking:
        raise FunctionReturn(
            "Error: Command is disabled during traffic masking.")

    queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_IMPORT_COMMAND,
                settings, nh_queue)
Ejemplo n.º 5
0
def export_file(settings: 'Settings', nh_queue: 'Queue') -> None:
    """Encrypt and export file to NH.

    This is a faster method to send large files. It is used together
    with file import (/fi) command that uploads ciphertext to RxM for
    RxM-side decryption. Key is generated automatically so that bad
    passwords selected by users do not affect security of ciphertexts.
    """
    if settings.session_traffic_masking:
        raise FunctionReturn(
            "Error: Command is disabled during traffic masking.")

    path = ask_path_gui("Select file to export...", settings, get_file=True)
    name = path.split('/')[-1]
    data = bytearray()
    data.extend(str_to_bytes(name))

    if not os.path.isfile(path):
        raise FunctionReturn("Error: File not found.")

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

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

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

    phase("Encrypting data")
    file_key = csprng()
    file_ct = encrypt_and_sign(comp, key=file_key)
    phase(DONE)

    phase("Exporting data")
    queue_to_nh(EXPORTED_FILE_HEADER + file_ct, settings, nh_queue)
    phase(DONE)

    print_key(f"Decryption key for file '{name}':",
              file_key,
              settings,
              no_split=True,
              file_key=True)
Ejemplo n.º 6
0
def exit_tfc(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
    """Exit TFC on TxM/RxM/NH."""
    for q in [COMMAND_PACKET_QUEUE, NH_PACKET_QUEUE]:
        while queues[q].qsize() != 0:
            queues[q].get()

    queue_command(EXIT_PROGRAM_HEADER, settings, queues[COMMAND_PACKET_QUEUE])

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

    queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND, settings,
                queues[NH_PACKET_QUEUE])
Ejemplo n.º 7
0
        def queue_delayer():
            time.sleep(0.1)
            queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE])

            time.sleep(0.1)
            queue_to_nh(
                PUBLIC_KEY_PACKET_HEADER + KEY_LENGTH * b'a' +
                b'*****@*****.**' + US_BYTE + b'*****@*****.**', settings,
                queues[NH_PACKET_QUEUE])

            time.sleep(0.1)
            queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND,
                        settings, queues[NH_PACKET_QUEUE])

            time.sleep(0.1)
            queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND,
                        settings, queues[NH_PACKET_QUEUE])

            time.sleep(0.1)
            queues[KEY_MANAGEMENT_QUEUE].put(
                (KDB_ADD_ENTRY_HEADER, LOCAL_ID, KEY_LENGTH * b'a',
                 KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a'))

            time.sleep(0.1)
            queue_message(user_input, window, settings,
                          queues[MESSAGE_PACKET_QUEUE])

            time.sleep(0.1)
            queue_message(user_input, window, settings,
                          queues[FILE_PACKET_QUEUE])

            time.sleep(0.1)
            queues[KEY_MANAGEMENT_QUEUE].put(
                (KDB_ADD_ENTRY_HEADER, '*****@*****.**', KEY_LENGTH * b'a',
                 KEY_LENGTH * b'a', KEY_LENGTH * b'a', KEY_LENGTH * b'a'))

            time.sleep(0.1)
            queue_message(user_input, window, settings,
                          queues[MESSAGE_PACKET_QUEUE])

            time.sleep(0.1)
            queue_message(user_input, window, settings,
                          queues[FILE_PACKET_QUEUE])

            time.sleep(0.1)
            queues[UNITTEST_QUEUE].put(EXIT)
Ejemplo n.º 8
0
    def test_loop(self):
        # Setup
        queues = {
            MESSAGE_PACKET_QUEUE: Queue(),
            FILE_PACKET_QUEUE: Queue(),
            COMMAND_PACKET_QUEUE: Queue(),
            NH_PACKET_QUEUE: Queue(),
            LOG_PACKET_QUEUE: Queue(),
            NOISE_PACKET_QUEUE: Queue(),
            NOISE_COMMAND_QUEUE: Queue(),
            KEY_MANAGEMENT_QUEUE: Queue(),
            WINDOW_SELECT_QUEUE: Queue(),
            EXIT_QUEUE: Queue()
        }

        settings = Settings(session_traffic_masking=True)
        gateway = Gateway()
        key_list = KeyList(nicks=['Alice', LOCAL_ID])
        window = TxWindow(log_messages=True)
        contact_list = ContactList(nicks=['Alice', LOCAL_ID])
        window.contact_list = contact_list
        window.window_contacts = [contact_list.get_contact('Alice')]
        user_input = UserInput(plaintext='test')

        queue_message(user_input, window, settings,
                      queues[MESSAGE_PACKET_QUEUE])
        queue_message(user_input, window, settings,
                      queues[MESSAGE_PACKET_QUEUE])
        queue_message(user_input, window, settings,
                      queues[MESSAGE_PACKET_QUEUE])
        queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE])
        queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE])
        queue_command(b'test', settings, queues[COMMAND_PACKET_QUEUE], window)
        queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND,
                    settings, queues[NH_PACKET_QUEUE])
        queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND,
                    settings, queues[NH_PACKET_QUEUE])

        def queue_delayer():
            time.sleep(0.1)
            queues[WINDOW_SELECT_QUEUE].put((window, True))

        # Test
        threading.Thread(target=queue_delayer).start()
        self.assertIsNone(
            sender_loop(queues, settings, gateway, key_list, unittest=True))

        threading.Thread(target=queue_delayer).start()

        self.assertIsNone(
            sender_loop(queues, settings, gateway, key_list, unittest=True))

        threading.Thread(target=queue_delayer).start()

        self.assertIsNone(
            sender_loop(queues, settings, gateway, key_list, unittest=True))

        self.assertEqual(len(gateway.packets), 8)
        self.assertEqual(queues[EXIT_QUEUE].qsize(), 2)

        # Teardown
        for key in queues:
            while not queues[key].empty():
                queues[key].get()
            time.sleep(0.1)
            queues[key].close()
Ejemplo n.º 9
0
def new_local_key(contact_list: 'ContactList',
                  settings:     'Settings',
                  queues:       Dict[bytes, 'Queue']) -> None:
    """Run Tx-side local key exchange protocol.

    Local key encrypts commands and data sent from TxM to RxM. The key is
    delivered to RxM in packet encrypted with an ephemeral symmetric key.

    The checksummed Base58 format key decryption key is typed on RxM manually.
    This prevents local key leak in following scenarios:

        1. CT is intercepted by adversary on compromised NH but no visual
           eavesdropping takes place.
        2. CT is not intercepted by adversary on NH but visual eavesdropping
           records decryption key.
        3. CT is delivered from TxM to RxM (compromised NH is bypassed) and
           visual eavesdropping records decryption key.

    Once correct key decryption key is entered on RxM, Receiver program will
    display the 1-byte confirmation code generated by Transmitter program.
    The code will be entered on TxM to confirm user has successfully delivered
    the key decryption key.

    The protocol is completed with Transmitter program sending an ACK message
    to Receiver program, that then moves to wait for public keys from contact.
    """
    try:
        if settings.session_traffic_masking and contact_list.has_local_contact:
            raise FunctionReturn("Error: Command is disabled during traffic masking.")

        clear_screen()
        c_print("Local key setup", head=1, tail=1)

        c_code = os.urandom(1)
        key    = csprng()
        hek    = csprng()
        kek    = csprng()
        packet = LOCAL_KEY_PACKET_HEADER + encrypt_and_sign(key + hek + c_code, key=kek)

        nh_bypass_msg(NH_BYPASS_START, settings)
        queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE])

        while True:
            print_key("Local key decryption key (to RxM)", kek, settings)
            purp_code = ask_confirmation_code()
            if purp_code == c_code.hex():
                break
            elif purp_code == RESEND:
                phase("Resending local key", head=2)
                queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE])
                phase(DONE)
                print_on_previous_line(reps=(9 if settings.local_testing_mode else 10))
            else:
                box_print(["Incorrect confirmation code. If RxM did not receive",
                           "encrypted local key, resend it by typing 'resend'."], head=1)
                print_on_previous_line(reps=(11 if settings.local_testing_mode else 12), delay=2)

        nh_bypass_msg(NH_BYPASS_STOP, settings)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_ID, LOCAL_ID, LOCAL_ID,
                                 bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN),
                                 False, False, False)

        # Add local contact to keyset database
        queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, LOCAL_ID,
                                          key, csprng(),
                                          hek, csprng()))

        # Notify RxM that confirmation code was successfully entered
        queue_command(LOCAL_KEY_INSTALLED_HEADER, settings, queues[COMMAND_PACKET_QUEUE])

        box_print("Successfully added a new local key.")
        clear_screen(delay=1)

    except KeyboardInterrupt:
        raise FunctionReturn("Local key setup aborted.", delay=1, head=3, tail_clear=True)
Ejemplo n.º 10
0
def start_key_exchange(account:      str,
                       user:         str,
                       nick:         str,
                       contact_list: 'ContactList',
                       settings:     'Settings',
                       queues:       Dict[bytes, 'Queue']) -> None:
    """Start X25519 key exchange with recipient.

    Variable naming:

        tx     = user's key                 rx  = contact's key
        sk     = private (secret) key       pk  = public key
        key    = message key                hek = header key
        dh_ssk = X25519 shared secret

    :param account:      The contact's account name (e.g. [email protected])
    :param user:         The user's account name (e.g. [email protected])
    :param nick:         Contact's nickname
    :param contact_list: Contact list object
    :param settings:     Settings object
    :param queues:       Dictionary of multiprocessing queues
    :return:             None
    """
    try:
        tx_sk = nacl.public.PrivateKey(csprng())
        tx_pk = bytes(tx_sk.public_key)

        while True:
            queue_to_nh(PUBLIC_KEY_PACKET_HEADER
                        + tx_pk
                        + user.encode()
                        + US_BYTE
                        + account.encode(),
                        settings, queues[NH_PACKET_QUEUE])

            rx_pk = get_b58_key(B58_PUB_KEY, settings)

            if rx_pk != RESEND.encode():
                break

        if rx_pk == bytes(KEY_LENGTH):
            # Public key is zero with negligible probability, therefore we
            # assume such key is malicious and attempts to either result in
            # zero shared key (pointless considering implementation), or to
            # DoS the key exchange as libsodium does not accept zero keys.
            box_print(["Warning!",
                       "Received a malicious public key from network.",
                       "Aborting key exchange for your safety."], tail=1)
            raise FunctionReturn("Error: Zero public key", output=False)

        dh_box = nacl.public.Box(tx_sk, nacl.public.PublicKey(rx_pk))
        dh_ssk = dh_box.shared_key()

        # Domain separate each key with key-type specific context variable
        # and with public keys that both clients know which way to place.
        tx_key = hash_chain(dh_ssk + rx_pk + b'message_key')
        rx_key = hash_chain(dh_ssk + tx_pk + b'message_key')

        tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key')
        rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key')

        # Domain separate fingerprints of public keys by using the shared
        # secret as salt. This way entities who might monitor fingerprint
        # verification channel are unable to correlate spoken values with
        # public keys that transit through a compromised IM server. This
        # protects against de-anonymization of IM accounts in cases where
        # clients connect to the compromised server via Tor. The preimage
        # resistance of hash chain protects the shared secret from leaking.
        tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint')
        rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint')

        if not verify_fingerprints(tx_fp, rx_fp):
            box_print(["Warning!",
                       "Possible man-in-the-middle attack detected.",
                       "Aborting key exchange for your safety."], tail=1)
            raise FunctionReturn("Error: Fingerprint mismatch", output=False)

        packet = KEY_EX_X25519_HEADER \
                 + tx_key + tx_hek \
                 + rx_key + rx_hek \
                 + account.encode() + US_BYTE + nick.encode()

        queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])

        contact_list.add_contact(account, user, nick,
                                 tx_fp, rx_fp,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)

        # Use random values as Rx-keys to prevent decryption if they're accidentally used.
        queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, account,
                                          tx_key, csprng(),
                                          tx_hek, csprng()))

        box_print(f"Successfully added {nick}.")
        clear_screen(delay=1)

    except KeyboardInterrupt:
        raise FunctionReturn("Key exchange aborted.", delay=1, head=2, tail_clear=True)