Example #1
0
    def test_disabled_gui_uses_cli(self):
        # Setup
        self.settings.disable_gui_dialog = True
        builtins.input                   = lambda _: '/bin/mv'

        # Test
        self.assertEqual(ask_path_gui('prompt_msg', self.settings, get_file=True), '/bin/mv')
Example #2
0
    def test_tcl_error_falls_back_to_cli(self):
        # Setup
        builtins.input             = lambda _: '/bin/mv'
        filedialog.askopenfilename = lambda title: (_ for _ in ()).throw(_tkinter.TclError)

        # Test
        self.assertEqual(ask_path_gui('prompt_msg', self.settings, get_file=True), '/bin/mv')
Example #3
0
def store_keys_on_removable_drive(
        ct_tag: bytes,  # Encrypted PSK
        salt: bytes,  # Salt for PSK decryption key derivation
        nick: str,  # Contact's nickname
        onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
        onion_service: 'OnionService',  # OnionService object
        settings: 'Settings',  # Settings object
) -> None:
    """Store keys for contact on a removable media."""
    trunc_addr = pub_key_to_short_address(onion_pub_key)
    while True:
        store_d = ask_path_gui(f"Select removable media for {nick}", settings)
        f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"

        try:
            with open(f_name, "wb+") as f:
                f.write(salt + ct_tag)
                f.flush()
                os.fsync(f.fileno())
            break
        except PermissionError:
            m_print(
                "Error: Did not have permission to write to the directory.",
                delay=0.5)
            continue
Example #4
0
def new_psk(account: str, user: str, nick: str, contact_list: 'ContactList',
            settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
    """Generate new pre-shared key for manual key delivery.

    :param account:      The contact's account name (e.g. [email protected])
    :param user:         The user's account name (e.g. [email protected])
    :param nick:         Nick of contact
    :param contact_list: Contact list object
    :param settings:     Settings object
    :param queues:       Dictionary of multiprocessing queues
    :return:             None
    """
    try:
        tx_key = keygen()
        tx_hek = keygen()
        salt = keygen()
        password = MasterKey.new_password("password for PSK")

        phase("Deriving key encryption key", head=2)
        kek, _ = argon2_kdf(password,
                            salt,
                            rounds=16,
                            memory=128000,
                            parallelism=1)
        phase('Done')

        ct_tag = encrypt_and_sign(tx_key + tx_hek, key=kek)
        store_d = ask_path_gui(f"Select removable media for {nick}", settings)
        f_name = f"{store_d}/{user}.psk - Give to {account}"

        try:
            with open(f_name, 'wb+') as f:
                f.write(salt + ct_tag)
        except PermissionError:
            raise FunctionReturn(
                "Error: Did not have permission to write to directory.")

        packet = KEY_EX_PSK_TX_HEADER \
                 + tx_key \
                 + tx_hek \
                 + account.encode() + US_BYTE +  nick.encode()

        queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])

        contact_list.add_contact(account, user, nick, bytes(32), bytes(32),
                                 settings.log_msg_by_default,
                                 settings.store_file_default,
                                 settings.n_m_notify_privacy)

        queues[KEY_MANAGEMENT_QUEUE].put(
            ('ADD', account, tx_key, bytes(32), tx_hek, bytes(32)))

        box_print([f"Successfully added {nick}."], head=1)
        clear_screen(delay=1)

    except KeyboardInterrupt:
        raise FunctionReturn("PSK generation aborted.")
Example #5
0
File: packet.py Project: gtog/tfc
def queue_file(window: 'TxWindow', settings: 'Settings',
               queues: 'QueueDict') -> None:
    """Ask file path and load file data.

    In TFC there are two ways to send a file.

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

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

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

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

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

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

    queue_assembly_packets(assembly_packets,
                           FILE,
                           settings,
                           queues,
                           window,
                           log_as_ph=True)
Example #6
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)
Example #7
0
def queue_file(window:   'TxWindow',
               settings: 'Settings',
               f_queue:  'Queue',
               gateway:  'Gateway') -> None:
    """Ask file path and load file data."""
    path = ask_path_gui("Select file to send...", settings, get_file=True)
    file = File(path, window, settings, gateway)

    packet_list = split_to_assembly_packets(file.plaintext, FILE)

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

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

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

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

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

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

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

    else:
        for c in window:
            for p in packet_list:
                f_queue.put((p, settings, c.rx_account, c.tx_account,
                             c.log_messages, window.uid))
Example #9
0
def export_file(settings: 'Settings', gateway: 'Gateway'):
    """Encrypt and export file to NH.

    This is a faster method of sending large files. It is used together with '/fi' import_file
    command that loads ciphertext to RxM for later decryption. Key is generated automatically
    so that bad passwords by users do not affect security of ciphertexts.

    As use of this command reveals use of TFC, it is disabled during trickle connection.
    """
    if settings.session_trickle:
        raise FunctionReturn("Command disabled during trickle connection.")

    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. No file was sent.")

    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=9))
    phase("Done")

    phase("Encrypting data")
    file_key = keygen()
    file_ct  = encrypt_and_sign(comp, key=file_key)
    phase("Done")

    phase("Exporting data")
    transmit(EXPORTED_FILE_CT_HEADER + file_ct, settings, gateway)
    phase("Done")

    box_print([f"Decryption key for file {name}:", '', b58encode(file_key)], head=1, tail=1)
Example #10
0
 def test_get_path_gui(self, _):
     self.assertEqual(ask_path_gui('select path for file:', self.settings),
                      self.path)
Example #11
0
 def test_tcl_error_falls_back_to_cli(self, *_):
     self.assertEqual(
         ask_path_gui('prompt_msg', self.settings, get_file=True),
         self.file_path)
Example #12
0
 def test_get_path_to_file_gui(self, *_):
     self.assertEqual(
         ask_path_gui('path to file:', self.settings, get_file=True),
         self.file_path)
Example #13
0
    def test_get_path_gui(self):
        # Setup
        filedialog.askdirectory = lambda title: 'test_path'

        # Test
        self.assertEqual(ask_path_gui('test message', self.settings, get_file=False), 'test_path')
Example #14
0
 def test_disabled_gui_uses_cli(self, *_):
     self.settings.disable_gui_dialog = True
     self.assertEqual(
         ask_path_gui('prompt_msg', self.settings, get_file=True),
         self.file_path)
Example #15
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 SoftError(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 SoftError("Error: No read permission for the PSK file.")

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

    salt, ct_tag = separate_header(psk_data, ARGON2_SALT_LENGTH)
    psk = decrypt_rx_psk(ct_tag, salt)
    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 SoftError("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})."
    cmd_win = window_list.get_command_window()
    cmd_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)
Example #16
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)
Example #17
0
    def test_get_path_to_file_gui(self):
        # Setup
        filedialog.askopenfilename = lambda title: 'test_path_to_file'

        # Test
        self.assertEqual(ask_path_gui('test message', self.settings, get_file=True), 'test_path_to_file')
Example #18
0
def create_pre_shared_key(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Nick of contact
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    onion_service: 'OnionService',  # OnionService object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Generate a new pre-shared key for manual key delivery.

    Pre-shared keys offer a low-tech solution against the slowly
    emerging threat of quantum computers. PSKs are less convenient and
    not usable in every scenario, but until a quantum-safe key exchange
    algorithm with reasonably short keys is standardized, TFC can't
    provide a better alternative against quantum computers.

    The generated keys are protected by a key encryption key, derived
    from a 256-bit salt and a password (that is to be shared with the
    recipient) using Argon2d key derivation function.

    The encrypted message and header keys are stored together with salt
    on a removable media. This media must be a never-before-used device
    from sealed packaging. Re-using an old device might infect Source
    Computer, and the malware could either copy sensitive data on that
    removable media, or Source Computer might start transmitting the
    sensitive data covertly over the serial interface to malware on
    Networked Computer.

    Once the key has been exported to the clean drive, contact data and
    keys are exported to the Receiver Program on Destination computer.
    The transmission is encrypted with the local key.
    """
    try:
        tx_mk = csprng()
        tx_hk = csprng()
        salt = csprng()

        password = MasterKey.new_password("password for PSK")

        phase("Deriving key encryption key", head=2)
        kek = argon2_kdf(password,
                         salt,
                         time_cost=ARGON2_PSK_TIME_COST,
                         memory_cost=ARGON2_PSK_MEMORY_COST)
        phase(DONE)

        ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek)

        while True:
            trunc_addr = pub_key_to_short_address(onion_pub_key)
            store_d = ask_path_gui(f"Select removable media for {nick}",
                                   settings)
            f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"
            try:
                with open(f_name, 'wb+') as f:
                    f.write(salt + ct_tag)
                break
            except PermissionError:
                m_print(
                    "Error: Did not have permission to write to the directory.",
                    delay=0.5)
                continue

        command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk +
                   csprng() + str_to_bytes(nick))

        queue_command(command, settings, queues)

        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH),
                                 KEX_STATUS_NO_RX_PSK,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("PSK generation aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
Example #19
0
def rxm_import(settings: 'Settings', queue_to_rxm: 'Queue') -> None:
    """Import encrypted file to RxM."""
    f_path = ask_path_gui("Select file to import...", settings, get_file=True)
    with open(f_path, 'rb') as f:
        f_data = f.read()
    queue_to_rxm.put(IMPORTED_FILE_HEADER + f_data)
Example #20
0
def nh_command(
        settings: 'Settings',
        q_to_nh: 'Queue',
        q_to_rxm: 'Queue',
        q_im_cmd: 'Queue',
        file_no: int  # stdin input file descriptor
) -> None:
    """Process NH side commands."""

    sys.stdin = os.fdopen(file_no)

    while True:
        try:
            if q_to_nh.empty():
                time.sleep(0.001)
                continue
            command = q_to_nh.get()
            header = command[:2]

            if header in [UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET]:
                # Handle race condition with RxM command notification
                time.sleep(0.1)
                if settings.local_testing_mode and settings.data_diode_sockets:
                    time.sleep(0.7)

            if header == UNENCRYPTED_SCREEN_CLEAR:
                q_im_cmd.put(command)
                clear_screen()

            if header == UNENCRYPTED_SCREEN_RESET:
                q_im_cmd.put(command)
                os.system('reset')

            if header == UNENCRYPTED_EXIT_COMMAND:
                exit()

            if header == UNENCRYPTED_EC_RATIO:
                value = eval(command[2:])

                if not isinstance(value, int) or value < 1:
                    c_print("Error: Received Invalid EC ratio value from TxM.")
                    continue

                settings.e_correction_ratio = value
                settings.store_settings()
                c_print("Error correction ratio will change on restart.",
                        head=1,
                        tail=1)

            if header == UNENCRYPTED_BAUDRATE:
                value = eval(command[2:])

                if not isinstance(value,
                                  int) or value not in serial.Serial.BAUDRATES:
                    c_print(
                        "Error: Received invalid baud rate value from TxM.")
                    continue

                settings.serial_iface_speed = value
                settings.store_settings()
                c_print("Baud rate will change on restart.", head=1, tail=1)

            if header == UNENCRYPTED_IMPORT_COMMAND:
                f_path = ask_path_gui("Select file to import...",
                                      settings,
                                      get_file=True)
                with open(f_path, 'rb') as f:
                    f_data = f.read()
                q_to_rxm.put(IMPORTED_FILE_CT_HEADER + f_data)

            if header == UNENCRYPTED_GUI_DIALOG:
                value = eval(command[2:])

                settings.disable_gui_dialog = value
                settings.store_settings()

                c_print(
                    "Changed setting disable_gui_dialog to {}.".format(value),
                    head=1,
                    tail=1)

        except (KeyboardInterrupt, EOFError, FunctionReturn):
            pass
Example #21
0
def import_psk_rx_keys(cmd_data: bytes, ts: 'datetime',
                       window_list: 'WindowList', contact_list: 'ContactList',
                       key_list: 'KeyList', settings: 'Settings') -> None:
    """Import Rx-PSK of contact."""
    account = cmd_data.decode()

    if not contact_list.has_contact(account):
        raise FunctionReturn(f"Error: Unknown account '{account}'")

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

    with open(psk_file, 'rb') as f:
        psk_data = f.read()

    if len(psk_data) != PSK_FILE_SIZE:
        raise FunctionReturn("Error: Invalid PSK data in file.")

    salt = psk_data[:ARGON2_SALT_LEN]
    ct_tag = psk_data[ARGON2_SALT_LEN:]

    while True:
        try:
            password = MasterKey.get_password("PSK password")
            phase("Deriving key decryption key", head=2)
            kdk, _ = argon2_kdf(password, salt, parallelism=1)
            psk_pt = auth_and_decrypt(ct_tag, key=kdk, soft_e=True)
            phase(DONE)
            break

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

    rx_key = psk_pt[0:32]
    rx_hek = psk_pt[32:64]

    if any(k == bytes(KEY_LENGTH) for k in [rx_key, rx_hek]):
        raise FunctionReturn("Error: Received invalid keys from contact.")

    keyset = key_list.get_keyset(account)
    keyset.rx_key = rx_key
    keyset.rx_hek = rx_hek
    key_list.store_keys()

    # Pipes protects against shell injection. Source of command's parameter
    # is user's own RxM 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):
        box_print(
            f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.",
            manual_proceed=True)

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

    box_print([
        message, '', "Warning!",
        "Physically destroy the keyfile transmission ",
        "media to ensure that no data escapes RxM!"
    ],
              head=1,
              tail=1)
Example #22
0
def psk_import(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
               contact_list: 'ContactList', key_list: 'KeyList',
               settings: 'Settings') -> None:
    """Import rx-PSK of contact."""
    account = cmd_data.decode()

    if not contact_list.has_contact(account):
        raise FunctionReturn(f"Unknown account {account}.")

    contact = contact_list.get_contact(account)
    pskf = ask_path_gui(f"Select PSK for {contact.nick}",
                        settings,
                        get_file=True)

    with open(pskf, 'rb') as f:
        psk_data = f.read()

    if len(
            psk_data
    ) != 136:  # Nonce (24) + Salt (32) + rx-key (32) + rx-hek (32) + tag (16)
        raise FunctionReturn("Invalid PSK data in file.")

    salt = psk_data[:32]
    ct_tag = psk_data[32:]

    while True:
        try:
            password = MasterKey.get_password("PSK password")
            phase("Deriving key decryption key", head=2)
            kdk, _ = argon2_kdf(password,
                                salt,
                                rounds=16,
                                memory=128000,
                                parallelism=1)
            psk_pt = auth_and_decrypt(ct_tag, key=kdk, soft_e=True)
            phase("Done")
            break

        except nacl.exceptions.CryptoError:
            print_on_previous_line()
            c_print("Invalid password. Try again.", head=1)
            print_on_previous_line(reps=5, delay=1.5)
        except KeyboardInterrupt:
            raise FunctionReturn("PSK import aborted.")

    rx_key = psk_pt[0:32]
    rx_hek = psk_pt[32:64]

    if rx_key == bytes(32) or rx_hek == bytes(32):
        raise FunctionReturn("Keys from contact are not valid.")

    keyset = key_list.get_keyset(account)
    keyset.rx_key = rx_key
    keyset.rx_hek = rx_hek
    key_list.store_keys()

    # Pipes protects against shell injection. Source of command
    # is trusted (user's own TxM) but it's still good practice.
    subprocess.Popen("shred -n 3 -z -u {}".format(pipes.quote(pskf)),
                     shell=True).wait()
    if os.path.isfile(pskf):
        box_print(f"Warning! Overwriting of PSK ({pskf}) failed.")
        time.sleep(3)

    local_win = window_list.get_local_window()
    local_win.print_new(ts,
                        f"Added Rx-PSK for {contact.nick} ({account})",
                        print_=False)

    box_print([
        f"Added Rx-PSK for {contact.nick}.", '', "Warning!",
        "Physically destroy the keyfile transmission ",
        "media to ensure that no data escapes RxM!"
    ],
              head=1,
              tail=1)
Example #23
0
def create_pre_shared_key(account:      str,
                          user:         str,
                          nick:         str,
                          contact_list: 'ContactList',
                          settings:     'Settings',
                          queues:       Dict[bytes, 'Queue']) -> None:
    """Generate new pre-shared key for manual key delivery.

    :param account:      The contact's account name (e.g. [email protected])
    :param user:         The user's account name (e.g. [email protected])
    :param nick:         Nick of contact
    :param contact_list: Contact list object
    :param settings:     Settings object
    :param queues:       Dictionary of multiprocessing queues
    :return:             None
    """
    try:
        tx_key   = csprng()
        tx_hek   = csprng()
        salt     = csprng()
        password = MasterKey.new_password("password for PSK")

        phase("Deriving key encryption key", head=2)
        kek, _ = argon2_kdf(password, salt, parallelism=1)
        phase(DONE)

        ct_tag = encrypt_and_sign(tx_key + tx_hek, key=kek)

        while True:
            store_d = ask_path_gui(f"Select removable media for {nick}", settings)
            f_name  = f"{store_d}/{user}.psk - Give to {account}"
            try:
                with open(f_name, 'wb+') as f:
                    f.write(salt + ct_tag)
                break
            except PermissionError:
                c_print("Error: Did not have permission to write to directory.")
                time.sleep(0.5)
                continue

        packet = KEY_EX_PSK_TX_HEADER \
                 + tx_key \
                 + tx_hek \
                 + account.encode() + US_BYTE + nick.encode()

        queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])

        contact_list.add_contact(account, user, nick,
                                 bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN),
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)

        queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, account,
                                          tx_key, csprng(),
                                          tx_hek, csprng()))

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

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