util: Remove legacy keyring support (#13398)
This commit is contained in:
parent
22a1d1b1c2
commit
2e2c297a80
|
@ -8,7 +8,6 @@ import sys
|
|||
import tempfile
|
||||
|
||||
excepted_packages = {
|
||||
"keyrings.cryptfile", # pure python
|
||||
"dnslib", # pure python
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
1
pylintrc
1
pylintrc
|
@ -186,7 +186,6 @@ ignored-modules=blspy,
|
|||
cryptography,
|
||||
aiohttp,
|
||||
keyring,
|
||||
keyrings.cryptfile,
|
||||
bitstring,
|
||||
clvm_tools,
|
||||
clvm_tools_rs,
|
||||
|
|
3
setup.py
3
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue