util: Remove legacy keyring support (#13398)

This commit is contained in:
dustinface 2022-11-18 17:33:18 +01:00 committed by GitHub
parent 22a1d1b1c2
commit 2e2c297a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 26 additions and 964 deletions

View File

@ -8,7 +8,6 @@ import sys
import tempfile
excepted_packages = {
"keyrings.cryptfile", # pure python
"dnslib", # pure python
}

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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(

View File

@ -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")

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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:
"""

View File

@ -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)

View File

@ -186,7 +186,6 @@ ignored-modules=blspy,
cryptography,
aiohttp,
keyring,
keyrings.cryptfile,
bitstring,
clvm_tools,
clvm_tools_rs,

View File

@ -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

View File

@ -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

View File

@ -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):