expands recovery CLI into three commands for keystore identification and auditing

pull/3554/head
KPrasch 2024-08-13 18:55:21 +02:00 committed by derekpierre
parent 98e3b37365
commit 9c345a97df
No known key found for this signature in database
3 changed files with 104 additions and 62 deletions

View File

@ -1,5 +1,4 @@
import os
from pathlib import Path
import click
from constant_sorrow.constants import NO_PASSWORD
@ -95,7 +94,7 @@ def collect_mnemonic(emitter: StdoutEmitter) -> str:
return __words
def recover_keystore(emitter) -> Path:
def recover_keystore(emitter) -> Keystore:
emitter.message('This procedure will recover your nucypher keystore from mnemonic seed words. '
'You will need to provide the entire mnemonic (space seperated) in the correct '
'order and choose a new password.', color='cyan')
@ -104,4 +103,4 @@ def recover_keystore(emitter) -> Path:
__password = get_nucypher_password(emitter=emitter, confirm=True)
keystore = Keystore.restore(words=__words, password=__password)
emitter.message(f'Recovered nucypher keystore {keystore.id} to \n {keystore.keystore_path}', color='green')
return keystore.keystore_path
return keystore

View File

@ -1,4 +1,3 @@
import json
from pathlib import Path
import click
@ -392,35 +391,25 @@ def init(general_config, config_options, force, config_root, key_material):
@ursula.command()
@option_config_file
@click.option(
"--check",
help="Check a mnemonic phrase against a keystore",
is_flag=True,
default=False,
)
@click.option(
"--keystore-filepath",
help="Path to keystore .priv file",
type=EXISTING_READABLE_FILE,
required=False,
)
def recover(config_file, check, keystore_filepath):
# TODO: Combine with work in PR #2682
@click.option(
"--view-mnemonic",
help="View mnemonic seed words",
is_flag=True,
)
def audit(config_file, keystore_filepath, view_mnemonic):
"""Audit a mnemonic phrase against a local keystore or view mnemonic seed words."""
emitter = StdoutEmitter()
if check and config_file:
raise click.BadOptionUsage(
"--check",
message=click.style(
"--check is incompatible with --config-file",
fg="red",
),
)
if not check and keystore_filepath:
if keystore_filepath and config_file:
raise click.BadOptionUsage(
"--keystore-filepath",
message=click.style(
"--keystore-filepath is only compatible with --check",
"--keystore-filepath is incompatible with --config-file",
fg="red",
),
)
@ -430,35 +419,66 @@ def recover(config_file, check, keystore_filepath):
emitter.error(f"Ursula configuration file not found - {config_file.absolute()}")
click.Abort()
if check:
if keystore_filepath:
keystore = Keystore(keystore_filepath)
else:
ursula_config = UrsulaConfiguration.from_configuration_file(
filepath=config_file
)
keystore = ursula_config.keystore
password = get_nucypher_password(emitter=emitter, confirm=False)
try:
keystore.unlock(password=password)
except Keystore.AuthenticationFailed:
emitter.message("Password is incorrect.", color="red")
return
emitter.message("Password is correct.", color="green")
correct = keystore.check(words=collect_mnemonic(emitter), password=password)
emitter.message(
f"Mnemonic is {'' if correct else 'in'}correct.",
color="green" if correct else "red",
if keystore_filepath:
keystore = Keystore(keystore_filepath)
else:
ursula_config = UrsulaConfiguration.from_configuration_file(
filepath=config_file
)
keystore = ursula_config.keystore
password = get_nucypher_password(emitter=emitter, confirm=False)
try:
keystore.unlock(password=password)
except Keystore.AuthenticationFailed:
emitter.message("Password is incorrect.", color="red")
return
emitter.message("Password is correct.", color="green")
if view_mnemonic:
mnemonic = keystore.get_mnemonic()
emitter.message(f"\n{mnemonic}", color="cyan")
return
# Recover keystore
new_keystore_path = recover_keystore(emitter=emitter)
try:
correct = keystore.audit(words=collect_mnemonic(emitter), password=password)
except Keystore.InvalidMnemonic:
emitter.message("Mnemonic is incorrect.", color="red")
emitter.message(
f"Mnemonic is {'' if correct else 'in'}correct.",
color="green" if correct else "red",
)
@ursula.command()
@option_config_file
@click.option(
"--keystore-filepath",
help="Path to keystore .priv file",
type=EXISTING_READABLE_FILE,
required=False,
)
def recover(config_file, keystore_filepath):
# TODO: Combine with work in PR #2682
emitter = StdoutEmitter()
if keystore_filepath and config_file:
raise click.BadOptionUsage(
"--keystore-filepath",
message=click.style(
"--keystore-filepath is incompatible with --config-file",
fg="red",
),
)
config_file = config_file or DEFAULT_CONFIG_FILEPATH
if not config_file.exists():
emitter.error(f"Ursula configuration file not found - {config_file.absolute()}")
click.Abort()
keystore = recover_keystore(emitter=emitter)
update_config_keystore_path(
keystore_path=new_keystore_path, config_file=config_file
keystore_path=keystore.keystore_path, config_file=config_file
)
emitter.message(
f"Updated {config_file} to use keystore filepath: {new_keystore_path}",
f"Updated {config_file} to use keystore filepath: {keystore.keystore_path}",
color="green",
)
@ -482,37 +502,38 @@ def destroy(general_config, config_options, config_file, force):
"--keystore-filepath",
help="Path to keystore .priv file",
type=EXISTING_READABLE_FILE,
required=True,
)
def public_keys(config_file, keystore_filepath):
@click.option(
"--from-mnemonic",
help="View TACo public keys from mnemonic seed words",
is_flag=True,
)
def public_keys(config_file, keystore_filepath, from_mnemonic):
"""Display the public key of a keystore."""
emitter = StdoutEmitter()
if keystore_filepath and config_file:
if sum(1 for i in (keystore_filepath, config_file, from_mnemonic) if i) > 1:
raise click.BadOptionUsage(
"--keystore-filepath",
message=click.style(
"--keystore-filepath is incompatible with --config-file",
"Exactly one of --keystore-filepath, --config-file, or --from-mnemonic must be specified",
fg="red",
),
)
if keystore_filepath:
keystore = Keystore(keystore_filepath)
if from_mnemonic:
keystore = Keystore.from_mnemonic(collect_mnemonic(emitter))
else:
config_file = config_file or DEFAULT_CONFIG_FILEPATH
ursula_config = UrsulaConfiguration.from_configuration_file(
filepath=config_file
)
keystore = ursula_config.keystore
password = get_nucypher_password(emitter=emitter, confirm=False)
keystore.unlock(password)
keystore = Keystore(keystore_filepath or ursula_config.keystore.keystore_path)
keystore.unlock(get_nucypher_password(emitter=emitter, confirm=False))
ritualistic_power = keystore.derive_crypto_power(RitualisticPower)
ferveo_public_key = bytes(ritualistic_power.public_key()).hex()
keystore_file_data = json.load(open(keystore_filepath, "r"))
emitter.echo(f"Keystore timestamp ........ {keystore_file_data['created']}")
emitter.echo(f"Ferveo Public Key ......... {ferveo_public_key}")
emitter.message(f"\nFerveo Public Key: {ferveo_public_key}", color="cyan")
@ursula.command()

View File

@ -253,12 +253,34 @@ class Keystore:
class AuthenticationFailed(RuntimeError):
pass
def __init__(self, keystore_path: Path):
self.keystore_path = keystore_path
self.__created, self.__id = _parse_path(keystore_path)
class InvalidMnemonic(ValueError):
pass
def __init__(self, keystore_path: Path = None):
self.__secret = KEYSTORE_LOCKED
self.keystore_path = keystore_path
if self.keystore_path:
self.__created, self.__id = _parse_path(keystore_path)
@classmethod
def from_keystore_id(cls, filepath: Path) -> "Keystore":
instance = cls(keystore_path=filepath)
return instance
@classmethod
def from_mnemonic(cls, words: str) -> "Keystore":
__mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
__secret = bytes(__mnemonic.to_entropy(words))
keystore = cls()
keystore.__secret = __secret
return keystore
def __decrypt_keystore(self, path: Path, password: str) -> bool:
if not self.keystore_path:
raise Keystore.Invalid(
"Keystore path not set, initialize with a valid path."
)
payload = _read_keystore(path, deserializer=_deserialize_keystore)
__password_material = derive_key_material_from_password(password=password.encode(),
salt=payload['password_salt'])