From 2e2c297a802cb5f0e22508959929c98f15a9c976 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 18 Nov 2022 17:33:18 +0100 Subject: [PATCH] util: Remove legacy keyring support (#13398) --- build_scripts/check_dependency_artifacts.py | 1 - chia/cmds/chia.py | 7 - chia/cmds/keys.py | 15 - chia/cmds/keys_funcs.py | 91 +----- chia/cmds/passphrase.py | 8 +- chia/cmds/passphrase_funcs.py | 26 -- chia/cmds/start.py | 2 +- chia/cmds/start_funcs.py | 10 +- chia/daemon/client.py | 6 - chia/daemon/server.py | 88 +----- chia/simulator/keyring.py | 75 +---- chia/util/errors.py | 5 - chia/util/keychain.py | 46 +-- chia/util/keyring_wrapper.py | 316 +------------------- pylintrc | 1 - setup.py | 3 - tests/core/cmds/test_keys.py | 196 +----------- tests/core/util/test_keyring_wrapper.py | 94 +----- 18 files changed, 26 insertions(+), 964 deletions(-) diff --git a/build_scripts/check_dependency_artifacts.py b/build_scripts/check_dependency_artifacts.py index d02d7db5bc..7386a1c0ce 100644 --- a/build_scripts/check_dependency_artifacts.py +++ b/build_scripts/check_dependency_artifacts.py @@ -8,7 +8,6 @@ import sys import tempfile excepted_packages = { - "keyrings.cryptfile", # pure python "dnslib", # pure python } diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 618b6b090b..ae9de4b475 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -42,24 +42,17 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) "--keys-root-path", default=DEFAULT_KEYS_ROOT_PATH, help="Keyring file root", type=click.Path(), show_default=True ) @click.option("--passphrase-file", type=click.File("r"), help="File or descriptor to read the keyring passphrase from") -@click.option( - "--force-legacy-keyring-migration/--no-force-legacy-keyring-migration", - default=True, - help="Force legacy keyring migration. Legacy keyring support will be removed in an upcoming version!", -) @click.pass_context def cli( ctx: click.Context, root_path: str, keys_root_path: Optional[str] = None, passphrase_file: Optional[TextIOWrapper] = None, - force_legacy_keyring_migration: bool = True, ) -> None: from pathlib import Path ctx.ensure_object(dict) ctx.obj["root_path"] = Path(root_path) - ctx.obj["force_legacy_keyring_migration"] = force_legacy_keyring_migration # keys_root_path and passphrase_file will be None if the passphrase options have been # scrubbed from the CLI options diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index b001589bc9..e3f6f578ac 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -1,7 +1,5 @@ from __future__ import annotations -import asyncio -import sys from typing import Optional, Tuple import click @@ -13,15 +11,10 @@ def keys_cmd(ctx: click.Context): """Create, delete, view and use your key pairs""" from pathlib import Path - from .keys_funcs import migrate_keys - root_path: Path = ctx.obj["root_path"] if not root_path.is_dir(): raise RuntimeError("Please initialize (or migrate) your config directory with chia init") - if ctx.obj["force_legacy_keyring_migration"] and not asyncio.run(migrate_keys(root_path, True)): - sys.exit(1) - @keys_cmd.command("generate", short_help="Generates and adds a key to keychain") @click.option( @@ -226,14 +219,6 @@ def verify_cmd(message: str, public_key: str, signature: str): verify(message, public_key, signature) -@keys_cmd.command("migrate", short_help="Attempt to migrate keys to the Chia keyring") -@click.pass_context -def migrate_cmd(ctx: click.Context): - from .keys_funcs import migrate_keys - - asyncio.run(migrate_keys(ctx.obj["root_path"])) - - @keys_cmd.group("derive", short_help="Derive child keys or wallet addresses") @click.option( "--fingerprint", diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index d1ee9ee48c..9baf2d7f78 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import logging import os import sys from enum import Enum @@ -12,11 +11,9 @@ from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey from chia.cmds.passphrase_funcs import obtain_current_passphrase from chia.consensus.coinbase import create_puzzlehash_for_pk -from chia.daemon.client import connect_to_daemon_and_validate -from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate, wrap_local_keychain from chia.util.bech32m import encode_puzzle_hash from chia.util.config import load_config -from chia.util.errors import KeychainException, KeychainNotSet +from chia.util.errors import KeychainException from chia.util.file_keyring import MAX_LABEL_LENGTH from chia.util.ints import uint32 from chia.util.keychain import Keychain, bytes_to_mnemonic, generate_mnemonic, mnemonic_to_seed @@ -268,92 +265,6 @@ def verify(message: str, public_key: str, signature: str): print(AugSchemeMPL.verify(public_key, messageBytes, signature)) -async def migrate_keys(root_path: Path, forced: bool = False) -> bool: - from chia.util.keyring_wrapper import KeyringWrapper - from chia.util.misc import prompt_yes_no - - deprecation_message = ( - "\nLegacy keyring support is deprecated and will be removed in an upcoming version. " - "You need to migrate your keyring to continue using Chia.\n" - ) - - # Check if the keyring needs a full migration (i.e. if it's using the old keyring) - if Keychain.needs_migration(): - print(deprecation_message) - return await KeyringWrapper.get_shared_instance().migrate_legacy_keyring_interactive() - else: - already_checked_marker = KeyringWrapper.get_shared_instance().keys_root_path / ".checked_legacy_migration" - if forced and already_checked_marker.exists(): - return True - - log = logging.getLogger("migrate_keys") - config = load_config(root_path, "config.yaml") - # Connect to the daemon here first to see if ts running since `connect_to_keychain_and_validate` just tries to - # connect forever if it's not up. - keychain_proxy: Optional[KeychainProxy] = None - daemon = await connect_to_daemon_and_validate(root_path, config, quiet=True) - if daemon is not None: - await daemon.close() - keychain_proxy = await connect_to_keychain_and_validate(root_path, log) - if keychain_proxy is None: - keychain_proxy = wrap_local_keychain(Keychain(), log=log) - - try: - legacy_keyring = Keychain(force_legacy=True) - all_sks = await keychain_proxy.get_all_private_keys() - all_legacy_sks = legacy_keyring.get_all_private_keys() - set_legacy_sks = {str(x[0]) for x in all_legacy_sks} - set_sks = {str(x[0]) for x in all_sks} - missing_legacy_keys = set_legacy_sks - set_sks - keys_to_migrate = [x for x in all_legacy_sks if str(x[0]) in missing_legacy_keys] - except KeychainNotSet: - keys_to_migrate = [] - - if len(keys_to_migrate) > 0: - print(deprecation_message) - print(f"Found {len(keys_to_migrate)} key(s) that need migration:") - for key, _ in keys_to_migrate: - print(f"Fingerprint: {key.get_g1().get_fingerprint()}") - - print() - if not prompt_yes_no("Migrate these keys?"): - await keychain_proxy.close() - print("Migration aborted, can't run any chia commands.") - return False - - for sk, seed_bytes in keys_to_migrate: - mnemonic = bytes_to_mnemonic(seed_bytes) - await keychain_proxy.add_private_key(mnemonic) - fingerprint = sk.get_g1().get_fingerprint() - print(f"Added private key with public key fingerprint {fingerprint}") - - print(f"Migrated {len(keys_to_migrate)} key(s)") - - print("Verifying migration results...", end="") - all_sks = await keychain_proxy.get_all_private_keys() - await keychain_proxy.close() - set_sks = {str(x[0]) for x in all_sks} - keys_present = set_sks.issuperset(set(map(lambda x: str(x[0]), keys_to_migrate))) - if keys_present: - print(" Verified") - print() - response = prompt_yes_no("Remove key(s) from old keyring (recommended)?") - if response: - legacy_keyring.delete_keys(keys_to_migrate) - print(f"Removed {len(keys_to_migrate)} key(s) from old keyring") - print("Migration complete") - else: - print(" Failed") - return False - return True - elif not forced: - print("No keys need migration") - if already_checked_marker.parent.exists(): - already_checked_marker.touch() - await keychain_proxy.close() - return True - - def _clear_line_part(n: int): # Move backward, overwrite with spaces, then move backward again sys.stdout.write("\b" * n) diff --git a/chia/cmds/passphrase.py b/chia/cmds/passphrase.py index ed925865c2..2b1f693550 100644 --- a/chia/cmds/passphrase.py +++ b/chia/cmds/passphrase.py @@ -11,12 +11,8 @@ from chia.util.config import load_config @click.group("passphrase", short_help="Manage your keyring passphrase") -@click.pass_context -def passphrase_cmd(ctx: click.Context): - from .keys_funcs import migrate_keys - - if ctx.obj["force_legacy_keyring_migration"] and not asyncio.run(migrate_keys(ctx.obj["root_path"], True)): - sys.exit(1) +def passphrase_cmd(): + pass @passphrase_cmd.command( diff --git a/chia/cmds/passphrase_funcs.py b/chia/cmds/passphrase_funcs.py index f98fa70f12..98c5fe243c 100644 --- a/chia/cmds/passphrase_funcs.py +++ b/chia/cmds/passphrase_funcs.py @@ -8,16 +8,13 @@ from io import TextIOWrapper from pathlib import Path from typing import Any, Dict, Optional, Tuple -import click import colorama from chia.daemon.client import acquire_connection_to_daemon -from chia.util.config import load_config from chia.util.errors import KeychainMaxUnlockAttempts from chia.util.keychain import Keychain, supports_os_passphrase_storage from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper from chia.util.misc import prompt_yes_no -from chia.util.ws_message import WsRpcMessage DEFAULT_PASSPHRASE_PROMPT = ( colorama.Fore.YELLOW + colorama.Style.BRIGHT + "(Unlock Keyring)" + colorama.Style.RESET_ALL + " Passphrase: " @@ -348,26 +345,3 @@ async def async_update_daemon_passphrase_cache_if_running(root_path: Path, confi raise Exception(error) except Exception as e: print(f"Failed to notify daemon of updated keyring passphrase: {e}") - - -async def async_update_daemon_migration_completed_if_running() -> None: - """ - Attempt to connect to the daemon to notify that keyring migration has completed. - This allows the daemon to refresh its keyring so that it can stop using the - legacy keyring. - """ - ctx: click.Context = click.get_current_context() - root_path: Path = ctx.obj["root_path"] - - if root_path is None: - print("Missing root_path in context. Unable to notify daemon") - return None - - async with acquire_connection_to_daemon(root_path, load_config(root_path, "config.yaml"), quiet=True) as daemon: - if daemon is not None: - passphrase: str = Keychain.get_cached_master_passphrase() - - print("Updating daemon... ", end="") - response: WsRpcMessage = await daemon.notify_keyring_migration_completed(passphrase) - success: bool = response.get("data", {}).get("success", False) - print("succeeded" if success is True else "failed") diff --git a/chia/cmds/start.py b/chia/cmds/start.py index 0a5cc0b624..a674350642 100644 --- a/chia/cmds/start.py +++ b/chia/cmds/start.py @@ -20,4 +20,4 @@ def start_cmd(ctx: click.Context, restart: bool, group: str) -> None: root_path = ctx.obj["root_path"] config = load_config(root_path, "config.yaml") warn_if_beta_enabled(config) - asyncio.run(async_start(root_path, config, group, restart, ctx.obj["force_legacy_keyring_migration"])) + asyncio.run(async_start(root_path, config, group, restart)) diff --git a/chia/cmds/start_funcs.py b/chia/cmds/start_funcs.py index cb3f983bd6..ed33b80f9f 100644 --- a/chia/cmds/start_funcs.py +++ b/chia/cmds/start_funcs.py @@ -8,7 +8,6 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path from typing import Any, Dict, Optional -from chia.cmds.keys_funcs import migrate_keys from chia.cmds.passphrase_funcs import get_current_passphrase from chia.daemon.client import DaemonProxy, connect_to_daemon_and_validate from chia.util.errors import KeychainMaxUnlockAttempts @@ -51,9 +50,7 @@ async def create_start_daemon_connection(root_path: Path, config: Dict[str, Any] return None -async def async_start( - root_path: Path, config: Dict[str, Any], group: str, restart: bool, force_keyring_migration: bool -) -> None: +async def async_start(root_path: Path, config: Dict[str, Any], group: str, restart: bool) -> None: try: daemon = await create_start_daemon_connection(root_path, config) except KeychainMaxUnlockAttempts: @@ -64,11 +61,6 @@ async def async_start( print("Failed to create the chia daemon") return None - if force_keyring_migration: - if not await migrate_keys(root_path, True): - await daemon.close() - sys.exit(1) - for service in services_for_groups(group): if await daemon.is_running(service_name=service): print(f"{service}: ", end="", flush=True) diff --git a/chia/daemon/client.py b/chia/daemon/client.py index 99df01a12e..07195e6b74 100644 --- a/chia/daemon/client.py +++ b/chia/daemon/client.py @@ -128,12 +128,6 @@ class DaemonProxy: response = await self._get(request) return response - async def notify_keyring_migration_completed(self, passphrase: Optional[str]) -> WsRpcMessage: - data: Dict[str, Any] = {"key": passphrase} - request: WsRpcMessage = self.format_request("notify_keyring_migration_completed", data) - response: WsRpcMessage = await self._get(request) - return response - async def ping(self) -> WsRpcMessage: request = self.format_request("ping", {}) response = await self._get(request) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 407aa1d838..7a1a670ecf 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -29,7 +29,7 @@ from chia.ssl.create_ssl import get_mozilla_ca_crt from chia.util.beta_metrics import BetaMetricsLogger from chia.util.chia_logging import initialize_service_logging from chia.util.config import load_config -from chia.util.errors import KeychainCurrentPassphraseIsInvalid, KeychainRequiresMigration +from chia.util.errors import KeychainCurrentPassphraseIsInvalid from chia.util.json_util import dict_to_json_str from chia.util.keychain import Keychain, passphrase_requirements, supports_os_passphrase_storage from chia.util.lock import Lockfile, LockfileError @@ -345,14 +345,10 @@ class WebSocketServer: response = await self.unlock_keyring(data) elif command == "validate_keyring_passphrase": response = await self.validate_keyring_passphrase(data) - elif command == "migrate_keyring": - response = await self.migrate_keyring(data) elif command == "set_keyring_passphrase": response = await self.set_keyring_passphrase(data) elif command == "remove_keyring_passphrase": response = await self.remove_keyring_passphrase(data) - elif command == "notify_keyring_migration_completed": - response = await self.notify_keyring_migration_completed(data) elif command == "exit": response = await self.stop() elif command == "register_service": @@ -379,8 +375,6 @@ class WebSocketServer: can_save_passphrase: bool = supports_os_passphrase_storage() user_passphrase_is_set: bool = Keychain.has_master_passphrase() and not using_default_passphrase() locked: bool = Keychain.is_keyring_locked() - needs_migration: bool = Keychain.needs_migration() - can_remove_legacy_keys: bool = False # Disabling GUI support for removing legacy keys post-migration can_set_passphrase_hint: bool = True passphrase_hint: str = Keychain.get_master_passphrase_hint() or "" requirements: Dict[str, Any] = passphrase_requirements() @@ -389,8 +383,6 @@ class WebSocketServer: "is_keyring_locked": locked, "can_save_passphrase": can_save_passphrase, "user_passphrase_is_set": user_passphrase_is_set, - "needs_migration": needs_migration, - "can_remove_legacy_keys": can_remove_legacy_keys, "can_set_passphrase_hint": can_set_passphrase_hint, "passphrase_hint": passphrase_hint, "passphrase_requirements": requirements, @@ -448,54 +440,6 @@ class WebSocketServer: response: Dict[str, Any] = {"success": success, "error": error} return response - async def migrate_keyring(self, request: Dict[str, Any]) -> Dict[str, Any]: - if Keychain.needs_migration() is False: - # If the keyring has already been migrated, we'll raise an error to the client. - # The reason for raising an error is because the migration request has side- - # effects beyond copying keys from the legacy keyring to the new keyring. The - # request may have set a passphrase and indicated that keys should be cleaned - # from the legacy keyring. If we were to return early and indicate success, - # the client and user's expectations may not match reality (were my keys - # deleted from the legacy keyring? was my passphrase set?). - return {"success": False, "error": "migration not needed"} - - success: bool = False - error: Optional[str] = None - passphrase: Optional[str] = request.get("passphrase", None) - passphrase_hint: Optional[str] = request.get("passphrase_hint", None) - save_passphrase: bool = request.get("save_passphrase", False) - cleanup_legacy_keyring: bool = request.get("cleanup_legacy_keyring", False) - - if passphrase is not None and type(passphrase) is not str: - return {"success": False, "error": 'expected string value for "passphrase"'} - - if passphrase_hint is not None and type(passphrase_hint) is not str: - return {"success": False, "error": 'expected string value for "passphrase_hint"'} - - if not Keychain.passphrase_meets_requirements(passphrase): - return {"success": False, "error": "passphrase doesn't satisfy requirements"} - - if type(cleanup_legacy_keyring) is not bool: - return {"success": False, "error": 'expected bool value for "cleanup_legacy_keyring"'} - - try: - Keychain.migrate_legacy_keyring( - passphrase=passphrase, - passphrase_hint=passphrase_hint, - save_passphrase=save_passphrase, - cleanup_legacy_keyring=cleanup_legacy_keyring, - ) - success = True - # Inform the GUI of keyring status changes - self.keyring_status_changed(await self.keyring_status(), "wallet_ui") - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Legacy keyring migration failed: {e} {tb}") - error = f"keyring migration failed: {e}" - - response: Dict[str, Any] = {"success": success, "error": error} - return response - async def set_keyring_passphrase(self, request: Dict[str, Any]) -> Dict[str, Any]: success: bool = False error: Optional[str] = None @@ -527,8 +471,6 @@ class WebSocketServer: passphrase_hint=passphrase_hint, save_passphrase=save_passphrase, ) - except KeychainRequiresMigration: - error = "keyring requires migration" except KeychainCurrentPassphraseIsInvalid: error = "current passphrase is invalid" except Exception as e: @@ -569,32 +511,6 @@ class WebSocketServer: response: Dict[str, Any] = {"success": success, "error": error} return response - async def notify_keyring_migration_completed(self, request: Dict[str, Any]) -> Dict[str, Any]: - success: bool = False - error: Optional[str] = None - key: Optional[str] = request.get("key", None) - - if type(key) is not str: - return {"success": False, "error": "missing key"} - - Keychain.handle_migration_completed() - - try: - if Keychain.master_passphrase_is_valid(key, force_reload=True): - Keychain.set_cached_master_passphrase(key) - success = True - # Inform the GUI of keyring status changes - self.keyring_status_changed(await self.keyring_status(), "wallet_ui") - else: - error = "bad passphrase" - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Keyring passphrase validation failed: {e} {tb}") - error = "validation exception" - - response: Dict[str, Any] = {"success": success, "error": error} - return response - def get_status(self) -> Dict[str, Any]: response = {"success": True, "genesis_initialized": True} return response @@ -611,7 +527,7 @@ class WebSocketServer: async def _keyring_status_changed(self, keyring_status: Dict[str, Any], destination: str): """ Attempt to communicate with the GUI to inform it of any keyring status changes - (e.g. keyring becomes unlocked or migration completes) + (e.g. keyring becomes unlocked) """ websockets = self.connections.get("wallet_ui", None) diff --git a/chia/simulator/keyring.py b/chia/simulator/keyring.py index 8c059e1c5d..e775add6f4 100644 --- a/chia/simulator/keyring.py +++ b/chia/simulator/keyring.py @@ -5,38 +5,15 @@ import shutil import tempfile from functools import wraps from pathlib import Path -from typing import Any, Optional +from typing import Optional from unittest.mock import patch -from keyring.util import platform_ -from keyrings.cryptfile.cryptfile import CryptFileKeyring # pyright: reportMissingImports=false - from chia.util.file_keyring import FileKeyring, keyring_path_from_root -from chia.util.keychain import Keychain, default_keychain_service, default_keychain_user, get_private_key_user +from chia.util.keychain import Keychain from chia.util.keyring_wrapper import KeyringWrapper -def create_empty_cryptfilekeyring() -> CryptFileKeyring: - """ - Create an empty legacy keyring - """ - crypt_file_keyring = CryptFileKeyring() - fd = os.open(crypt_file_keyring.file_path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600) - os.close(fd) - assert Path(crypt_file_keyring.file_path).exists() - return crypt_file_keyring - - -def add_dummy_key_to_cryptfilekeyring(crypt_file_keyring: CryptFileKeyring) -> None: - """ - Add a fake key to the CryptFileKeyring - """ - crypt_file_keyring.keyring_key = "your keyring password" - user: str = get_private_key_user(default_keychain_user(), 0) - crypt_file_keyring.set_password(default_keychain_service(), user, "abc123") - - -def setup_mock_file_keyring(mock_configure_backend, temp_file_keyring_dir, populate=False) -> None: +def setup_mock_file_keyring(mock_configure_backend, temp_file_keyring_dir, populate=False): if populate: # Populate the file keyring with an empty (but encrypted) data set file_keyring_path = keyring_path_from_root(Path(temp_file_keyring_dir)) @@ -80,24 +57,6 @@ def using_temp_file_keyring(populate: bool = False): return outer -def using_temp_file_keyring_and_cryptfilekeyring(populate: bool = False): - """ - Like the `using_temp_file_keyring` decorator, this decorator will create a temp - dir and temp keyring. Additionally, an empty legacy Cryptfile keyring will be - created in the temp directory. - """ - - def outer(method): - @wraps(method) - def inner(self, *args, **kwargs): - with TempKeyring(populate=populate, setup_cryptfilekeyring=True): - return method(self, *args, **kwargs) - - return inner - - return outer - - class TempKeyring: def __init__( self, @@ -105,7 +64,6 @@ class TempKeyring: user: str = "testing-1.8.0", service: str = "testing-chia-1.8.0", populate: bool = False, - setup_cryptfilekeyring: bool = False, existing_keyring_path: Optional[str] = None, delete_on_cleanup: bool = True, use_os_credential_store: bool = False, @@ -116,7 +74,6 @@ class TempKeyring: populate=populate, existing_keyring_path=existing_keyring_path, use_os_credential_store=use_os_credential_store, - setup_cryptfilekeyring=setup_cryptfilekeyring, ) self.old_keys_root_path = None self.delete_on_cleanup = delete_on_cleanup @@ -128,7 +85,6 @@ class TempKeyring: user: str, service: str, populate: bool, - setup_cryptfilekeyring: bool, existing_keyring_path: Optional[str], use_os_credential_store: bool, ): @@ -145,23 +101,6 @@ class TempKeyring: 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)) @@ -171,8 +110,6 @@ class TempKeyring: # Stash the patches in the keychain instance 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 @@ -200,12 +137,6 @@ class TempKeyring: self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher() shutil.rmtree(self.keychain._temp_dir) - self.keychain._mock_supports_os_passphrase_storage_patch.stop() - self.keychain._mock_configure_backend_patch.stop() - if self.keychain._mock_configure_legacy_backend_patch is not None: - self.keychain._mock_configure_legacy_backend_patch.stop() - self.keychain._mock_data_root_patch.stop() - if self.old_keys_root_path is not None: if KeyringWrapper.get_shared_instance(create_if_necessary=False) is not None: shared_keys_root_path = KeyringWrapper.get_shared_instance().keys_root_path diff --git a/chia/util/errors.py b/chia/util/errors.py index 4e79c02b70..ae947a2a37 100644 --- a/chia/util/errors.py +++ b/chia/util/errors.py @@ -200,11 +200,6 @@ class KeychainSecretsMissing(KeychainException): pass -class KeychainRequiresMigration(KeychainException): - def __init__(self) -> None: - super().__init__("Keychain requires migration") - - class KeychainCurrentPassphraseIsInvalid(KeychainException): def __init__(self) -> None: super().__init__("Invalid current passphrase") diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 44c7501125..8d6e0775ba 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -258,16 +258,14 @@ class Keychain: list of all keys. """ - def __init__(self, user: Optional[str] = None, service: Optional[str] = None, force_legacy: bool = False): + 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() - keyring_wrapper: Optional[KeyringWrapper] = ( - KeyringWrapper.get_legacy_instance() if force_legacy else KeyringWrapper.get_shared_instance() - ) + keyring_wrapper: Optional[KeyringWrapper] = KeyringWrapper.get_shared_instance() if keyring_wrapper is None: - raise KeychainNotSet(f"KeyringWrapper not set: force_legacy={force_legacy}") + raise KeychainNotSet("KeyringWrapper not set") self.keyring_wrapper = keyring_wrapper @@ -493,44 +491,6 @@ class Keychain: # Locked: Everything else return True - @staticmethod - def needs_migration() -> bool: - """ - Returns a bool indicating whether the underlying keyring needs to be migrated to the new - format for passphrase support. - """ - return KeyringWrapper.get_shared_instance().using_legacy_keyring() - - @staticmethod - 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() - - @staticmethod - def migrate_legacy_keyring( - passphrase: Optional[str] = None, - passphrase_hint: Optional[str] = None, - save_passphrase: bool = False, - 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, - passphrase_hint=passphrase_hint, - save_passphrase=save_passphrase, - ) - - KeyringWrapper.get_shared_instance().migrate_legacy_keyring(cleanup_legacy_keyring=cleanup_legacy_keyring) - @staticmethod def passphrase_is_optional() -> bool: """ diff --git a/chia/util/keyring_wrapper.py b/chia/util/keyring_wrapper.py index 7212c9fa2a..6f9fdde190 100644 --- a/chia/util/keyring_wrapper.py +++ b/chia/util/keyring_wrapper.py @@ -2,17 +2,14 @@ from __future__ import annotations from pathlib import Path from sys import platform -from typing import Any, List, Optional, Tuple, Type, Union +from typing import Optional, Tuple, Union -from blspy import PrivateKey # pyright: reportMissingImports=false from keyring.backends.macOS import Keyring as MacKeyring from keyring.backends.Windows import WinVaultKeyring as WinKeyring from keyring.errors import KeyringError, PasswordDeleteError -from keyrings.cryptfile.cryptfile import CryptFileKeyring # pyright: reportMissingImports=false from chia.util.default_root import DEFAULT_KEYS_ROOT_PATH from chia.util.file_keyring import FileKeyring -from chia.util.misc import prompt_yes_no # We want to protect the keyring, even if a user-specified master passphrase isn't provided # @@ -25,22 +22,9 @@ MASTER_PASSPHRASE_SERVICE_NAME = "Chia Passphrase" MASTER_PASSPHRASE_USER_NAME = "Chia Passphrase" -LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring] OSPassphraseStore = Union[MacKeyring, WinKeyring] -def get_legacy_keyring_instance() -> Optional[LegacyKeyring]: - if platform == "darwin": - return MacKeyring() - elif platform == "win32" or platform == "cygwin": - return WinKeyring() - elif platform == "linux": - keyring: CryptFileKeyring = CryptFileKeyring() - keyring.keyring_key = "your keyring password" - return keyring - return None - - def get_os_passphrase_store() -> Optional[OSPassphraseStore]: if platform == "darwin": return MacKeyring() @@ -49,22 +33,6 @@ def get_os_passphrase_store() -> Optional[OSPassphraseStore]: return None -def check_legacy_keyring_keys_present(keyring: LegacyKeyring) -> bool: - from keyring.credentials import Credential - - from chia.util.keychain import MAX_KEYS, default_keychain_service, default_keychain_user, get_private_key_user - - keychain_user: str = default_keychain_user() - keychain_service: str = default_keychain_service() - - for index in range(0, MAX_KEYS): - current_user: str = get_private_key_user(keychain_user, index) - credential: Optional[Credential] = keyring.get_credential(keychain_service, current_user) - if credential is not None: - return True - return False - - def warn_if_macos_errSecInteractionNotAllowed(error: KeyringError) -> bool: """ Check if the macOS Keychain error is errSecInteractionNotAllowed. This commonly @@ -90,8 +58,7 @@ class KeyringWrapper: a keyring backend is selected based on the OS. The wrapper is implemented as a singleton, as it may need to manage state - related to the master passphrase and handle migration from the legacy - CryptFileKeyring implementation. + related to the master passphrase. """ # Static members @@ -100,57 +67,24 @@ class KeyringWrapper: # Instance members keys_root_path: Path - keyring: Union[Any, FileKeyring] = None + keyring: FileKeyring cached_passphrase: Optional[str] = None cached_passphrase_is_validated: bool = False - legacy_keyring = None - def __init__(self, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH, force_legacy: bool = False): + def __init__(self, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH): """ - Initializes the keyring backend based on the OS. For Linux, we previously - used CryptFileKeyring. We now use our own FileKeyring backend and migrate - the data from the legacy CryptFileKeyring (on write). + Initializes the keyring backend. """ - from chia.util.errors import KeychainNotSet self.keys_root_path = keys_root_path - if force_legacy: - legacy_keyring = get_legacy_keyring_instance() - if legacy_keyring is not None and check_legacy_keyring_keys_present(legacy_keyring): - self.legacy_keyring = legacy_keyring - else: - self.refresh_keyrings() - - if self.keyring is None and self.legacy_keyring is None: - raise KeychainNotSet( - f"Unable to initialize keyring backend: keys_root_path={keys_root_path}, force_legacy={force_legacy}" - ) - - def refresh_keyrings(self): - self.keyring = None self.keyring = self._configure_backend() - # Configure the legacy keyring if keyring passphrases are supported to support migration (if necessary) - self.legacy_keyring = self._configure_legacy_backend() - # Initialize the cached_passphrase self.cached_passphrase = self._get_initial_cached_passphrase() def _configure_backend(self) -> FileKeyring: - if self.keyring: - raise Exception("KeyringWrapper has already been instantiated") return FileKeyring.create(keys_root_path=self.keys_root_path) - def _configure_legacy_backend(self) -> LegacyKeyring: - # If keyring.yaml isn't found or is empty, check if we're using - # CryptFileKeyring, Mac Keychain, or Windows Credential Manager - filekeyring = self.keyring if type(self.keyring) == FileKeyring else None - if filekeyring and not filekeyring.has_content(): - keyring: Optional[LegacyKeyring] = get_legacy_keyring_instance() - if keyring is not None and check_legacy_keyring_keys_present(keyring): - return keyring - return None - def _get_initial_cached_passphrase(self) -> str: """ Grab the saved passphrase from the OS credential store (if available), otherwise @@ -186,24 +120,14 @@ class KeyringWrapper: def cleanup_shared_instance() -> None: KeyringWrapper.__shared_instance = None - @staticmethod - def get_legacy_instance() -> Optional["KeyringWrapper"]: - return KeyringWrapper(force_legacy=True) - def get_keyring(self): """ - Return the current keyring backend. The legacy keyring is preferred if it's in use + Return the current keyring backend. """ - return self.keyring if not self.using_legacy_keyring() else self.legacy_keyring - - def using_legacy_keyring(self) -> bool: - return self.legacy_keyring is not None + return self.keyring # Master passphrase support - def keyring_supports_master_passphrase(self) -> bool: - return type(self.get_keyring()) in [FileKeyring] - def get_cached_master_passphrase(self) -> Tuple[Optional[str], bool]: """ Returns a tuple including the currently cached passphrase and a bool @@ -228,7 +152,7 @@ class KeyringWrapper: Returns a bool indicating whether the underlying keyring data is secured by a master passphrase. """ - return self.keyring_supports_master_passphrase() and self.keyring.has_content() + return self.keyring.has_content() def master_passphrase_is_valid(self, passphrase: str, force_reload: bool = False) -> bool: return self.keyring.check_passphrase(passphrase, force_reload=force_reload) @@ -245,7 +169,7 @@ class KeyringWrapper: """ Sets a new master passphrase for the keyring """ - from chia.util.errors import KeychainCurrentPassphraseIsInvalid, KeychainRequiresMigration + from chia.util.errors import KeychainCurrentPassphraseIsInvalid from chia.util.keychain import supports_os_passphrase_storage # Require a valid current_passphrase @@ -261,8 +185,6 @@ class KeyringWrapper: self.keyring.set_passphrase_hint(passphrase_hint) if write_to_keyring: - if self.using_legacy_keyring(): - raise KeychainRequiresMigration() # We're reencrypting the keyring contents using the new passphrase. Ensure that the # payload has been decrypted by calling load_keyring with the current passphrase. self.keyring.load_keyring(passphrase=current_passphrase) @@ -318,220 +240,11 @@ class KeyringWrapper: return None def get_master_passphrase_hint(self) -> Optional[str]: - if self.keyring_supports_master_passphrase(): - return self.keyring.get_passphrase_hint() - return None - - # Legacy keyring migration - - class MigrationResults: - def __init__( - self, - original_private_keys: List[Tuple[PrivateKey, bytes]], - legacy_keyring: LegacyKeyring, - keychain_service: str, - keychain_users: List[str], - ): - self.original_private_keys = original_private_keys - self.legacy_keyring = legacy_keyring - self.keychain_service = keychain_service - self.keychain_users = keychain_users - - def confirm_migration(self) -> bool: - """ - Before beginning migration, we'll notify the user that the legacy keyring needs to be - migrated and warn about backing up the mnemonic seeds. - - If a master passphrase hasn't been explicitly set yet, we'll attempt to prompt and set - the passphrase prior to beginning migration. - """ - - master_passphrase, _ = self.get_cached_master_passphrase() - if master_passphrase == DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE: - print( - "\nYour existing keys will be migrated to a new keyring that is optionally secured by a master " - "passphrase." - ) - print( - "Would you like to set a master passphrase now? Use 'chia passphrase set' to change the passphrase.\n" - ) - - response = prompt_yes_no("Set keyring master passphrase?") - if response: - from chia.cmds.passphrase_funcs import prompt_for_new_passphrase - - # Prompt for a master passphrase and cache it - new_passphrase, save_passphrase = prompt_for_new_passphrase() - self.set_master_passphrase( - current_passphrase=None, - new_passphrase=new_passphrase, - write_to_keyring=False, - save_passphrase=save_passphrase, - ) - else: - print( - "Will skip setting a master passphrase. Use 'chia passphrase set' to set the master passphrase.\n" - ) - else: - import colorama - - colorama.init() - - print("\nYour existing keys will be migrated to a new keyring that is secured by your master passphrase") - print(colorama.Fore.YELLOW + colorama.Style.BRIGHT + "WARNING: " + colorama.Style.RESET_ALL, end="") - print( - "It is strongly recommended that you ensure you have a copy of the mnemonic seed for each of your " - "keys prior to beginning migration\n" - ) - - return prompt_yes_no("Begin keyring migration?") - - def migrate_legacy_keys(self) -> MigrationResults: - from chia.util.keychain import MAX_KEYS, Keychain, get_private_key_user - - print("Migrating contents from legacy keyring") - - keychain: Keychain = Keychain() - # Obtain contents from the legacy keyring. When using the Keychain interface - # to read, the legacy keyring will be preferred over the new keyring. - original_private_keys = keychain.get_all_private_keys() - service = keychain.service - user_passphrase_pairs = [] - index = 0 - user = get_private_key_user(keychain.user, index) - while index <= MAX_KEYS: - # Build up a list of user/passphrase tuples from the legacy keyring contents - if user is not None: - passphrase = self.get_passphrase(service, user) - - if passphrase is not None: - user_passphrase_pairs.append((user, passphrase)) - - index += 1 - user = get_private_key_user(keychain.user, index) - - # Write the keys directly to the new keyring (self.keyring) - for (user, passphrase) in user_passphrase_pairs: - self.keyring.set_password(service, user, passphrase) - - return KeyringWrapper.MigrationResults( - original_private_keys, self.legacy_keyring, service, [user for (user, _) in user_passphrase_pairs] - ) - - def verify_migration_results(self, migration_results: MigrationResults) -> bool: - from chia.util.keychain import Keychain - - # Stop using the legacy keyring. This will direct subsequent reads to the new keyring. - self.legacy_keyring = None - success: bool = False - - print("Verifying migration results...", end="") - - # Compare the original keyring contents with the new - try: - keychain: Keychain = Keychain() - original_private_keys = migration_results.original_private_keys - post_migration_private_keys = keychain.get_all_private_keys() - - # Sort the key collections prior to comparing - original_private_keys.sort(key=lambda e: str(e[0])) - post_migration_private_keys.sort(key=lambda e: str(e[0])) - - if post_migration_private_keys == original_private_keys: - success = True - print(" Verified") - else: - print(" Failed") - raise ValueError("Migrated keys don't match original keys") - except Exception as e: - print(f"\nMigration failed: {e}") - print("Leaving legacy keyring intact") - self.legacy_keyring = migration_results.legacy_keyring # Restore the legacy keyring - raise - - return success - - def confirm_legacy_keyring_cleanup(self, migration_results) -> bool: - """ - Ask the user whether we should remove keys from the legacy keyring. In the case - of CryptFileKeyring, we can't just delete the file because other python processes - might use the same keyring file. - """ - keyring_name: str = "" - legacy_keyring_type: Type = type(migration_results.legacy_keyring) - - if legacy_keyring_type is CryptFileKeyring: - keyring_name = str(migration_results.legacy_keyring.file_path) - elif legacy_keyring_type is MacKeyring: - keyring_name = "macOS Keychain" - elif legacy_keyring_type is WinKeyring: - keyring_name = "Windows Credential Manager" - - prompt = "Remove keys from old keyring (recommended)" - if len(keyring_name) > 0: - prompt += f" ({keyring_name})?" - else: - prompt += "?" - return prompt_yes_no(prompt) - - def cleanup_legacy_keyring(self, migration_results: MigrationResults): - for user in migration_results.keychain_users: - migration_results.legacy_keyring.delete_password(migration_results.keychain_service, user) - - def migrate_legacy_keyring(self, cleanup_legacy_keyring: bool = False): - results = self.migrate_legacy_keys() - success = self.verify_migration_results(results) - - if success and cleanup_legacy_keyring: - self.cleanup_legacy_keyring(results) - - async def migrate_legacy_keyring_interactive(self) -> bool: - """ - Handle importing keys from the legacy keyring into the new keyring. - - Prior to beginning, we'll ensure that we at least suggest setting a master passphrase - and backing up mnemonic seeds. After importing keys from the legacy keyring, we'll - perform a before/after comparison of the keyring contents, and on success we'll prompt - to cleanup the legacy keyring. - """ - from chia.cmds.passphrase_funcs import async_update_daemon_migration_completed_if_running - - # Let the user know about the migration. - if not self.confirm_migration(): - print("Migration aborted, can't run any chia commands.") - return False - - try: - results = self.migrate_legacy_keys() - success = self.verify_migration_results(results) - - if success: - print(f"Keyring migration completed successfully ({str(self.keyring.keyring_path)})\n") - except Exception as e: - print(f"\nMigration failed: {e}") - print("Leaving legacy keyring intact") - return False - - # Ask if we should clean up the legacy keyring - if self.confirm_legacy_keyring_cleanup(results): - self.cleanup_legacy_keyring(results) - print("Removed keys from old keyring") - else: - print("Keys in old keyring left intact") - - # Notify the daemon (if running) that migration has completed - await async_update_daemon_migration_completed_if_running() - return True + return self.keyring.get_passphrase_hint() # Keyring interface def get_passphrase(self, service: str, user: str) -> str: - # Continue reading from the legacy keyring until we want to write something, - # at which point we'll migrate the legacy contents to the new keyring - if self.using_legacy_keyring(): - passphrase = self.legacy_keyring.get_password(service, user) # type: ignore - return passphrase.hex() if type(passphrase) == bytes else passphrase - return self.get_keyring().get_password(service, user) def set_passphrase(self, service: str, user: str, passphrase: str): @@ -541,19 +254,10 @@ class KeyringWrapper: self.get_keyring().delete_password(service, user) def get_label(self, fingerprint: int) -> Optional[str]: - if self.using_legacy_keyring(): - return None # Legacy keyring doesn't support key labels - return self.keyring.get_label(fingerprint) def set_label(self, fingerprint: int, label: str) -> None: - if self.using_legacy_keyring(): - raise NotImplementedError("Legacy keyring doesn't support key labels") - self.keyring.set_label(fingerprint, label) def delete_label(self, fingerprint: int) -> None: - if self.using_legacy_keyring(): - raise NotImplementedError("Legacy keyring doesn't support key labels") - self.keyring.delete_label(fingerprint) diff --git a/pylintrc b/pylintrc index 8982b9d1cd..1038e9a6cd 100644 --- a/pylintrc +++ b/pylintrc @@ -186,7 +186,6 @@ ignored-modules=blspy, cryptography, aiohttp, keyring, - keyrings.cryptfile, bitstring, clvm_tools, clvm_tools_rs, diff --git a/setup.py b/setup.py index fc652a9c01..df40518929 100644 --- a/setup.py +++ b/setup.py @@ -23,9 +23,6 @@ dependencies = [ "cryptography==38.0.3", # Python cryptography library for TLS - keyring conflict "filelock==3.8.0", # For reading and writing config multiprocess and multithread safely (non-reentrant locks) "keyring==23.9.3", # Store keys in MacOS Keychain, Windows Credential Locker - "keyrings.cryptfile==1.3.4", # Secure storage for keys on Linux (Will be replaced) - # "keyrings.cryptfile==1.3.8", # Secure storage for keys on Linux (Will be replaced) - # See https://github.com/frispete/keyrings.cryptfile/issues/15 "PyYAML==6.0", # Used for config file format "setproctitle==1.2.3", # Gives the chia processes readable names "sortedcontainers==2.4.0", # For maintaining sorted mempools diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index 15ad614a20..1233190620 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -6,11 +6,9 @@ import re from chia.cmds.chia import cli from chia.cmds.keys import delete_all_cmd, generate_and_print_cmd, sign_cmd, verify_cmd from chia.util.config import load_config -from chia.util.file_keyring import FileKeyring -from chia.util.keychain import KeyData, DEFAULT_USER, DEFAULT_SERVICE, Keychain, generate_mnemonic -from chia.util.keyring_wrapper import DEFAULT_KEYS_ROOT_PATH, KeyringWrapper, LegacyKeyring +from chia.util.keychain import KeyData, Keychain, generate_mnemonic +from chia.util.keyring_wrapper import DEFAULT_KEYS_ROOT_PATH, KeyringWrapper from click.testing import CliRunner, Result -from keyring.backend import KeyringBackend from pathlib import Path from typing import Dict, List, Optional @@ -23,46 +21,6 @@ TEST_MNEMONIC_SEED = ( TEST_FINGERPRINT = 2877570395 -class DummyLegacyKeyring(KeyringBackend): - - # Fingerprint 2474840988 - KEY_0 = ( - "89e29e5f9c3105b2a853475cab2392468cbfb1d65c3faabea8ebc78fe903fd279e56a8d93f6325fc6c3d833a2ae74832" - "b8feaa3d6ee49998f43ce303b66dcc5abb633e5c1d80efe85c40766135e4a44c" - ) - - # Fingerprint 4149609062 - KEY_1 = ( - "8b0d72288727af6238fcd9b0a663cd7d4728738fca597d0046cbb42b6432e0a5ae8026683fc5f9c73df26fb3e1cec2c8" - "ad1b4f601107d96a99f6fa9b9d2382918fb1e107fb6655c7bdd8c77c1d9c201f" - ) - - # Fingerprint 3618811800 - KEY_2 = ( - "8b2a26ba319f83bd3da5b1b147a817ecc4ca557f037c9db1cfedc59b16ee6880971b7d292f023358710a292c8db0eb82" - "35808f914754ae24e493fad9bc7f654b0f523fb406973af5235256a39bed1283" - ) - - def __init__(self, populate: bool = True): - self.service_dict = {} - - if populate: - self.service_dict[DEFAULT_SERVICE] = { - f"wallet-{DEFAULT_USER}-0": DummyLegacyKeyring.KEY_0, - f"wallet-{DEFAULT_USER}-1": DummyLegacyKeyring.KEY_1, - f"wallet-{DEFAULT_USER}-2": DummyLegacyKeyring.KEY_2, - } - - def get_password(self, service, username, password=None): - return self.service_dict.get(service, {}).get(username) - - def set_password(self, service, username, password): - self.service_dict.setdefault(service, {})[username] = password - - def delete_password(self, service, username): - del self.service_dict[service][username] - - @pytest.fixture(scope="function") def keyring_with_one_key(empty_keyring): keychain = empty_keyring @@ -88,22 +46,6 @@ def setup_keyringwrapper(tmp_path): KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) -@pytest.fixture(scope="function") -def setup_legacy_keyringwrapper(tmp_path, monkeypatch): - def mock_setup_keyring_file_watcher(_): - pass - - # Silence errors in the watchdog module during testing - monkeypatch.setattr(FileKeyring, "setup_keyring_file_watcher", mock_setup_keyring_file_watcher) - - KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(tmp_path) - KeyringWrapper.get_shared_instance().legacy_keyring = DummyLegacyKeyring() - yield - KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) - - def assert_label(keychain: Keychain, label: Optional[str], index: int) -> None: all_keys = keychain.get_keys() assert len(all_keys) > index @@ -134,7 +76,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -180,7 +121,6 @@ class TestKeysCommands: generate_result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -208,7 +148,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -250,7 +189,6 @@ class TestKeysCommands: keychain = empty_keyring keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -268,7 +206,6 @@ class TestKeysCommands: keychain = keyring_with_one_key keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -296,7 +233,6 @@ class TestKeysCommands: keychain = keyring_with_one_key keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -321,7 +257,6 @@ class TestKeysCommands: runner = CliRunner() keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -362,7 +297,6 @@ class TestKeysCommands: keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -389,7 +323,6 @@ class TestKeysCommands: keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -418,7 +351,6 @@ class TestKeysCommands: keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -447,7 +379,6 @@ class TestKeysCommands: keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -485,7 +416,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -519,7 +449,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -555,7 +484,6 @@ class TestKeysCommands: add_result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -575,7 +503,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -768,7 +695,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -827,7 +753,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -876,7 +801,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -966,7 +890,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -1017,7 +940,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -1076,7 +998,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -1137,7 +1058,6 @@ class TestKeysCommands: result: Result = runner.invoke( cli, [ - "--no-force-legacy-keyring-migration", "--root-path", os.fspath(tmp_path), "--keys-root-path", @@ -1195,115 +1115,3 @@ class TestKeysCommands: ) != -1 ) - - def test_migration_not_needed(self, tmp_path, setup_keyringwrapper, monkeypatch): - """ - Test the `chia keys migrate` command when no migration is necessary - """ - keys_root_path = KeyringWrapper.get_shared_instance().keys_root_path - runner = CliRunner() - init_result = runner.invoke( - cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] - ) - assert init_result.exit_code == 0 - - def mock_keychain_needs_migration() -> bool: - return False - - monkeypatch.setattr(Keychain, "needs_migration", mock_keychain_needs_migration) - - runner = CliRunner() - result: Result = runner.invoke( - cli, - [ - "--root-path", - os.fspath(tmp_path), - "keys", - "migrate", - ], - ) - - assert result.exit_code == 0 - assert result.output.find("No keys need migration") != -1 - - def test_migration_full(self, tmp_path, setup_legacy_keyringwrapper): - """ - Test the `chia keys migrate` command when a full migration is needed - """ - - legacy_keyring = KeyringWrapper.get_shared_instance().legacy_keyring - - assert legacy_keyring is not None - assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 - - runner = CliRunner() - init_result: Result = runner.invoke( - cli, - ["--root-path", os.fspath(tmp_path), "init"], - ) - - assert init_result.exit_code == 0 - - runner = CliRunner() - result: Result = runner.invoke( - cli, - [ - "--root-path", - os.fspath(tmp_path), - "keys", - "migrate", - ], - input="n\ny\ny\n", # Prompts: 'n' = don't set a passphrase, 'y' = begin migration, 'y' = remove legacy keys - ) - - assert result.exit_code == 0 - assert KeyringWrapper.get_shared_instance().using_legacy_keyring() is False # legacy keyring unset - assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set - assert len(Keychain().get_all_public_keys()) == 3 # new keyring has 3 keys - assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 0 # legacy keys removed - - def test_migration_incremental(self, tmp_path, keyring_with_one_key, monkeypatch): - KeyringWrapper.set_keys_root_path(tmp_path) - KeyringWrapper.cleanup_shared_instance() - - keychain = keyring_with_one_key - legacy_keyring = DummyLegacyKeyring() - - def mock_get_legacy_keyring_instance() -> Optional[LegacyKeyring]: - nonlocal legacy_keyring - return legacy_keyring - - from chia.util import keyring_wrapper - - monkeypatch.setattr(keyring_wrapper, "get_legacy_keyring_instance", mock_get_legacy_keyring_instance) - - assert len(keychain.get_all_private_keys()) == 1 - assert keychain.keyring_wrapper.legacy_keyring is None - assert legacy_keyring is not None - assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 - - runner = CliRunner() - init_result: Result = runner.invoke( - cli, - ["--root-path", os.fspath(tmp_path), "init"], - ) - - assert init_result.exit_code == 0 - - runner = CliRunner() - result: Result = runner.invoke( - cli, - [ - "--root-path", - os.fspath(tmp_path), - "keys", - "migrate", - ], - input="y\ny\n", # Prompts: 'y' = migrate keys, 'y' = remove legacy keys - ) - - assert result.exit_code == 0 - assert KeyringWrapper.get_shared_instance().using_legacy_keyring() is False # legacy keyring is not set - assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set - assert len(Keychain().get_all_public_keys()) == 4 # new keyring has 4 keys - assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 0 # legacy keys removed diff --git a/tests/core/util/test_keyring_wrapper.py b/tests/core/util/test_keyring_wrapper.py index 00c5b693a7..276ce4a391 100644 --- a/tests/core/util/test_keyring_wrapper.py +++ b/tests/core/util/test_keyring_wrapper.py @@ -3,10 +3,8 @@ import pytest from chia.util.errors import KeychainLabelError, KeychainLabelExists, KeychainFingerprintNotFound, KeychainLabelInvalid from chia.util.keyring_wrapper import KeyringWrapper, DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE -from pathlib import Path from typing import Type -from sys import platform -from chia.simulator.keyring import using_temp_file_keyring, using_temp_file_keyring_and_cryptfilekeyring +from chia.simulator.keyring import using_temp_file_keyring log = logging.getLogger(__name__) @@ -39,84 +37,6 @@ class TestKeyringWrapper: # Expect: the shared instance should be cleared assert KeyringWrapper.get_shared_instance(create_if_necessary=False) is None - # When: creating a new file keyring with a legacy keyring in place - @using_temp_file_keyring_and_cryptfilekeyring() - @pytest.mark.skip(reason="Does only work if `test_keyring_wrapper.py` gets called separately.") - def test_using_legacy_cryptfilekeyring(self): - """ - In the case that an existing CryptFileKeyring (legacy) keyring exists and we're - creating a new FileKeyring, the legacy keyring's use should be prioritized over - the FileKeyring (until migration is triggered by a write to the keyring.) - """ - - if platform != "linux": - return - - # Expect: the new keyring should not have content (not actually empty though...) - assert KeyringWrapper.get_shared_instance().keyring.has_content() is False - assert Path(KeyringWrapper.get_shared_instance().keyring.keyring_path).exists() is True - assert Path(KeyringWrapper.get_shared_instance().keyring.keyring_path).stat().st_size != 0 - - # Expect: legacy keyring should be in use - 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().get_keyring() == KeyringWrapper.get_shared_instance().legacy_keyring - - # When: a file keyring has content and the legacy keyring exists - @using_temp_file_keyring_and_cryptfilekeyring(populate=True) - 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 - - # When: a file keyring has content and the legacy keyring doesn't exists - @using_temp_file_keyring(populate=True) - 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 - - # When: a file keyring is empty/unpopulated and the legacy keyring doesn't exists - @using_temp_file_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 - - # When: using a file keyring - @using_temp_file_keyring() - 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 - # When: creating a new/unpopulated file keyring @using_temp_file_keyring() def test_empty_file_keyring_doesnt_have_master_passphrase(self): @@ -135,18 +55,6 @@ class TestKeyringWrapper: # Expect: master passphrase is set assert KeyringWrapper.get_shared_instance().has_master_passphrase() is True - # When: creating a new file keyring with a legacy keyring in place - @pytest.mark.xfail(reason="wasn't running, fails now, to be removed soon") - @using_temp_file_keyring_and_cryptfilekeyring() - 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 - # When: creating a new file keyring @using_temp_file_keyring() def test_default_cached_master_passphrase(self):