Exemplo n.º 1
0
 def handle_migration_completed():
     """
     When migration completes outside of the current process, we rely on a notification to inform
     the current process that it needs to reset/refresh its keyring. This allows us to stop using
     the legacy keyring in an already-running daemon if migration is completed using the CLI.
     """
     KeyringWrapper.get_shared_instance().refresh_keyrings()
Exemplo n.º 2
0
 def remove_master_passphrase(current_passphrase: Optional[str]) -> None:
     """
     Removes the user-provided master passphrase, and replaces it with the default
     master passphrase. The keyring contents will remain encrypted, but to the
     default passphrase.
     """
     KeyringWrapper.get_shared_instance().remove_master_passphrase(
         current_passphrase)
 def test_legacy_keyring_does_not_support_master_passphrase(self):
     """
     CryptFileKeyring (legacy keyring) should not support setting a master passphrase
     """
     # Expect: legacy keyring in use and master passphrase is not supported
     assert KeyringWrapper.get_shared_instance().legacy_keyring is not None
     assert KeyringWrapper.get_shared_instance().using_legacy_keyring(
     ) is True
     assert KeyringWrapper.get_shared_instance(
     ).keyring_supports_master_passphrase() is False
    def test_set_master_passphrase_on_keyring(self):
        """
        Setting a master passphrase should cache the passphrase and be usable to unlock
        the keyring. Using an old passphrase should not unlock the keyring.
        """
        # When: setting the master passphrase
        KeyringWrapper.get_shared_instance().set_master_passphrase(
            DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE,
            "testing one two three")

        # Expect: the master passphrase is cached and can be validated
        assert KeyringWrapper.get_shared_instance(
        ).get_cached_master_passphrase() == ("testing one two three", True)
        assert KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
            "testing one two three") is True

        # When: changing the master passphrase
        KeyringWrapper.get_shared_instance().set_master_passphrase(
            "testing one two three", "potato potato potato")

        # Expect: the new master passphrase is cached and can be validated
        assert KeyringWrapper.get_shared_instance(
        ).get_cached_master_passphrase() == ("potato potato potato", True)
        assert KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
            "potato potato potato") is True

        # Expect: old passphrase should not validate
        assert KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
            "testing one two three") is False
Exemplo n.º 5
0
 def set_master_passphrase(current_passphrase: Optional[str],
                           new_passphrase: str,
                           allow_migration: bool = True) -> None:
     """
     Encrypts the keyring contents to new passphrase, provided that the current
     passphrase can decrypt the contents
     """
     KeyringWrapper.get_shared_instance().set_master_passphrase(
         current_passphrase,
         new_passphrase,
         allow_migration=allow_migration)
    def test_master_passphrase_is_valid(self):
        """
        The default master passphrase should unlock the populated keyring (without any keys)
        """
        # Expect: default master passphrase should validate
        assert (
            KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
                DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE) is True)

        # Expect: bogus passphrase should not validate
        assert KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
            "foobarbaz") is False
 def test_default_cached_master_passphrase(self):
     """
     The default passphrase DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE is set
     """
     # Expect: cached passphrase set to DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE by default
     assert KeyringWrapper.get_shared_instance(
     ).get_cached_master_passphrase() == (
         DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE,
         False,
     )
     assert KeyringWrapper.get_shared_instance(
     ).has_cached_master_passphrase() is True
    def test_set_master_passphrase_with_hint(self):
        """
        Setting a passphrase hint at the same time as setting the passphrase
        """
        # When: setting the master passphrase with a hint
        KeyringWrapper.get_shared_instance().set_master_passphrase(
            None,
            "new master passphrase",
            passphrase_hint="some passphrase hint")

        # Expect: hint can be retrieved
        assert KeyringWrapper.get_shared_instance().get_master_passphrase_hint(
        ) == "some passphrase hint"
Exemplo n.º 9
0
    def migrate_legacy_keyring(passphrase: Optional[str] = None,
                               cleanup_legacy_keyring: bool = False) -> None:
        """
        Begins legacy keyring migration in a non-interactive manner
        """
        if passphrase is not None and passphrase != "":
            KeyringWrapper.get_shared_instance().set_master_passphrase(
                current_passphrase=None,
                new_passphrase=passphrase,
                write_to_keyring=False,
                allow_migration=False)

        KeyringWrapper.get_shared_instance().migrate_legacy_keyring(
            cleanup_legacy_keyring=cleanup_legacy_keyring)
    def test_using_file_keyring_without_legacy_keyring(self):
        """
        In the case of a new installation (no legacy CryptFileKeyring) using a FileKeyring
        with some content, the legacy keyring should not be used.
        """
        # Expect: the new keyring should have content
        assert KeyringWrapper.get_shared_instance().keyring.has_content(
        ) is True

        # Expect: the new keyring should be in use
        assert KeyringWrapper.get_shared_instance().legacy_keyring is None
        assert KeyringWrapper.get_shared_instance().using_legacy_keyring(
        ) is False
        assert KeyringWrapper.get_shared_instance().get_keyring(
        ) == KeyringWrapper.get_shared_instance().keyring
    def test_using_new_file_keyring(self):
        """
        In the case of a new installation using a new FileKeyring, the legacy keyring
        should not be used.
        """
        # Expect: the new keyring should not have any content
        assert KeyringWrapper.get_shared_instance().keyring.has_content(
        ) is False

        # Expect: the new keyring should be in use
        assert KeyringWrapper.get_shared_instance().legacy_keyring is None
        assert KeyringWrapper.get_shared_instance().using_legacy_keyring(
        ) is False
        assert KeyringWrapper.get_shared_instance().get_keyring(
        ) == KeyringWrapper.get_shared_instance().keyring
    def test_passphrase_hint(self):
        """
        Setting and retrieving the passphrase hint
        """
        # Expect: no hint set by default
        assert KeyringWrapper.get_shared_instance().get_master_passphrase_hint(
        ) is None

        # When: setting the passphrase hint while setting the master passphrase
        KeyringWrapper.get_shared_instance().set_master_passphrase(
            None, "passphrase", passphrase_hint="rhymes with bassphrase")

        # Expect: to retrieve the passphrase hint that was just set
        assert KeyringWrapper.get_shared_instance().get_master_passphrase_hint(
        ) == "rhymes with bassphrase"
Exemplo n.º 13
0
    def test_multiple_writers(self):
        num_workers = 20
        keyring_path = str(
            KeyringWrapper.get_shared_instance().keyring.keyring_path)
        passphrase_list = list(
            map(
                lambda x: ("test-service", f"test-user-{x}", f"passphrase {x}",
                           keyring_path, x, num_workers),
                range(num_workers),
            ))

        # Create a directory for each process to indicate readiness
        ready_dir: Path = Path(keyring_path).parent / "ready"
        mkdir(ready_dir)

        finished_dir: Path = Path(keyring_path).parent / "finished"
        mkdir(finished_dir)

        # When: spinning off children to each set a passphrase concurrently
        with Pool(processes=num_workers) as pool:
            res = pool.starmap_async(dummy_set_passphrase, passphrase_list)

            # Wait up to 30 seconds for all processes to indicate readiness
            assert poll_directory(ready_dir, num_workers, 30) is True

            log.warning(f"Test setup complete: {num_workers} workers ready")

            # Signal that testing should begin
            start_file_path: Path = ready_dir / "start"
            with open(start_file_path, "w") as f:
                f.write(f"{os.getpid()}\n")

            # Wait up to 30 seconds for all processes to indicate completion
            assert poll_directory(finished_dir, num_workers, 30) is True

            log.warning(f"Finished: {num_workers} workers finished")

            # Collect results
            res.get(
                timeout=10
            )  # 10 second timeout to prevent a bad test from spoiling the fun

        # Expect: parent process should be able to find all passphrases that were set by the child processes
        for item in passphrase_list:
            expected_passphrase = item[2]
            actual_passphrase = KeyringWrapper.get_shared_instance(
            ).get_passphrase(service=item[0], user=item[1])
            assert expected_passphrase == actual_passphrase
    def test_using_file_keyring_with_legacy_keyring(self):
        """
        In the case that an existing CryptFileKeyring (legacy) keyring exists and we're
        using a new FileKeyring with some keys in it, the FileKeyring's use should be
        used instead of the legacy keyring.
        """
        # Expect: the new keyring should have content
        assert KeyringWrapper.get_shared_instance().keyring.has_content(
        ) is True

        # Expect: the new keyring should be in use
        assert KeyringWrapper.get_shared_instance().legacy_keyring is None
        assert KeyringWrapper.get_shared_instance().using_legacy_keyring(
        ) is False
        assert KeyringWrapper.get_shared_instance().get_keyring(
        ) == KeyringWrapper.get_shared_instance().keyring
 def test_populated_file_keyring_has_master_passphrase(self):
     """
     Populated keyring should have the default master passphrase set
     """
     # Expect: master passphrase is set
     assert KeyringWrapper.get_shared_instance().has_master_passphrase(
     ) is True
Exemplo n.º 16
0
    def test_writer_lock_succeeds(self):
        """
        If a write lock is already held, another process will be able to acquire the
        same lock once the lock is released by the current holder
        """
        lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        lock = fasteners.InterProcessReaderWriterLock(str(lock_path))

        # When: a writer lock is already acquired
        lock.acquire_write_lock()

        child_proc_fn = dummy_fn_requiring_writer_lock
        timeout = 0.25
        attempts = 4

        with Pool(processes=1) as pool:
            # When: a child process attempts to acquire the same writer lock, failing after 1 second
            res = pool.starmap_async(child_writer_dispatch, [(child_proc_fn, lock_path, timeout, attempts)])

            # Brief delay to allow the child to timeout once
            sleep(0.25)

            # When: the writer lock is released
            lock.release_write_lock()

            # Expect: the child to acquire the writer lock
            result = res.get(timeout=10)  # 10 second timeout to prevent a bad test from spoiling the fun
            assert result[0] == "A winner is you!"
Exemplo n.º 17
0
    def _patch_and_create_keychain(
        self,
        *,
        user: str,
        service: str,
        populate: bool,
        setup_cryptfilekeyring: bool,
        existing_keyring_path: Optional[str],
        use_os_credential_store: bool,
    ):
        existing_keyring_dir = Path(existing_keyring_path).parent if existing_keyring_path else None
        temp_dir = existing_keyring_dir or tempfile.mkdtemp(prefix="test_keyring_wrapper")

        mock_supports_keyring_passphrase_patch = patch("chia.util.keychain.supports_keyring_passphrase")
        mock_supports_keyring_passphrase = mock_supports_keyring_passphrase_patch.start()

        # Patch supports_keyring_passphrase() to return True
        mock_supports_keyring_passphrase.return_value = True

        mock_supports_os_passphrase_storage_patch = patch("chia.util.keychain.supports_os_passphrase_storage")
        mock_supports_os_passphrase_storage = mock_supports_os_passphrase_storage_patch.start()

        # Patch supports_os_passphrase_storage() to return use_os_credential_store
        mock_supports_os_passphrase_storage.return_value = use_os_credential_store

        mock_configure_backend_patch = patch.object(KeyringWrapper, "_configure_backend")
        mock_configure_backend = mock_configure_backend_patch.start()
        setup_mock_file_keyring(mock_configure_backend, temp_dir, populate=populate)

        mock_configure_legacy_backend_patch: Any = None
        if setup_cryptfilekeyring is False:
            mock_configure_legacy_backend_patch = patch.object(KeyringWrapper, "_configure_legacy_backend")
            mock_configure_legacy_backend = mock_configure_legacy_backend_patch.start()
            mock_configure_legacy_backend.return_value = None

        mock_data_root_patch = patch.object(platform_, "data_root")
        mock_data_root = mock_data_root_patch.start()

        # Mock CryptFileKeyring's file_path indirectly by changing keyring.util.platform_.data_root
        # We don't want CryptFileKeyring finding the real legacy keyring
        mock_data_root.return_value = temp_dir

        if setup_cryptfilekeyring is True:
            crypt_file_keyring = create_empty_cryptfilekeyring()
            add_dummy_key_to_cryptfilekeyring(crypt_file_keyring)

        keychain = Keychain(user=user, service=service)
        keychain.keyring_wrapper = KeyringWrapper(keys_root_path=Path(temp_dir))

        # Stash the temp_dir in the keychain instance
        keychain._temp_dir = temp_dir  # type: ignore

        # Stash the patches in the keychain instance
        keychain._mock_supports_keyring_passphrase_patch = mock_supports_keyring_passphrase_patch  # type: ignore
        keychain._mock_supports_os_passphrase_storage_patch = mock_supports_os_passphrase_storage_patch  # type: ignore
        keychain._mock_configure_backend_patch = mock_configure_backend_patch  # type: ignore
        keychain._mock_configure_legacy_backend_patch = mock_configure_legacy_backend_patch  # type: ignore
        keychain._mock_data_root_patch = mock_data_root_patch  # type: ignore

        return keychain
Exemplo n.º 18
0
 def get_cached_master_passphrase() -> str:
     """
     Returns the cached master passphrase
     """
     passphrase, _ = KeyringWrapper.get_shared_instance(
     ).get_cached_master_passphrase()
     return passphrase
 def test_file_keyring_supports_master_passphrase(self):
     """
     File keyrings should support setting a master passphrase
     """
     # Expect: keyring supports a master passphrase
     assert KeyringWrapper.get_shared_instance(
     ).keyring_supports_master_passphrase() is True
Exemplo n.º 20
0
    def test_writer_lock_timeout(self):
        """
        If a writer lock is already held, another process should not be able to acquire
        the same lock, failing after n attempts
        """
        lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        lock = fasteners.InterProcessReaderWriterLock(str(lock_path))

        # When: a writer lock is already acquired
        lock.acquire_write_lock()

        child_proc_fn = dummy_fn_requiring_writer_lock
        timeout = 0.25
        attempts = 4

        with Pool(processes=1) as pool:
            # When: a child process attempts to acquire the same writer lock, failing after 1 second
            res = pool.starmap_async(child_writer_dispatch, [(child_proc_fn, lock_path, timeout, attempts)])

            # Expect: the child to fail acquiring the writer lock (raises as FileKeyringLockTimeout)
            with pytest.raises(FileKeyringLockTimeout):
                # 10 second timeout to prevent a bad test from spoiling the fun (raises as TimeoutException)
                res.get(timeout=10)

        lock.release_write_lock()
Exemplo n.º 21
0
 def __init__(self,
              user: Optional[str] = None,
              service: Optional[str] = None):
     self.user = user if user is not None else default_keychain_user()
     self.service = service if service is not None else default_keychain_service(
     )
     self.keyring_wrapper = KeyringWrapper.get_shared_instance()
Exemplo n.º 22
0
    def test_writer_lock_initially_blocked_by_readers(self):
        """
        When a reader lock is already held, another thread/process should not be able
        to acquire the lock for writing until the reader releases its lock
        """
        lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        lock = fasteners.InterProcessReaderWriterLock(str(lock_path))

        # When: a reader lock is already acquired
        assert lock.acquire_read_lock() is True

        child_proc_function = dummy_fn_requiring_writer_lock
        timeout = 1
        attempts = 4

        with Pool(processes=1) as pool:
            # When: a child process attempts to acquire the same lock for writing, failing after 4 seconds
            res = pool.starmap_async(child_writer_dispatch, [(child_proc_function, lock_path, timeout, attempts)])

            # When: we verify that the writer lock is not immediately acquired
            with pytest.raises(TimeoutError):
                res.get(timeout=1)

            # When: the reader releases its lock
            lock.release_read_lock()

            # Expect: the child process to acquire the writer lock
            result = res.get(timeout=10)  # 10 second timeout to prevent a bad test from spoiling the fun
            assert result[0] == "A winner is you!"
Exemplo n.º 23
0
    def test_writer_lock_released_on_abort(self):
        """
        When a child process is holding the lock and aborts/crashes, we should be
        able to acquire the lock
        """
        # Avoid running on macOS: calling abort() triggers the CrashReporter prompt, interfering with automated testing
        if platform == "darwin":
            return

        lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        lock = fasteners.InterProcessReaderWriterLock(str(lock_path))

        # When: a writer lock is already acquired
        lock.acquire_write_lock()

        child_proc_function = dummy_abort_fn
        timeout = 0.25
        attempts = 4

        with Pool(processes=1) as pool:
            # When: a child process attempts to acquire the same writer lock, failing after 1 second
            res = pool.starmap_async(child_writer_dispatch, [(child_proc_function, lock_path, timeout, attempts)])

            # When: the writer lock is released
            lock.release_write_lock()

            # When: timing out waiting for the child process (because it aborted)
            with pytest.raises(TimeoutError):
                res.get(timeout=1)

            # Expect: Reacquiring the lock should succeed after the child exits, automatically releasing the lock
            assert lock.acquire_write_lock(timeout=(1)) is True
Exemplo n.º 24
0
    def test_writer_lock_reacquisition_failure(self):
        """
        After the child process acquires the writer lock (and sleeps), the previous
        holder should not be able to quickly reacquire the lock
        """
        lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        lock = fasteners.InterProcessReaderWriterLock(str(lock_path))

        # When: a writer lock is already acquired
        lock.acquire_write_lock()

        child_proc_function = dummy_sleep_fn  # Sleeps for DUMMY_SLEEP_VALUE seconds
        timeout = 0.25
        attempts = 8

        with Pool(processes=1) as pool:
            # When: a child process attempts to acquire the same writer lock, failing after 1 second
            pool.starmap_async(child_writer_dispatch, [(child_proc_function, lock_path, timeout, attempts)])

            # When: the writer lock is released
            lock.release_write_lock()

            # Brief delay to allow the child to acquire the lock
            sleep(1)

            # Expect: Reacquiring the lock should fail due to the child holding the lock and sleeping
            assert lock.acquire_write_lock(timeout=0.25) is False
 def test_empty_file_keyring_doesnt_have_master_passphrase(self):
     """
     A new/unpopulated file keyring should not have a master passphrase set
     """
     # Expect: no master passphrase set
     assert KeyringWrapper.get_shared_instance().has_master_passphrase(
     ) is False
Exemplo n.º 26
0
def dummy_set_passphrase(service, user, passphrase, keyring_path, index,
                         num_workers):
    with TempKeyring(existing_keyring_path=keyring_path,
                     delete_on_cleanup=False):
        if platform == "linux" or platform == "win32" or platform == "cygwin":
            # FileKeyring's setup_keyring_file_watcher needs to be called explicitly here,
            # otherwise file events won't be detected in the child process
            KeyringWrapper.get_shared_instance(
            ).keyring.setup_keyring_file_watcher()

        # Write out a file indicating this process is ready to begin
        ready_file_path: Path = Path(
            keyring_path).parent / "ready" / f"{index}.ready"
        with open(ready_file_path, "w") as f:
            f.write(f"{os.getpid()}\n")

        # Wait up to 30 seconds for all processes to indicate readiness
        start_file_path: Path = Path(ready_file_path.parent) / "start"
        remaining_attempts = 120
        while remaining_attempts > 0:
            if start_file_path.exists():
                break
            else:
                sleep(0.25)
                remaining_attempts -= 1

        assert remaining_attempts >= 0

        KeyringWrapper.get_shared_instance().set_passphrase(
            service=service, user=user, passphrase=passphrase)

        found_passphrase = KeyringWrapper.get_shared_instance().get_passphrase(
            service, user)
        if found_passphrase != passphrase:
            log.error(
                f"[pid:{os.getpid()}] error: didn't get expected passphrase: "
                f"get_passphrase: {found_passphrase}"  # lgtm [py/clear-text-logging-sensitive-data]
                f", expected: {passphrase}"  # lgtm [py/clear-text-logging-sensitive-data]
            )

        # Write out a file indicating this process has completed its work
        finished_file_path: Path = Path(
            keyring_path).parent / "finished" / f"{index}.finished"
        with open(finished_file_path, "w") as f:
            f.write(f"{os.getpid()}\n")

        assert found_passphrase == passphrase
    def test_remove_master_passphrase_from_empty_keyring(self):
        """
        An empty keyring doesn't require a current passphrase to remove the master passphrase.
        Removing the master passphrase will set the default master passphrase on the keyring.
        """
        # When: removing the master passphrase from an empty keyring, current passphrase isn't necessary
        KeyringWrapper.get_shared_instance().remove_master_passphrase(None)

        # Expect: default master passphrase is set
        assert KeyringWrapper.get_shared_instance(
        ).get_cached_master_passphrase() == (
            DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE,
            True,
        )
        assert (
            KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
                DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE) is True)
Exemplo n.º 28
0
    def test_multiple_writers(self):
        num_workers = 20
        keyring_path = str(KeyringWrapper.get_shared_instance().keyring.keyring_path)
        passphrase_list = list(
            map(lambda x: ("test-service", f"test-user-{x}", f"passphrase {x}", keyring_path), range(num_workers))
        )

        # When: spinning off children to each set a passphrase concurrently
        with Pool(processes=num_workers) as pool:
            res = pool.starmap_async(dummy_set_passphrase, passphrase_list)
            res.get(timeout=10)  # 10 second timeout to prevent a bad test from spoiling the fun

        # Expect: parent process should be able to find all passphrases that were set by the child processes
        for item in passphrase_list:
            expected_passphrase = item[2]
            actual_passphrase = KeyringWrapper.get_shared_instance().get_passphrase(service=item[0], user=item[1])
            assert expected_passphrase == actual_passphrase
    def test_remove_master_passphrase_from_populated_keyring(self):
        """
        A populated keyring will require a current passphrase when removing the master passphrase.
        Removing the master passphrase will set the default master passphrase on the keyring.
        """
        # When: the master passphrase is set
        KeyringWrapper.get_shared_instance().set_master_passphrase(
            DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE,
            "It's dangerous to go alone, take this!")

        # When: removing the master passphrase
        KeyringWrapper.get_shared_instance().remove_master_passphrase(
            "It's dangerous to go alone, take this!")

        # Expect: default master passphrase is set, old passphrase doesn't validate
        assert KeyringWrapper.get_shared_instance(
        ).get_cached_master_passphrase() == (
            DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE,
            True,
        )
        assert (
            KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
                DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE) is True)
        assert (
            KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
                "It's dangerous to go alone, take this!") is False)
Exemplo n.º 30
0
 def master_passphrase_is_valid(passphrase: str,
                                force_reload: bool = False) -> bool:
     """
     Checks whether the provided passphrase can unlock the keyring. If force_reload
     is true, the keyring payload will be re-read from the backing file. If false,
     the passphrase will be checked against the in-memory payload.
     """
     return KeyringWrapper.get_shared_instance().master_passphrase_is_valid(
         passphrase, force_reload=force_reload)