Пример #1
0
 def test_invalid_size_key_from_argon2_raises_critical_error(self) -> None:
     with self.assertRaises(SystemExit):
         argon2_kdf(self.password, self.salt, ARGON2_MIN_TIME_COST,
                    ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM)
     with self.assertRaises(SystemExit):
         argon2_kdf(self.password, self.salt, ARGON2_MIN_TIME_COST,
                    ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM)
Пример #2
0
 def test_invalid_length_salt_raises_critical_error(self) -> None:
     invalid_salts = [
         salt_length * b'a' for salt_length in
         [0, ARGON2_SALT_LENGTH - 1, ARGON2_SALT_LENGTH + 1, 1000]
     ]
     for invalid_salt in invalid_salts:
         with self.assertRaises(SystemExit):
             argon2_kdf(self.password, invalid_salt, ARGON2_MIN_TIME_COST,
                        ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM)
Пример #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 test_valid_psk(self):
        # Setup
        packet          = b'*****@*****.**'
        contact_list    = ContactList(nicks=['Alice', 'local'])
        key_list        = KeyList(nicks=['Alice', 'local'])
        keyset          = key_list.get_keyset('*****@*****.**')
        keyset.rx_key   = bytes(32)
        keyset.rx_hek   = bytes(32)
        window_list     = WindowList(nicks=['Alice', 'local'])
        ts              = datetime.datetime.now()
        settings        = Settings(disable_gui_dialog=True)
        o_input         = builtins.input
        o_getpass       = getpass.getpass
        builtins.input  = lambda x: 'ut_psk'
        getpass.getpass = lambda x: 'testpassword'
        password        = '******'
        salt            = os.urandom(32)
        rx_key          = os.urandom(32)
        rx_hek          = os.urandom(32)
        kek, _          = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1)
        ct_tag          = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open('ut_psk', 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertTrue(os.path.isfile('ut_psk'))
        self.assertIsNone(psk_import(packet, ts, window_list, contact_list, key_list, settings))
        self.assertFalse(os.path.isfile('ut_psk'))
        self.assertEqual(keyset.rx_key, rx_key)
        self.assertEqual(keyset.rx_hek, rx_hek)

        # Teardown
        builtins.input  = o_input
        getpass.getpass = o_getpass
Пример #5
0
    def new_master_key(self) -> None:
        """Create a new master key from salt and password.

        The number of rounds starts at 1 but is increased dynamically based
        on system performance. This allows more security on faster platforms
        without additional cost on key derivation time.
        """
        password = MasterKey.new_password()
        salt = keygen()
        rounds = 1

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

            if time_final > 3.0:
                self.master_key = master_key
                master_key_hash = hash_chain(master_key)
                ensure_dir(f'{DIR_USER_DATA}/')
                with open(self.file_name, 'wb+') as f:
                    f.write(salt + master_key_hash + int_to_bytes(rounds) +
                            int_to_bytes(memory))
                phase('Done')
                break
            else:
                rounds *= 2
Пример #6
0
 def test_argon2d_kdf(self):
     key = argon2_kdf('password',
                      ARGON2_SALT_LENGTH * b'a',
                      rounds=1,
                      memory=100)
     self.assertIsInstance(key, bytes)
     self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH)
Пример #7
0
    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)
Пример #8
0
    def test_valid_psk_overwrite_failure(self):
        # Setup
        keyset        = self.key_list.get_keyset('*****@*****.**')
        keyset.rx_key = bytes(KEY_LENGTH)
        keyset.rx_hek = bytes(KEY_LENGTH)

        input_list       = ['ut_psk', '']
        gen              = iter(input_list)
        builtins.input   = lambda _: next(gen)
        subprocess.Popen = TestImportPSKRxKeys.MockPopen

        getpass.getpass = lambda _: 'testpassword'
        password        = '******'
        salt            = os.urandom(ARGON2_SALT_LEN)
        rx_key          = os.urandom(KEY_LENGTH)
        rx_hek          = os.urandom(KEY_LENGTH)
        kek, _          = argon2_kdf(password, salt, parallelism=1)
        ct_tag          = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open('ut_psk', 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertTrue(os.path.isfile('ut_psk'))
        self.assertIsNone(import_psk_rx_keys(self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings))
        self.assertTrue(os.path.isfile('ut_psk'))
        self.assertEqual(keyset.rx_key, rx_key)
        self.assertEqual(keyset.rx_hek, rx_hek)
Пример #9
0
    def test_valid_psk_overwrite_failure(self, *_: Any) -> None:
        # Setup
        keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
        keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH)
        keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH)

        salt = os.urandom(ARGON2_SALT_LENGTH)
        rx_key = os.urandom(SYMMETRIC_KEY_LENGTH)
        rx_hek = os.urandom(SYMMETRIC_KEY_LENGTH)
        kek = argon2_kdf('test_password',
                         salt,
                         time_cost=1,
                         memory_cost=100,
                         parallelism=1)
        ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open(self.file_name, 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertTrue(os.path.isfile(self.file_name))
        self.assertIsNone(key_ex_psk_rx(*self.args))
        self.assertTrue(os.path.isfile(self.file_name))
        self.assertEqual(keyset.rx_mk, rx_key)
        self.assertEqual(keyset.rx_hk, rx_hek)
Пример #10
0
 def test_argon2_kdf_local_testing(self):
     key, parallelism = argon2_kdf('password',
                                   ARGON2_SALT_LEN * b'a',
                                   local_test=True)
     self.assertIsInstance(key, bytes)
     self.assertEqual(len(key), KEY_LENGTH)
     self.assertEqual(parallelism, max(multiprocessing.cpu_count() // 2, 1))
Пример #11
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)
Пример #12
0
    def test_local_testing_sanity_check(self):
        key, mem = argon2_kdf("test_password",
                              salt=bytes(32),
                              rounds=1,
                              local_testing=True)

        self.assertIsInstance(key, bytes)
        self.assertIsInstance(mem, int)
Пример #13
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.")
Пример #14
0
    def timed_key_derivation(password: str, salt: bytes, time_cost: int,
                             memory_cost: int,
                             parallelism: int) -> Tuple[bytes, float]:
        """Derive key and measure its derivation time."""
        time_start = time.monotonic()
        master_key = argon2_kdf(password, salt, time_cost, memory_cost,
                                parallelism)
        kd_time = time.monotonic() - time_start

        return master_key, kd_time
Пример #15
0
    def test_sanity_check(self):
        key, mem = argon2_kdf("test_password",
                              salt=bytes(32),
                              rounds=1,
                              memory=128000,
                              parallelism=1)

        self.assertEqual(mem, 128000)
        self.assertEqual(
            key.hex(), "73883b6b2ea60d0adf27fb52e1f41af4"
            "29bfe8a0d79ae4a2f87be6c4d73e6a11")
Пример #16
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
Пример #17
0
    def test_invalid_keys_raise_fr(self, *_):
        # Setup
        keyset       = self.key_list.get_keyset(nick_to_pub_key("Alice"))
        keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH)
        keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH)

        salt   = bytes(ARGON2_SALT_LENGTH)
        rx_key = bytes(SYMMETRIC_KEY_LENGTH)
        rx_hek = bytes(SYMMETRIC_KEY_LENGTH)
        kek    = argon2_kdf('password', salt, time_cost=1, memory_cost=100, parallelism=1)
        ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open(self.file_name, 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assert_fr("Error: Received invalid keys from contact.", key_ex_psk_rx, *self.args)
Пример #18
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)
Пример #19
0
    def test_invalid_keys_raise_fr(self):
        # Setup
        keyset          = self.key_list.get_keyset('*****@*****.**')
        keyset.rx_key   = bytes(KEY_LENGTH)
        keyset.rx_hek   = bytes(KEY_LENGTH)
        password        = '******'
        input_list      = ['bad', password]
        gen             = iter(input_list)
        getpass.getpass = lambda _: str(next(gen))

        salt    = os.urandom(ARGON2_SALT_LEN)
        rx_key  = bytes(KEY_LENGTH)
        rx_hek  = os.urandom(KEY_LENGTH)
        kek, _  = argon2_kdf(password, salt, parallelism=1)
        ct_tag  = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open('ut_psk', 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertFR("Error: Received invalid keys from contact.",
                      import_psk_rx_keys, self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings)
Пример #20
0
    def test_invalid_keys_raise_fr(self):
        # Setup
        packet          = b'*****@*****.**'
        contact_list    = ContactList(nicks=['Alice', 'local'])
        key_list        = KeyList(nicks=['Alice', 'local'])
        keyset          = key_list.get_keyset('*****@*****.**')
        keyset.rx_key   = bytes(32)
        keyset.rx_hek   = bytes(32)
        window_list     = WindowList(nicks=['Alice', 'local'])
        ts              = datetime.datetime.now()
        settings        = Settings(disable_gui_dialog=True)
        o_input         = builtins.input
        o_getpass       = getpass.getpass
        builtins.input  = lambda x: 'ut_psk'
        input_list      = ['bad', 'testpassword']
        gen             = iter(input_list)

        def mock_input(_):
            return str(next(gen))

        getpass.getpass = mock_input
        password        = '******'
        salt            = os.urandom(32)
        rx_key          = bytes(32)
        rx_hek          = os.urandom(32)
        kek, _          = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1)
        ct_tag          = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open('ut_psk', 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertFR("Keys from contact are not valid.", psk_import, packet, ts, window_list, contact_list, key_list, settings)

        # Teardown
        os.remove('ut_psk')
        builtins.input  = o_input
        getpass.getpass = o_getpass
Пример #21
0
    def new_master_key(self):
        password = MasterKey.new_password()
        salt     = os.urandom(32)
        rounds   = 1

        assert isinstance(salt, bytes)
        while True:
            time_start         = time.monotonic()
            master_key, memory = argon2_kdf(password, salt, rounds, local_testing=False)
            time_final         = time.monotonic() - time_start

            if time_final > 3.0:
                self.master_key = master_key
                master_key_hash = hash_chain(master_key)
                ensure_dir(f'{DIR_USER_DATA}/')
                with open(self.file_name, 'wb+') as f:
                    f.write(salt
                            + master_key_hash
                            + int_to_bytes(rounds)
                            + int_to_bytes(memory))
                break
            else:
                rounds *= 2
Пример #22
0
    def test_valid_psk(self):
        # Setup
        keyset        = self.key_list.get_keyset('*****@*****.**')
        keyset.rx_key = bytes(KEY_LENGTH)
        keyset.rx_hek = bytes(KEY_LENGTH)

        getpass.getpass = lambda _: 'testpassword'
        password        = '******'
        salt            = os.urandom(ARGON2_SALT_LEN)
        rx_key          = os.urandom(KEY_LENGTH)
        rx_hek          = os.urandom(KEY_LENGTH)
        kek, _          = argon2_kdf(password, salt, parallelism=1)
        ct_tag          = encrypt_and_sign(rx_key + rx_hek, key=kek)

        with open('ut_psk', 'wb+') as f:
            f.write(salt + ct_tag)

        # Test
        self.assertTrue(os.path.isfile('ut_psk'))
        self.assertIsNone(import_psk_rx_keys(self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings))
        self.assertFalse(os.path.isfile('ut_psk'))
        self.assertEqual(keyset.rx_key, rx_key)
        self.assertEqual(keyset.rx_hek, rx_hek)
Пример #23
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)
Пример #24
0
 def test_too_small_parallelism_raises_critical_error(self) -> None:
     with self.assertRaises(SystemExit):
         argon2_kdf(self.password, self.salt, ARGON2_MIN_TIME_COST,
                    ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM - 1)
Пример #25
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)
Пример #26
0
 def test_invalid_salt_length_raises_critical_error(self):
     for salt_length in [v for v in range(1000) if v != ARGON2_SALT_LENGTH]:
         with self.assertRaises(SystemExit):
             argon2_kdf('password', salt_length * b'a')
Пример #27
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)

        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

        c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
        command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk +
                   csprng() + 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)

        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)
Пример #28
0
 def test_argon2_kdf_key_type_and_length(self) -> None:
     key = argon2_kdf(self.password, self.salt, ARGON2_MIN_TIME_COST,
                      ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM)
     self.assertIsInstance(key, bytes)
     self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH)
Пример #29
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)
Пример #30
0
 def test_argon2_kdf(self):
     key, parallelism = argon2_kdf('password', ARGON2_SALT_LEN * b'a')
     self.assertIsInstance(key, bytes)
     self.assertEqual(len(key), KEY_LENGTH)
     self.assertEqual(parallelism, multiprocessing.cpu_count())