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 import tempfile
excepted_packages = { excepted_packages = {
"keyrings.cryptfile", # pure python
"dnslib", # 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 "--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("--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 @click.pass_context
def cli( def cli(
ctx: click.Context, ctx: click.Context,
root_path: str, root_path: str,
keys_root_path: Optional[str] = None, keys_root_path: Optional[str] = None,
passphrase_file: Optional[TextIOWrapper] = None, passphrase_file: Optional[TextIOWrapper] = None,
force_legacy_keyring_migration: bool = True,
) -> None: ) -> None:
from pathlib import Path from pathlib import Path
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj["root_path"] = Path(root_path) 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 # keys_root_path and passphrase_file will be None if the passphrase options have been
# scrubbed from the CLI options # scrubbed from the CLI options

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import sys
from typing import Optional, Tuple from typing import Optional, Tuple
import click import click
@ -13,15 +11,10 @@ def keys_cmd(ctx: click.Context):
"""Create, delete, view and use your key pairs""" """Create, delete, view and use your key pairs"""
from pathlib import Path from pathlib import Path
from .keys_funcs import migrate_keys
root_path: Path = ctx.obj["root_path"] root_path: Path = ctx.obj["root_path"]
if not root_path.is_dir(): if not root_path.is_dir():
raise RuntimeError("Please initialize (or migrate) your config directory with chia init") 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") @keys_cmd.command("generate", short_help="Generates and adds a key to keychain")
@click.option( @click.option(
@ -226,14 +219,6 @@ def verify_cmd(message: str, public_key: str, signature: str):
verify(message, public_key, signature) 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") @keys_cmd.group("derive", short_help="Derive child keys or wallet addresses")
@click.option( @click.option(
"--fingerprint", "--fingerprint",

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import json import json
import logging
import os import os
import sys import sys
from enum import Enum 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.cmds.passphrase_funcs import obtain_current_passphrase
from chia.consensus.coinbase import create_puzzlehash_for_pk 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.bech32m import encode_puzzle_hash
from chia.util.config import load_config 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.file_keyring import MAX_LABEL_LENGTH
from chia.util.ints import uint32 from chia.util.ints import uint32
from chia.util.keychain import Keychain, bytes_to_mnemonic, generate_mnemonic, mnemonic_to_seed 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)) 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): def _clear_line_part(n: int):
# Move backward, overwrite with spaces, then move backward again # Move backward, overwrite with spaces, then move backward again
sys.stdout.write("\b" * n) 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.group("passphrase", short_help="Manage your keyring passphrase")
@click.pass_context def passphrase_cmd():
def passphrase_cmd(ctx: click.Context): pass
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)
@passphrase_cmd.command( @passphrase_cmd.command(

View File

@ -8,16 +8,13 @@ from io import TextIOWrapper
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
import click
import colorama import colorama
from chia.daemon.client import acquire_connection_to_daemon 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.errors import KeychainMaxUnlockAttempts
from chia.util.keychain import Keychain, supports_os_passphrase_storage 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.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper
from chia.util.misc import prompt_yes_no from chia.util.misc import prompt_yes_no
from chia.util.ws_message import WsRpcMessage
DEFAULT_PASSPHRASE_PROMPT = ( DEFAULT_PASSPHRASE_PROMPT = (
colorama.Fore.YELLOW + colorama.Style.BRIGHT + "(Unlock Keyring)" + colorama.Style.RESET_ALL + " Passphrase: " 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) raise Exception(error)
except Exception as e: except Exception as e:
print(f"Failed to notify daemon of updated keyring passphrase: {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"] root_path = ctx.obj["root_path"]
config = load_config(root_path, "config.yaml") config = load_config(root_path, "config.yaml")
warn_if_beta_enabled(config) 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 pathlib import Path
from typing import Any, Dict, Optional 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.cmds.passphrase_funcs import get_current_passphrase
from chia.daemon.client import DaemonProxy, connect_to_daemon_and_validate from chia.daemon.client import DaemonProxy, connect_to_daemon_and_validate
from chia.util.errors import KeychainMaxUnlockAttempts 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 return None
async def async_start( async def async_start(root_path: Path, config: Dict[str, Any], group: str, restart: bool) -> None:
root_path: Path, config: Dict[str, Any], group: str, restart: bool, force_keyring_migration: bool
) -> None:
try: try:
daemon = await create_start_daemon_connection(root_path, config) daemon = await create_start_daemon_connection(root_path, config)
except KeychainMaxUnlockAttempts: except KeychainMaxUnlockAttempts:
@ -64,11 +61,6 @@ async def async_start(
print("Failed to create the chia daemon") print("Failed to create the chia daemon")
return None 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): for service in services_for_groups(group):
if await daemon.is_running(service_name=service): if await daemon.is_running(service_name=service):
print(f"{service}: ", end="", flush=True) print(f"{service}: ", end="", flush=True)

View File

@ -128,12 +128,6 @@ class DaemonProxy:
response = await self._get(request) response = await self._get(request)
return response 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: async def ping(self) -> WsRpcMessage:
request = self.format_request("ping", {}) request = self.format_request("ping", {})
response = await self._get(request) 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.beta_metrics import BetaMetricsLogger
from chia.util.chia_logging import initialize_service_logging from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config 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.json_util import dict_to_json_str
from chia.util.keychain import Keychain, passphrase_requirements, supports_os_passphrase_storage from chia.util.keychain import Keychain, passphrase_requirements, supports_os_passphrase_storage
from chia.util.lock import Lockfile, LockfileError from chia.util.lock import Lockfile, LockfileError
@ -345,14 +345,10 @@ class WebSocketServer:
response = await self.unlock_keyring(data) response = await self.unlock_keyring(data)
elif command == "validate_keyring_passphrase": elif command == "validate_keyring_passphrase":
response = await self.validate_keyring_passphrase(data) response = await self.validate_keyring_passphrase(data)
elif command == "migrate_keyring":
response = await self.migrate_keyring(data)
elif command == "set_keyring_passphrase": elif command == "set_keyring_passphrase":
response = await self.set_keyring_passphrase(data) response = await self.set_keyring_passphrase(data)
elif command == "remove_keyring_passphrase": elif command == "remove_keyring_passphrase":
response = await self.remove_keyring_passphrase(data) response = await self.remove_keyring_passphrase(data)
elif command == "notify_keyring_migration_completed":
response = await self.notify_keyring_migration_completed(data)
elif command == "exit": elif command == "exit":
response = await self.stop() response = await self.stop()
elif command == "register_service": elif command == "register_service":
@ -379,8 +375,6 @@ class WebSocketServer:
can_save_passphrase: bool = supports_os_passphrase_storage() can_save_passphrase: bool = supports_os_passphrase_storage()
user_passphrase_is_set: bool = Keychain.has_master_passphrase() and not using_default_passphrase() user_passphrase_is_set: bool = Keychain.has_master_passphrase() and not using_default_passphrase()
locked: bool = Keychain.is_keyring_locked() 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 can_set_passphrase_hint: bool = True
passphrase_hint: str = Keychain.get_master_passphrase_hint() or "" passphrase_hint: str = Keychain.get_master_passphrase_hint() or ""
requirements: Dict[str, Any] = passphrase_requirements() requirements: Dict[str, Any] = passphrase_requirements()
@ -389,8 +383,6 @@ class WebSocketServer:
"is_keyring_locked": locked, "is_keyring_locked": locked,
"can_save_passphrase": can_save_passphrase, "can_save_passphrase": can_save_passphrase,
"user_passphrase_is_set": user_passphrase_is_set, "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, "can_set_passphrase_hint": can_set_passphrase_hint,
"passphrase_hint": passphrase_hint, "passphrase_hint": passphrase_hint,
"passphrase_requirements": requirements, "passphrase_requirements": requirements,
@ -448,54 +440,6 @@ class WebSocketServer:
response: Dict[str, Any] = {"success": success, "error": error} response: Dict[str, Any] = {"success": success, "error": error}
return response 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]: async def set_keyring_passphrase(self, request: Dict[str, Any]) -> Dict[str, Any]:
success: bool = False success: bool = False
error: Optional[str] = None error: Optional[str] = None
@ -527,8 +471,6 @@ class WebSocketServer:
passphrase_hint=passphrase_hint, passphrase_hint=passphrase_hint,
save_passphrase=save_passphrase, save_passphrase=save_passphrase,
) )
except KeychainRequiresMigration:
error = "keyring requires migration"
except KeychainCurrentPassphraseIsInvalid: except KeychainCurrentPassphraseIsInvalid:
error = "current passphrase is invalid" error = "current passphrase is invalid"
except Exception as e: except Exception as e:
@ -569,32 +511,6 @@ class WebSocketServer:
response: Dict[str, Any] = {"success": success, "error": error} response: Dict[str, Any] = {"success": success, "error": error}
return response 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]: def get_status(self) -> Dict[str, Any]:
response = {"success": True, "genesis_initialized": True} response = {"success": True, "genesis_initialized": True}
return response return response
@ -611,7 +527,7 @@ class WebSocketServer:
async def _keyring_status_changed(self, keyring_status: Dict[str, Any], destination: str): 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 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) websockets = self.connections.get("wallet_ui", None)

View File

@ -5,38 +5,15 @@ import shutil
import tempfile import tempfile
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Optional
from unittest.mock import patch 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.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 from chia.util.keyring_wrapper import KeyringWrapper
def create_empty_cryptfilekeyring() -> CryptFileKeyring: def setup_mock_file_keyring(mock_configure_backend, temp_file_keyring_dir, populate=False):
"""
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:
if populate: if populate:
# Populate the file keyring with an empty (but encrypted) data set # Populate the file keyring with an empty (but encrypted) data set
file_keyring_path = keyring_path_from_root(Path(temp_file_keyring_dir)) 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 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: class TempKeyring:
def __init__( def __init__(
self, self,
@ -105,7 +64,6 @@ class TempKeyring:
user: str = "testing-1.8.0", user: str = "testing-1.8.0",
service: str = "testing-chia-1.8.0", service: str = "testing-chia-1.8.0",
populate: bool = False, populate: bool = False,
setup_cryptfilekeyring: bool = False,
existing_keyring_path: Optional[str] = None, existing_keyring_path: Optional[str] = None,
delete_on_cleanup: bool = True, delete_on_cleanup: bool = True,
use_os_credential_store: bool = False, use_os_credential_store: bool = False,
@ -116,7 +74,6 @@ class TempKeyring:
populate=populate, populate=populate,
existing_keyring_path=existing_keyring_path, existing_keyring_path=existing_keyring_path,
use_os_credential_store=use_os_credential_store, use_os_credential_store=use_os_credential_store,
setup_cryptfilekeyring=setup_cryptfilekeyring,
) )
self.old_keys_root_path = None self.old_keys_root_path = None
self.delete_on_cleanup = delete_on_cleanup self.delete_on_cleanup = delete_on_cleanup
@ -128,7 +85,6 @@ class TempKeyring:
user: str, user: str,
service: str, service: str,
populate: bool, populate: bool,
setup_cryptfilekeyring: bool,
existing_keyring_path: Optional[str], existing_keyring_path: Optional[str],
use_os_credential_store: bool, use_os_credential_store: bool,
): ):
@ -145,23 +101,6 @@ class TempKeyring:
mock_configure_backend = mock_configure_backend_patch.start() mock_configure_backend = mock_configure_backend_patch.start()
setup_mock_file_keyring(mock_configure_backend, temp_dir, populate=populate) 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 = Keychain(user=user, service=service)
keychain.keyring_wrapper = KeyringWrapper(keys_root_path=Path(temp_dir)) keychain.keyring_wrapper = KeyringWrapper(keys_root_path=Path(temp_dir))
@ -171,8 +110,6 @@ class TempKeyring:
# Stash the patches in the keychain instance # Stash the patches in the keychain instance
keychain._mock_supports_os_passphrase_storage_patch = mock_supports_os_passphrase_storage_patch # type: ignore keychain._mock_supports_os_passphrase_storage_patch = mock_supports_os_passphrase_storage_patch # type: ignore
keychain._mock_configure_backend_patch = mock_configure_backend_patch # type: ignore keychain._mock_configure_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 return keychain
@ -200,12 +137,6 @@ class TempKeyring:
self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher() self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher()
shutil.rmtree(self.keychain._temp_dir) 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 self.old_keys_root_path is not None:
if KeyringWrapper.get_shared_instance(create_if_necessary=False) 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 shared_keys_root_path = KeyringWrapper.get_shared_instance().keys_root_path

View File

@ -200,11 +200,6 @@ class KeychainSecretsMissing(KeychainException):
pass pass
class KeychainRequiresMigration(KeychainException):
def __init__(self) -> None:
super().__init__("Keychain requires migration")
class KeychainCurrentPassphraseIsInvalid(KeychainException): class KeychainCurrentPassphraseIsInvalid(KeychainException):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("Invalid current passphrase") super().__init__("Invalid current passphrase")

View File

@ -258,16 +258,14 @@ class Keychain:
list of all keys. 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.user = user if user is not None else default_keychain_user()
self.service = service if service is not None else default_keychain_service() self.service = service if service is not None else default_keychain_service()
keyring_wrapper: Optional[KeyringWrapper] = ( keyring_wrapper: Optional[KeyringWrapper] = KeyringWrapper.get_shared_instance()
KeyringWrapper.get_legacy_instance() if force_legacy else KeyringWrapper.get_shared_instance()
)
if keyring_wrapper is None: 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 self.keyring_wrapper = keyring_wrapper
@ -493,44 +491,6 @@ class Keychain:
# Locked: Everything else # Locked: Everything else
return True 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 @staticmethod
def passphrase_is_optional() -> bool: def passphrase_is_optional() -> bool:
""" """

View File

@ -2,17 +2,14 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from sys import platform 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.macOS import Keyring as MacKeyring
from keyring.backends.Windows import WinVaultKeyring as WinKeyring from keyring.backends.Windows import WinVaultKeyring as WinKeyring
from keyring.errors import KeyringError, PasswordDeleteError 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.default_root import DEFAULT_KEYS_ROOT_PATH
from chia.util.file_keyring import FileKeyring 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 # 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" MASTER_PASSPHRASE_USER_NAME = "Chia Passphrase"
LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring]
OSPassphraseStore = Union[MacKeyring, WinKeyring] 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]: def get_os_passphrase_store() -> Optional[OSPassphraseStore]:
if platform == "darwin": if platform == "darwin":
return MacKeyring() return MacKeyring()
@ -49,22 +33,6 @@ def get_os_passphrase_store() -> Optional[OSPassphraseStore]:
return None 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: def warn_if_macos_errSecInteractionNotAllowed(error: KeyringError) -> bool:
""" """
Check if the macOS Keychain error is errSecInteractionNotAllowed. This commonly 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. a keyring backend is selected based on the OS.
The wrapper is implemented as a singleton, as it may need to manage state 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 related to the master passphrase.
CryptFileKeyring implementation.
""" """
# Static members # Static members
@ -100,57 +67,24 @@ class KeyringWrapper:
# Instance members # Instance members
keys_root_path: Path keys_root_path: Path
keyring: Union[Any, FileKeyring] = None keyring: FileKeyring
cached_passphrase: Optional[str] = None cached_passphrase: Optional[str] = None
cached_passphrase_is_validated: bool = False 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 Initializes the keyring backend.
used CryptFileKeyring. We now use our own FileKeyring backend and migrate
the data from the legacy CryptFileKeyring (on write).
""" """
from chia.util.errors import KeychainNotSet
self.keys_root_path = keys_root_path 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() 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 # Initialize the cached_passphrase
self.cached_passphrase = self._get_initial_cached_passphrase() self.cached_passphrase = self._get_initial_cached_passphrase()
def _configure_backend(self) -> FileKeyring: 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) 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: def _get_initial_cached_passphrase(self) -> str:
""" """
Grab the saved passphrase from the OS credential store (if available), otherwise Grab the saved passphrase from the OS credential store (if available), otherwise
@ -186,24 +120,14 @@ class KeyringWrapper:
def cleanup_shared_instance() -> None: def cleanup_shared_instance() -> None:
KeyringWrapper.__shared_instance = None KeyringWrapper.__shared_instance = None
@staticmethod
def get_legacy_instance() -> Optional["KeyringWrapper"]:
return KeyringWrapper(force_legacy=True)
def get_keyring(self): 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 return self.keyring
def using_legacy_keyring(self) -> bool:
return self.legacy_keyring is not None
# Master passphrase support # 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]: def get_cached_master_passphrase(self) -> Tuple[Optional[str], bool]:
""" """
Returns a tuple including the currently cached passphrase and a 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 Returns a bool indicating whether the underlying keyring data
is secured by a master passphrase. 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: def master_passphrase_is_valid(self, passphrase: str, force_reload: bool = False) -> bool:
return self.keyring.check_passphrase(passphrase, force_reload=force_reload) return self.keyring.check_passphrase(passphrase, force_reload=force_reload)
@ -245,7 +169,7 @@ class KeyringWrapper:
""" """
Sets a new master passphrase for the keyring 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 from chia.util.keychain import supports_os_passphrase_storage
# Require a valid current_passphrase # Require a valid current_passphrase
@ -261,8 +185,6 @@ class KeyringWrapper:
self.keyring.set_passphrase_hint(passphrase_hint) self.keyring.set_passphrase_hint(passphrase_hint)
if write_to_keyring: if write_to_keyring:
if self.using_legacy_keyring():
raise KeychainRequiresMigration()
# We're reencrypting the keyring contents using the new passphrase. Ensure that the # 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. # payload has been decrypted by calling load_keyring with the current passphrase.
self.keyring.load_keyring(passphrase=current_passphrase) self.keyring.load_keyring(passphrase=current_passphrase)
@ -318,220 +240,11 @@ class KeyringWrapper:
return None return None
def get_master_passphrase_hint(self) -> Optional[str]: def get_master_passphrase_hint(self) -> Optional[str]:
if self.keyring_supports_master_passphrase(): return self.keyring.get_passphrase_hint()
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
# Keyring interface # Keyring interface
def get_passphrase(self, service: str, user: str) -> str: 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) return self.get_keyring().get_password(service, user)
def set_passphrase(self, service: str, user: str, passphrase: str): def set_passphrase(self, service: str, user: str, passphrase: str):
@ -541,19 +254,10 @@ class KeyringWrapper:
self.get_keyring().delete_password(service, user) self.get_keyring().delete_password(service, user)
def get_label(self, fingerprint: int) -> Optional[str]: 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) return self.keyring.get_label(fingerprint)
def set_label(self, fingerprint: int, label: str) -> None: 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) self.keyring.set_label(fingerprint, label)
def delete_label(self, fingerprint: int) -> None: 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) self.keyring.delete_label(fingerprint)

View File

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

View File

@ -23,9 +23,6 @@ dependencies = [
"cryptography==38.0.3", # Python cryptography library for TLS - keyring conflict "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) "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 "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 "PyYAML==6.0", # Used for config file format
"setproctitle==1.2.3", # Gives the chia processes readable names "setproctitle==1.2.3", # Gives the chia processes readable names
"sortedcontainers==2.4.0", # For maintaining sorted mempools "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.chia import cli
from chia.cmds.keys import delete_all_cmd, generate_and_print_cmd, sign_cmd, verify_cmd 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.config import load_config
from chia.util.file_keyring import FileKeyring from chia.util.keychain import KeyData, Keychain, generate_mnemonic
from chia.util.keychain import KeyData, DEFAULT_USER, DEFAULT_SERVICE, Keychain, generate_mnemonic from chia.util.keyring_wrapper import DEFAULT_KEYS_ROOT_PATH, KeyringWrapper
from chia.util.keyring_wrapper import DEFAULT_KEYS_ROOT_PATH, KeyringWrapper, LegacyKeyring
from click.testing import CliRunner, Result from click.testing import CliRunner, Result
from keyring.backend import KeyringBackend
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
@ -23,46 +21,6 @@ TEST_MNEMONIC_SEED = (
TEST_FINGERPRINT = 2877570395 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") @pytest.fixture(scope="function")
def keyring_with_one_key(empty_keyring): def keyring_with_one_key(empty_keyring):
keychain = empty_keyring keychain = empty_keyring
@ -88,22 +46,6 @@ def setup_keyringwrapper(tmp_path):
KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_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: def assert_label(keychain: Keychain, label: Optional[str], index: int) -> None:
all_keys = keychain.get_keys() all_keys = keychain.get_keys()
assert len(all_keys) > index assert len(all_keys) > index
@ -134,7 +76,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -180,7 +121,6 @@ class TestKeysCommands:
generate_result: Result = runner.invoke( generate_result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -208,7 +148,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -250,7 +189,6 @@ class TestKeysCommands:
keychain = empty_keyring keychain = empty_keyring
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -268,7 +206,6 @@ class TestKeysCommands:
keychain = keyring_with_one_key keychain = keyring_with_one_key
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -296,7 +233,6 @@ class TestKeysCommands:
keychain = keyring_with_one_key keychain = keyring_with_one_key
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -321,7 +257,6 @@ class TestKeysCommands:
runner = CliRunner() runner = CliRunner()
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -362,7 +297,6 @@ class TestKeysCommands:
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -389,7 +323,6 @@ class TestKeysCommands:
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -418,7 +351,6 @@ class TestKeysCommands:
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -447,7 +379,6 @@ class TestKeysCommands:
keys_root_path = keychain.keyring_wrapper.keys_root_path keys_root_path = keychain.keyring_wrapper.keys_root_path
base_params = [ base_params = [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -485,7 +416,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -519,7 +449,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -555,7 +484,6 @@ class TestKeysCommands:
add_result: Result = runner.invoke( add_result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -575,7 +503,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -768,7 +695,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -827,7 +753,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -876,7 +801,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -966,7 +890,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -1017,7 +940,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -1076,7 +998,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -1137,7 +1058,6 @@ class TestKeysCommands:
result: Result = runner.invoke( result: Result = runner.invoke(
cli, cli,
[ [
"--no-force-legacy-keyring-migration",
"--root-path", "--root-path",
os.fspath(tmp_path), os.fspath(tmp_path),
"--keys-root-path", "--keys-root-path",
@ -1195,115 +1115,3 @@ class TestKeysCommands:
) )
!= -1 != -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.errors import KeychainLabelError, KeychainLabelExists, KeychainFingerprintNotFound, KeychainLabelInvalid
from chia.util.keyring_wrapper import KeyringWrapper, DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE from chia.util.keyring_wrapper import KeyringWrapper, DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE
from pathlib import Path
from typing import Type from typing import Type
from sys import platform from chia.simulator.keyring import using_temp_file_keyring
from chia.simulator.keyring import using_temp_file_keyring, using_temp_file_keyring_and_cryptfilekeyring
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -39,84 +37,6 @@ class TestKeyringWrapper:
# Expect: the shared instance should be cleared # Expect: the shared instance should be cleared
assert KeyringWrapper.get_shared_instance(create_if_necessary=False) is None 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 # When: creating a new/unpopulated file keyring
@using_temp_file_keyring() @using_temp_file_keyring()
def test_empty_file_keyring_doesnt_have_master_passphrase(self): def test_empty_file_keyring_doesnt_have_master_passphrase(self):
@ -135,18 +55,6 @@ class TestKeyringWrapper:
# Expect: master passphrase is set # Expect: master passphrase is set
assert KeyringWrapper.get_shared_instance().has_master_passphrase() is True 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 # When: creating a new file keyring
@using_temp_file_keyring() @using_temp_file_keyring()
def test_default_cached_master_passphrase(self): def test_default_cached_master_passphrase(self):