예제 #1
0
def deliver_contact_data(header:        bytes,        # Key type (x448, PSK)
                         nick:          str,          # Contact's nickname
                         onion_pub_key: bytes,        # Public key of contact's v3 Onion Service
                         tx_mk:         bytes,        # Message key for outgoing messages
                         rx_mk:         bytes,        # Message key for incoming messages
                         tx_hk:         bytes,        # Header key for outgoing messages
                         rx_hk:         bytes,        # Header key for incoming messages
                         queues:        'QueueDict',  # Dictionary of multiprocessing queues
                         settings:      'Settings',   # Settings object
                         ) -> None:
    """Deliver contact data to Destination Computer."""
    c_code  = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
    command = (header + onion_pub_key + tx_mk + rx_mk + tx_hk + rx_hk + str_to_bytes(nick))

    queue_command(command, settings, queues)

    while True:
        purp_code = ask_confirmation_code("Receiver")
        if purp_code == c_code.hex():
            break

        elif purp_code == "":
            phase("Resending contact data", head=2)
            queue_command(command, settings, queues)
            phase(DONE)
            print_on_previous_line(reps=5)

        else:
            m_print("Incorrect confirmation code.", head=1)
            print_on_previous_line(reps=4, delay=2)
예제 #2
0
파일: pidgin.py 프로젝트: AJMartel/tfc
def ensure_im_connection() -> None:
    """\
    Check that nh.py has connection to Pidgin
    before launching other processes.
    """
    phase("Waiting for enabled account in Pidgin", offset=1)

    while True:
        try:
            bus = dbus.SessionBus(private=True)
            obj = bus.get_object("im.pidgin.purple.PurpleService",
                                 "/im/pidgin/purple/PurpleObject")
            purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")

            while not purple.PurpleAccountsGetAllActive():
                time.sleep(0.01)
            phase('OK', done=True)

            accounts = []
            for a in purple.PurpleAccountsGetAllActive():
                accounts.append(purple.PurpleAccountGetUsername(a)[:-1])

            just_len = len(max(accounts, key=len))
            justified = ["Active accounts in Pidgin:"] + [
                "* {}".format(a.ljust(just_len)) for a in accounts
            ]
            box_print(justified, head=1, tail=1)
            return None

        except (IndexError, dbus.exceptions.DBusException):
            continue
        except (EOFError, KeyboardInterrupt):
            clear_screen()
            exit()
예제 #3
0
    def new_master_key(self) -> None:
        """Create a new master key from salt and password."""
        password = MasterKey.new_password()
        salt = csprng()
        rounds = ARGON2_ROUNDS
        memory = ARGON2_MIN_MEMORY

        phase("Deriving master key", head=2)
        while True:
            time_start = time.monotonic()
            master_key, parallellism = argon2_kdf(password,
                                                  salt,
                                                  rounds,
                                                  memory=memory,
                                                  local_test=self.local_test)
            time_final = time.monotonic() - time_start

            if time_final > 3.0:
                self.master_key = master_key
                ensure_dir(f'{DIR_USER_DATA}/')
                with open(self.file_name, 'wb+') as f:
                    f.write(salt + hash_chain(self.master_key) +
                            int_to_bytes(rounds) + int_to_bytes(memory) +
                            int_to_bytes(parallellism))
                phase(DONE)
                break
            else:
                memory *= 2
예제 #4
0
def deliver_local_key(local_key_packet: bytes,
                      kek:              bytes,
                      c_code:           bytes,
                      settings:         'Settings',
                      queues:           'QueueDict'
                      ) -> None:
    """Deliver encrypted local key to Destination Computer."""
    nc_bypass_msg(NC_BYPASS_START, settings)
    queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])

    while True:
        print_key("Local key decryption key (to Receiver)", kek, settings)
        purp_code = ask_confirmation_code("Receiver")
        if purp_code == c_code.hex():
            nc_bypass_msg(NC_BYPASS_STOP, settings)
            break
        elif purp_code == "":
            phase("Resending local key", head=2)
            queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
            phase(DONE)
            print_on_previous_line(reps=(9 if settings.local_testing_mode else 10))
        else:
            m_print(["Incorrect confirmation code. If Receiver did not receive",
                     "the encrypted local key, resend it by pressing <Enter>."], head=1)
            print_on_previous_line(reps=(9 if settings.local_testing_mode else 10), delay=2)
예제 #5
0
def deliver_onion_service_data(relay_command: bytes,
                               onion_service: 'OnionService',
                               gateway: 'Gateway') -> None:
    """Send Onion Service data to Replay Program on Networked Computer."""
    gateway.write(relay_command)
    while True:
        purp_code = ask_confirmation_code('Relay')

        if purp_code == onion_service.conf_code.hex():
            onion_service.is_delivered = True
            onion_service.new_confirmation_code()
            break

        if purp_code == '':
            phase("Resending Onion Service data", head=2)
            gateway.write(relay_command)
            phase(DONE)
            print_on_previous_line(reps=5)

        else:
            m_print([
                "Incorrect confirmation code. If Relay Program did not",
                "receive Onion Service data, resend it by pressing <Enter>."
            ],
                    head=1)
            print_on_previous_line(reps=5, delay=2)
예제 #6
0
파일: gateway.py 프로젝트: todun/tfc
    def client_establish_socket(self) -> None:
        """Initialize the transmitter (IPC client)."""
        try:
            target = RECEIVER if self.settings.software_operation == NC else RELAY
            phase(f"Connecting to {target}")
            while True:
                try:
                    if self.settings.software_operation == TX:
                        socket_number = SRC_DD_LISTEN_SOCKET if self.settings.data_diode_sockets else RP_LISTEN_SOCKET
                    else:
                        socket_number = DST_DD_LISTEN_SOCKET if self.settings.data_diode_sockets else DST_LISTEN_SOCKET

                    try:
                        self.tx_socket = multiprocessing.connection.Client((LOCALHOST, socket_number))
                    except ConnectionRefusedError:
                        time.sleep(0.1)
                        continue

                    phase(DONE)
                    break

                except socket.error:
                    time.sleep(0.1)

        except KeyboardInterrupt:
            graceful_exit()
예제 #7
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.")
예제 #8
0
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList',
                      group_list: 'GroupList', settings: 'Settings',
                      queues: 'QueueDict', master_key: 'MasterKey',
                      onion_service: 'OnionService') -> None:
    """Change the master key on Transmitter/Receiver Program."""
    try:
        if settings.traffic_masking:
            raise FunctionReturn(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

        try:
            device = user_input.plaintext.split()[1].lower()
        except IndexError:
            raise FunctionReturn(
                f"Error: No target-system ('{TX}' or '{RX}') specified.",
                head_clear=True)

        if device not in [TX, RX]:
            raise FunctionReturn(f"Error: Invalid target system '{device}'.",
                                 head_clear=True)

        if device == RX:
            queue_command(CH_MASTER_KEY, settings, queues)
            return None

        old_master_key = master_key.master_key[:]
        new_master_key = master_key.master_key = master_key.new_master_key()

        phase("Re-encrypting databases")

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_CHANGE_MASTER_KEY_HEADER, master_key))

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

        contact_list.store_contacts()
        group_list.store_groups()
        settings.store_settings()
        onion_service.store_onion_service_private_key()

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

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Password change aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #9
0
파일: commands.py 프로젝트: dimwap/tfc
def ch_master_key(ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', group_list: 'GroupList',
                  key_list: 'KeyList', settings: 'Settings',
                  master_key: 'MasterKey') -> None:
    """Prompt the user for a new master password and derive a new master key from that."""
    if not master_key.authenticate_action():
        raise SoftError("Error: Invalid password.",
                        tail_clear=True,
                        delay=1,
                        head=2)

    # Cache old master key to allow log file re-encryption.
    old_master_key = master_key.master_key[:]

    # Create new master key but do not store new master key data into any database.
    new_master_key = master_key.master_key = master_key.new_master_key(
        replace=False)
    phase("Re-encrypting databases")

    # Update encryption keys for databases
    contact_list.database.database_key = new_master_key
    key_list.database.database_key = new_master_key
    group_list.database.database_key = new_master_key
    settings.database.database_key = new_master_key

    # Create temp databases for each database, do not replace original.
    with ignored(SoftError):
        change_log_db_key(old_master_key, new_master_key, settings)
    contact_list.store_contacts(replace=False)
    key_list.store_keys(replace=False)
    group_list.store_groups(replace=False)
    settings.store_settings(replace=False)

    # At this point all temp files exist and they have been checked to be valid by the respective
    # temp file writing function. It's now time to create a temp file for the new master key
    # database. Once the temp master key database is created, the `replace_database_data()` method
    # will also run the atomic `os.replace()` command for the master key database.
    master_key.replace_database_data()

    # Next we do the atomic `os.replace()` for all other files too.
    replace_log_db(settings)
    contact_list.database.replace_database()
    key_list.database.replace_database()
    group_list.database.replace_database()
    settings.database.replace_database()

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

    cmd_win = window_list.get_command_window()
    cmd_win.add_new(ts, "Changed Receiver master password.")
예제 #10
0
    def new_master_key(self) -> bytes:
        """Create a new master key from password and salt.

        The generated master key depends on a 256-bit salt and the
        password entered by the user. Additional computational strength
        is added by the slow hash function (Argon2d). This method
        automatically tweaks the Argon2 memory parameter so that key
        derivation on used hardware takes at least three seconds. The
        more cores and the faster each core is, the more security a
        given password provides.

        The preimage resistance of BLAKE2b prevents derivation of master
        key from the stored hash, and Argon2d ensures brute force and
        dictionary attacks against the master password are painfully
        slow even with GPUs/ASICs/FPGAs, as long as the password is
        sufficiently strong.

        The salt does not need additional protection as the security it
        provides depends on the salt space in relation to the number of
        attacked targets (i.e. if two or more physically compromised
        systems happen to share the same salt, the attacker can speed up
        the attack against those systems with time-memory-trade-off
        attack).

        A 256-bit salt ensures that even in a group of 4.8*10^29 users,
        the probability that two users share the same salt is just
        10^(-18).*
            * https://en.wikipedia.org/wiki/Birthday_attack
        """
        password = MasterKey.new_password()
        salt = csprng(ARGON2_SALT_LENGTH)
        memory = ARGON2_MIN_MEMORY

        parallelism = multiprocessing.cpu_count()
        if self.local_test:
            parallelism = max(1, parallelism // 2)

        phase("Deriving master key", head=2)
        while True:
            time_start = time.monotonic()
            master_key = argon2_kdf(password, salt, ARGON2_ROUNDS, memory,
                                    parallelism)
            kd_time = time.monotonic() - time_start

            if kd_time < MIN_KEY_DERIVATION_TIME:
                memory *= 2
            else:
                ensure_dir(DIR_USER_DATA)
                with open(self.file_name, 'wb+') as f:
                    f.write(salt + blake2b(master_key) + int_to_bytes(memory) +
                            int_to_bytes(parallelism))
                phase(DONE)
                return master_key
예제 #11
0
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList',
                      group_list: 'GroupList', settings: 'Settings',
                      queues: Dict[bytes,
                                   'Queue'], master_key: 'MasterKey') -> None:
    """Change master key on TxM/RxM."""
    try:
        if settings.session_traffic_masking:
            raise FunctionReturn(
                "Error: Command is disabled during traffic masking.")

        try:
            device = user_input.plaintext.split()[1].lower()
        except IndexError:
            raise FunctionReturn("Error: No target system specified.")

        if device not in [TX, RX]:
            raise FunctionReturn("Error: Invalid target system.")

        if device == RX:
            queue_command(CHANGE_MASTER_K_HEADER, settings,
                          queues[COMMAND_PACKET_QUEUE])
            return None

        old_master_key = master_key.master_key[:]
        master_key.new_master_key()
        new_master_key = master_key.master_key

        phase("Re-encrypting databases")

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_CHANGE_MASTER_KEY_HEADER, master_key))

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

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

        phase(DONE)
        box_print("Master key successfully changed.", head=1)
        clear_screen(delay=1.5)

    except KeyboardInterrupt:
        raise FunctionReturn("Password change aborted.",
                             delay=1,
                             head=3,
                             tail_clear=True)
예제 #12
0
    def determine_memory_cost(
        self,
        password: str,
        salt: bytes,
        time_cost: int,
        memory_cost: int,
        parallelism: int,
    ) -> Tuple[int, bytes]:
        """Determine suitable memory_cost value for Argon2id.

        If we reached this function, it means we found a `t+1` value for
        time_cost (explained in the `determine_time_cost` function). We
        therefore do a binary search on the amount of memory to use
        until we hit the desired key derivation time range.
        """
        lower_bound = ARGON2_MIN_MEMORY_COST
        upper_bound = memory_cost

        while True:
            memory_cost = int(round((lower_bound + upper_bound) // 2, -3))

            print_on_previous_line()
            phase(f"Trying memory cost {memory_cost} KiB")
            master_key, kd_time = self.timed_key_derivation(
                password, salt, time_cost, memory_cost, parallelism)
            phase(f"{kd_time:.1f}s", done=True)

            # If we found a suitable memory_cost value, we accept the key and the memory_cost.
            if MIN_KEY_DERIVATION_TIME <= kd_time <= MAX_KEY_DERIVATION_TIME:
                return memory_cost, master_key

            # The search might fail e.g. if external CPU load causes delay in key
            # derivation, which causes the search to continue into wrong branch. In
            # such a situation the search is restarted. The binary search is problematic
            # with tight key derivation time target ranges, so if the search keeps
            # restarting, increasing MAX_KEY_DERIVATION_TIME (and thus expanding the
            # range) will help finding suitable memory_cost value faster. Increasing
            # MAX_KEY_DERIVATION_TIME slightly affects security (positively) and user
            # experience (negatively).
            if memory_cost == lower_bound or memory_cost == upper_bound:
                lower_bound = ARGON2_MIN_MEMORY_COST
                upper_bound = self.get_available_memory()
                continue

            if kd_time < MIN_KEY_DERIVATION_TIME:
                lower_bound = memory_cost

            elif kd_time > MAX_KEY_DERIVATION_TIME:
                upper_bound = memory_cost
예제 #13
0
파일: gateway.py 프로젝트: barleyj/tfc
    def client_establish_socket(self) -> None:
        """Establish IPC client."""
        try:
            phase("Waiting for connection to NH", offset=11)
            while True:
                try:
                    socket_number = 5000 if self.settings.data_diode_sockets else 5001
                    self.interface = multiprocessing.connection.Client(
                        ('localhost', socket_number))
                    phase("Established", done=True)
                    break
                except socket.error:
                    time.sleep(0.1)

        except KeyboardInterrupt:
            graceful_exit()
예제 #14
0
파일: files.py 프로젝트: gtog/tfc
def process_file(
        ts: 'datetime',  # Timestamp of received_packet
        onion_pub_key: bytes,  # Onion Service pubkey of sender
        file_ct: bytes,  # File ciphertext
        file_key: bytes,  # File decryption key
        contact_list: 'ContactList',  # ContactList object
        window_list: 'WindowList',  # WindowList object
        settings: 'Settings'  # Settings object
) -> None:
    """Store file received from a contact."""
    nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick

    phase("Processing received file", head=1)
    try:
        file_pt = auth_and_decrypt(file_ct, file_key)
    except nacl.exceptions.CryptoError:
        raise FunctionReturn(
            f"Error: Decryption key for file from {nick} was invalid.")

    try:
        file_dc = decompress(file_pt, settings.max_decompress_size)
    except zlib.error:
        raise FunctionReturn(f"Error: Failed to decompress file from {nick}.")
    phase(DONE)
    print_on_previous_line(reps=2)

    try:
        file_name = bytes_to_str(file_dc[:PADDED_UTF32_STR_LENGTH])
    except UnicodeError:
        raise FunctionReturn(
            f"Error: Name of file from {nick} had invalid encoding.")

    if not file_name.isprintable() or not file_name or '/' in file_name:
        raise FunctionReturn(f"Error: Name of file from {nick} was invalid.")

    f_data = file_dc[PADDED_UTF32_STR_LENGTH:]

    file_dir = f'{DIR_RECV_FILES}{nick}/'
    final_name = store_unique(f_data, file_dir, file_name)
    message = f"Stored file from {nick} as '{final_name}'."

    if settings.traffic_masking and window_list.active_win is not None:
        window = window_list.active_win
    else:
        window = window_list.get_window(onion_pub_key)

    window.add_new(ts, message, onion_pub_key, output=True, event_msg=True)
예제 #15
0
파일: commands.py 프로젝트: barleyj/tfc
def change_master_key(user_input:   'UserInput',
                      contact_list: 'ContactList',
                      group_list:   'GroupList',
                      settings:     'Settings',
                      queues:       Dict[bytes, 'Queue'],
                      master_key:   'MasterKey') -> None:
    """Change master key on TxM/RxM."""
    try:
        if settings.session_trickle:
            raise FunctionReturn("Command disabled during trickle connection.")

        try:
            device = user_input.plaintext.split()[1]
        except IndexError:
            raise FunctionReturn("No target system specified.")

        if device.lower() not in ['tx', 'txm', 'rx', 'rxm']:
            raise FunctionReturn("Invalid target system.")

        if device.lower() in ['rx', 'rxm']:
            queue_command(CHANGE_MASTER_K_HEADER, settings, queues[COMMAND_PACKET_QUEUE])
            print('')
            return None

        old_master_key = master_key.master_key[:]
        master_key.new_master_key()
        new_master_key = master_key.master_key

        ensure_dir(f'{DIR_USER_DATA}/')
        file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
        if os.path.isfile(file_name):
            phase("Re-encrypting log-file")
            re_encrypt(old_master_key, new_master_key, settings)
            phase("Done")

        queues[KEY_MANAGEMENT_QUEUE].put(('KEY', master_key))

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

        box_print("Master key successfully changed.", head=1)
        clear_screen(delay=1.5)
    except KeyboardInterrupt:
        raise FunctionReturn("Password change aborted.")
예제 #16
0
def decrypt_rx_psk(ct_tag: bytes, salt: bytes) -> bytes:
    """Get PSK password from user and decrypt Rx-PSK."""
    while True:
        try:
            password = MasterKey.get_password("PSK password")
            phase("Deriving the key decryption key", head=2)
            kdk = argon2_kdf(password, salt, ARGON2_PSK_TIME_COST,
                             ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM)
            psk = auth_and_decrypt(ct_tag, kdk)
            phase(DONE)
            return psk

        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 SoftError("PSK import aborted.",
                            head=2,
                            delay=1,
                            tail_clear=True)
예제 #17
0
파일: crypto.py 프로젝트: gtog/tfc
def check_kernel_entropy() -> None:
    """Wait until the kernel CSPRNG is sufficiently seeded.

    Wait until the `entropy_avail` file states that kernel entropy pool
    has at least 512 bits of entropy. The waiting ensures the ChaCha20
    CSPRNG is fully seeded (i.e., it has the maximum of 384 bits of
    entropy) when it generates keys. The same entropy threshold is used
    by the GETRANDOM syscall in random.c:
        #define CRNG_INIT_CNT_THRESH (2*CHACHA20_KEY_SIZE)

    For more information on the kernel CSPRNG threshold, see
        https://security.stackexchange.com/a/175771/123524
        https://crypto.stackexchange.com/a/56377
    """
    message = "Waiting for kernel CSPRNG entropy pool to fill up"
    phase(message, head=1)

    ent_avail = 0
    while ent_avail < ENTROPY_THRESHOLD:
        with ignored(EOFError, KeyboardInterrupt):
            with open('/proc/sys/kernel/random/entropy_avail') as f:
                ent_avail = int(f.read().strip())
            m_print(f"{ent_avail}/{ENTROPY_THRESHOLD}")
            print_on_previous_line(delay=0.1)

    print_on_previous_line()
    phase(message)
    phase(DONE)
예제 #18
0
    def load_master_key(self) -> bytes:
        """Derive the master key from password and salt.

        Load the salt, hash, and key derivation settings from the login
        database. Derive the purported master key from the salt and
        entered password. If the BLAKE2b hash of derived master key
        matches the hash in the login database, accept the derived
        master key.
        """
        database_data = self.database.load_database()

        if len(database_data) != MASTERKEY_DB_SIZE:
            raise CriticalError(f"Invalid {self.file_name} database size.")

        salt, key_hash, time_bytes, memory_bytes, parallelism_bytes \
            = separate_headers(database_data, [ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH,
                                               ENCODED_INTEGER_LENGTH, ENCODED_INTEGER_LENGTH])

        time_cost   = bytes_to_int(time_bytes)
        memory_cost = bytes_to_int(memory_bytes)
        parallelism = bytes_to_int(parallelism_bytes)

        while True:
            password = MasterKey.get_password()
            phase("Deriving master key", head=2, offset=len("Password correct"))
            purp_key = argon2_kdf(password, salt, time_cost, memory_cost, parallelism)

            if blake2b(purp_key) == key_hash:
                phase("Password correct", done=True, delay=1)
                clear_screen()
                return purp_key

            phase("Invalid password", done=True, delay=1)
            print_on_previous_line(reps=5)
예제 #19
0
파일: crypto.py 프로젝트: barleyj/tfc
def init_entropy() -> None:
    """Wait until Kernel CSPRNG is sufficiently seeded.

    Wait until entropy_avail file states that system has at least 512 bits of
    entropy. The headroom allows room for error in accuracy of entropy
    collector's entropy estimator; As long as input has at least 4 bits per
    byte of actual entropy, /dev/urandom will be sufficiently seeded when
    it is allowed to generate keys.
    """
    clear_screen()
    phase("Waiting for Kernel CSPRNG random pool to fill up", head=1)

    ent_avail = 0
    threshold = 512

    while ent_avail < threshold:
        try:
            with open('/proc/sys/kernel/random/entropy_avail') as f:
                value = f.read()
            ent_avail = int(value.strip())
            c_print("{}/{}".format(ent_avail, threshold))
            print_on_previous_line(delay=0.01)
        except (KeyboardInterrupt, EOFError):
            pass

    print_on_previous_line()
    phase("Waiting for Kernel CSPRNG random pool to fill up")
    phase("Done")
예제 #20
0
파일: db_masterkey.py 프로젝트: barleyj/tfc
    def load_master_key(self) -> None:
        """Derive master key from password from stored values (salt, rounds, memory)."""
        ensure_dir(f'{DIR_USER_DATA}/')
        with open(self.file_name, 'rb') as f:
            data = f.read()
        salt = data[0:32]
        k_hash = data[32:64]
        rounds = bytes_to_int(data[64:72])
        memory = bytes_to_int(data[72:80])

        while True:
            password = MasterKey.get_password()
            phase("Deriving master key", head=2, offset=16)
            purp_key, _ = argon2_kdf(password,
                                     salt,
                                     rounds,
                                     memory,
                                     local_testing=self.local_test)
            if hash_chain(purp_key) == k_hash:
                phase("Password correct", done=True)
                self.master_key = purp_key
                clear_screen(delay=0.5)
                break
            else:
                phase("Invalid password", done=True)
                print_on_previous_line(reps=5, delay=1)
예제 #21
0
파일: gateway.py 프로젝트: barleyj/tfc
    def search_serial_interface(self) -> str:
        """Search for serial interface."""
        if self.settings.session_usb_iface:
            search_announced = False

            if not self.init_found:
                print_on_previous_line()
                phase("Searching for USB-to-serial interface")

            while True:
                time.sleep(0.1)
                for f in sorted(os.listdir('/dev')):
                    if f.startswith('ttyUSB'):
                        if self.init_found:
                            time.sleep(1.5)
                        phase('Found', done=True)
                        if self.init_found:
                            print_on_previous_line(reps=2)
                        self.init_found = True
                        return '/dev/{}'.format(f)
                else:
                    if not search_announced:
                        if self.init_found:
                            phase(
                                "Serial adapter disconnected. Waiting for interface",
                                head=1)
                        search_announced = True

        else:
            f = 'serial0' if 'Raspbian' in platform.platform() else 'ttyS0'
            if f in sorted(os.listdir('/dev/')):
                return '/dev/{}'.format(f)
            else:
                raise CriticalError("Error: /dev/{} was not found.".format(f))
예제 #22
0
    def search_serial_interface(self) -> str:
        """Search for a serial interface."""
        if self.settings.session_usb_serial_adapter:
            search_announced = False

            if not self.init_found:
                phase("Searching for USB-to-serial interface", offset=len('Found'))

            while True:
                for f in sorted(os.listdir('/dev/')):
                    if f.startswith('ttyUSB'):
                        if self.init_found:
                            time.sleep(1)
                        phase('Found', done=True)
                        if self.init_found:
                            print_on_previous_line(reps=2)
                        self.init_found = True
                        return f'/dev/{f}'

                time.sleep(0.1)
                if self.init_found and not search_announced:
                    phase("Serial adapter disconnected. Waiting for interface", head=1, offset=len('Found'))
                    search_announced = True

        else:
            if self.settings.built_in_serial_interface in sorted(os.listdir('/dev/')):
                return f'/dev/{self.settings.built_in_serial_interface}'
            raise CriticalError(f"Error: /dev/{self.settings.built_in_serial_interface} was not found.")
예제 #23
0
def ch_master_key(ts: 'datetime', window_list: 'WindowList',
                  contact_list: 'ContactList', group_list: 'GroupList',
                  key_list: 'KeyList', settings: 'Settings',
                  master_key: 'MasterKey') -> None:
    """Prompt the user for a new master password and derive a new master key from that."""
    try:
        old_master_key = master_key.master_key[:]
        master_key.master_key = master_key.new_master_key()

        phase("Re-encrypting databases")

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

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

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

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

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Password change aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #24
0
def process_imported_file(ts:          'datetime',
                          packet:      bytes,
                          window_list: 'WindowList'):
    """Decrypt and store imported file."""
    while True:
        try:
            print('')
            key = get_b58_key('imported_file')
            phase("Decrypting file", head=1)
            file_pt = auth_and_decrypt(packet[1:], key, soft_e=True)
            phase("Done")
            break
        except nacl.exceptions.CryptoError:
            c_print("Invalid decryption key. Try again.", head=2)
            print_on_previous_line(reps=6, delay=1.5)
        except KeyboardInterrupt:
            raise FunctionReturn("File import aborted.")

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

    try:
        f_name  = bytes_to_str(file_dc[:1024])
    except UnicodeError:
        raise FunctionReturn("Received file had an invalid name.")

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

    f_data     = file_dc[1024:]
    final_name = store_unique(f_data, DIR_IMPORTED, f_name)

    message = "Stored imported file to {}/{}".format(DIR_IMPORTED, final_name)
    box_print(message, head=1)

    local_win = window_list.get_local_window()
    local_win.print_new(ts, message, print_=False)
예제 #25
0
    def load_master_key(self) -> None:
        """Derive master key from password and salt."""
        with open(self.file_name, 'rb') as f:
            data = f.read()
        salt = data[0:32]
        key_hash = data[32:64]
        rounds = bytes_to_int(data[64:72])
        memory = bytes_to_int(data[72:80])
        parallelism = bytes_to_int(data[80:88])

        while True:
            password = MasterKey.get_password()
            phase("Deriving master key", head=2, offset=16)
            purp_key, _ = argon2_kdf(password, salt, rounds, memory,
                                     parallelism)

            if hash_chain(purp_key) == key_hash:
                self.master_key = purp_key
                phase("Password correct", done=True)
                clear_screen(delay=0.5)
                break
            else:
                phase("Invalid password", done=True)
                print_on_previous_line(reps=5, delay=1)
예제 #26
0
def check_kernel_entropy() -> None:
    """Wait until Kernel CSPRNG is sufficiently seeded.

    Wait until entropy_avail file states that system has at least 512
    bits of entropy. The headroom allows room for error in accuracy of
    entropy collector's entropy estimator; As long as input has at least
    4 bits per byte of actual entropy, kernel CSPRNG will be sufficiently
    seeded when it generates 256-bit keys.
    """
    clear_screen()
    phase("Waiting for Kernel CSPRNG entropy pool to fill up", head=1)

    ent_avail = 0
    while ent_avail < ENTROPY_THRESHOLD:
        with ignored(EOFError, KeyboardInterrupt):
            with open('/proc/sys/kernel/random/entropy_avail') as f:
                value = f.read()
            ent_avail = int(value.strip())
            c_print(f"{ent_avail}/{ENTROPY_THRESHOLD}")
            print_on_previous_line(delay=0.1)

    print_on_previous_line()
    phase("Waiting for Kernel CSPRNG entropy pool to fill up")
    phase(DONE)
예제 #27
0
def send_file(path:     str,
              settings: 'Settings',
              queues:   'QueueDict',
              window:   'TxWindow'
              ) -> None:
    """Send file to window members in a single transmission.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    key_delivery_msg = base64.b85encode(ct_hash + file_key).decode()
    for contact in window:
        queue_message(user_input=UserInput(key_delivery_msg, MESSAGE),
                      window    =MockWindow(contact.onion_pub_key, [contact]),
                      settings  =settings,
                      queues    =queues,
                      header    =FILE_KEY_HEADER,
                      log_as_ph =True)
    phase(DONE)
    print_on_previous_line(flush=True)
    m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
예제 #28
0
 def test_phase(self, _):
     self.assertIsNone(phase('Entering phase'))
     self.assertIsNone(phase(DONE))
     self.assertIsNone(
         phase('Starting phase', head=1, offset=len("Finished")))
     self.assertIsNone(phase('Finished', done=True))
예제 #29
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 Argon2id 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, ARGON2_PSK_TIME_COST,
                         ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM)
        phase(DONE)

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

        store_keys_on_removable_drive(ct_tag, salt, nick, onion_pub_key,
                                      onion_service, settings)

        deliver_contact_data(KEY_EX_PSK_TX, nick, onion_pub_key, tx_mk,
                             csprng(), tx_hk, csprng(), queues, settings)

        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 SoftError("PSK generation aborted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
예제 #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)