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