diff --git a/nucypher/config/base.py b/nucypher/config/base.py index 4188e2db4..952af7b63 100644 --- a/nucypher/config/base.py +++ b/nucypher/config/base.py @@ -19,7 +19,6 @@ import json import os import re -import tempfile from abc import ABC, abstractmethod from decimal import Decimal from pathlib import Path @@ -31,7 +30,6 @@ from constant_sorrow.constants import ( UNINITIALIZED_CONFIGURATION, NO_KEYSTORE_ATTACHED, NO_BLOCKCHAIN_CONNECTION, - FEDERATED_ADDRESS, DEVELOPMENT_CONFIGURATION, LIVE_CONFIGURATION ) @@ -57,6 +55,7 @@ from nucypher.crypto.powers import CryptoPower, CryptoPowerUp from nucypher.crypto.umbral_adapter import Signature from nucypher.network.middleware import RestMiddleware from nucypher.utilities.logging import Logger +from umbral.signing import Signature class BaseConfiguration(ABC): @@ -758,7 +757,7 @@ class CharacterConfiguration(BaseConfiguration): power_ups.append(power_up) return power_ups - def initialize(self, password: str) -> str: + def initialize(self, password: str, force: bool = False) -> str: """Initialize a new configuration and write installation files to disk.""" # Development @@ -769,7 +768,7 @@ class CharacterConfiguration(BaseConfiguration): # Persistent else: self._ensure_config_root_exists() - self.write_keystore(password=password) + self.write_keystore(password=password, force=force) self._cache_runtime_filepaths() self.node_storage.initialize() @@ -783,8 +782,8 @@ class CharacterConfiguration(BaseConfiguration): self.log.debug(message) return self.config_root - def write_keystore(self, password: str) -> Keystore: - self.__keystore = Keystore.generate(password=password, keystore_dir=self.keystore_dir) + def write_keystore(self, password: str, force: bool = False) -> Keystore: + self.__keystore = Keystore.generate(password=password, keystore_dir=self.keystore_dir, force=force) return self.keystore @classmethod diff --git a/nucypher/crypto/keystore.py b/nucypher/crypto/keystore.py index 7687ea0bd..ce883f49d 100644 --- a/nucypher/crypto/keystore.py +++ b/nucypher/crypto/keystore.py @@ -20,13 +20,13 @@ import json import os import stat import string -import tempfile from json import JSONDecodeError from os.path import abspath from pathlib import Path from secrets import token_bytes from typing import Callable, ClassVar, Dict, List, Union, Optional, Tuple +import click import time from constant_sorrow.constants import KEYSTORE_LOCKED from cryptography.hazmat.backends import default_backend @@ -36,6 +36,7 @@ from mnemonic.mnemonic import Mnemonic from nacl.exceptions import CryptoError from nacl.secret import SecretBox +from nucypher.characters.control.emitters import StdoutEmitter from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.crypto.constants import BLAKE2B from nucypher.crypto.keypairs import HostingKeypair @@ -340,6 +341,7 @@ class Keystore: @classmethod def restore(cls, words: str, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore': + """Restore a keystore from seed words""" __mnemonic = Mnemonic(_MNEMONIC_LANGUAGE) __secret = __mnemonic.to_entropy(words) path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir) @@ -347,14 +349,40 @@ class Keystore: return keystore @classmethod - def generate(cls, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore': + def generate(cls, password: str, keystore_dir: Optional[Path] = None, force: bool = False) -> 'Keystore': + """Generate a new nucypher keystore for use with characters""" mnemonic = Mnemonic(_MNEMONIC_LANGUAGE) __words = mnemonic.generate(strength=_ENTROPY_BITS) + cls._confirm_generate(__words, force=force) __secret = mnemonic.to_entropy(__words) path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir) keystore = cls(keystore_path=path) return keystore + @staticmethod + def _confirm_generate(__words: str, force: bool) -> None: + """ + Inform the caller of new keystore seed words generation the console + and optionally perform interactive confirmation + """ + + # notification + emitter = StdoutEmitter() + emitter.message(f'Backup your seed words, you will not be able to view them again.\n') + emitter.message(f'{__words}\n', color='cyan') + + # confirmation + if not force: + if not click.confirm("Have you backed up your seed phrase?"): + emitter.message('Keystore generation aborted.', color='red') + raise click.Abort() + click.clear() + + __response = click.prompt("Confirm seed words") + if __response != __words: + raise ValueError('Incorrect seed word confirmation. No keystore has been created, try again.') + click.clear() + @property def id(self) -> str: return self.__id diff --git a/tests/fixtures.py b/tests/fixtures.py index 0dc7664d1..737d0e58a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1051,3 +1051,9 @@ def stakeholder_configuration_file_location(custom_filepath): def mock_teacher_nodes(mocker): mock_nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values())[0:2] mocker.patch.dict(TEACHER_NODES, {TEMPORARY_DOMAIN: mock_nodes}, clear=True) + + +@pytest.fixture(autouse=True) +def disable_interactive_keystore_generation(mocker): + # Do not notify or confirm mnemonic seed words during tests normally + mocker.patch.object(Keystore, '_confirm_generate')