Notify and interactively confirm mnemonic generation.

pull/2701/head
Kieran Prasch 2021-06-15 10:41:41 -07:00 committed by Kieran R. Prasch
parent efee48aaba
commit b92d04ab00
3 changed files with 41 additions and 8 deletions

View File

@ -19,7 +19,6 @@
import json import json
import os import os
import re import re
import tempfile
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
@ -31,7 +30,6 @@ from constant_sorrow.constants import (
UNINITIALIZED_CONFIGURATION, UNINITIALIZED_CONFIGURATION,
NO_KEYSTORE_ATTACHED, NO_KEYSTORE_ATTACHED,
NO_BLOCKCHAIN_CONNECTION, NO_BLOCKCHAIN_CONNECTION,
FEDERATED_ADDRESS,
DEVELOPMENT_CONFIGURATION, DEVELOPMENT_CONFIGURATION,
LIVE_CONFIGURATION LIVE_CONFIGURATION
) )
@ -57,6 +55,7 @@ from nucypher.crypto.powers import CryptoPower, CryptoPowerUp
from nucypher.crypto.umbral_adapter import Signature from nucypher.crypto.umbral_adapter import Signature
from nucypher.network.middleware import RestMiddleware from nucypher.network.middleware import RestMiddleware
from nucypher.utilities.logging import Logger from nucypher.utilities.logging import Logger
from umbral.signing import Signature
class BaseConfiguration(ABC): class BaseConfiguration(ABC):
@ -758,7 +757,7 @@ class CharacterConfiguration(BaseConfiguration):
power_ups.append(power_up) power_ups.append(power_up)
return power_ups 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.""" """Initialize a new configuration and write installation files to disk."""
# Development # Development
@ -769,7 +768,7 @@ class CharacterConfiguration(BaseConfiguration):
# Persistent # Persistent
else: else:
self._ensure_config_root_exists() self._ensure_config_root_exists()
self.write_keystore(password=password) self.write_keystore(password=password, force=force)
self._cache_runtime_filepaths() self._cache_runtime_filepaths()
self.node_storage.initialize() self.node_storage.initialize()
@ -783,8 +782,8 @@ class CharacterConfiguration(BaseConfiguration):
self.log.debug(message) self.log.debug(message)
return self.config_root return self.config_root
def write_keystore(self, password: str) -> Keystore: def write_keystore(self, password: str, force: bool = False) -> Keystore:
self.__keystore = Keystore.generate(password=password, keystore_dir=self.keystore_dir) self.__keystore = Keystore.generate(password=password, keystore_dir=self.keystore_dir, force=force)
return self.keystore return self.keystore
@classmethod @classmethod

View File

@ -20,13 +20,13 @@ import json
import os import os
import stat import stat
import string import string
import tempfile
from json import JSONDecodeError from json import JSONDecodeError
from os.path import abspath from os.path import abspath
from pathlib import Path from pathlib import Path
from secrets import token_bytes from secrets import token_bytes
from typing import Callable, ClassVar, Dict, List, Union, Optional, Tuple from typing import Callable, ClassVar, Dict, List, Union, Optional, Tuple
import click
import time import time
from constant_sorrow.constants import KEYSTORE_LOCKED from constant_sorrow.constants import KEYSTORE_LOCKED
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -36,6 +36,7 @@ from mnemonic.mnemonic import Mnemonic
from nacl.exceptions import CryptoError from nacl.exceptions import CryptoError
from nacl.secret import SecretBox from nacl.secret import SecretBox
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.constants import BLAKE2B from nucypher.crypto.constants import BLAKE2B
from nucypher.crypto.keypairs import HostingKeypair from nucypher.crypto.keypairs import HostingKeypair
@ -340,6 +341,7 @@ class Keystore:
@classmethod @classmethod
def restore(cls, words: str, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore': def restore(cls, words: str, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore':
"""Restore a keystore from seed words"""
__mnemonic = Mnemonic(_MNEMONIC_LANGUAGE) __mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
__secret = __mnemonic.to_entropy(words) __secret = __mnemonic.to_entropy(words)
path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir) path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir)
@ -347,14 +349,40 @@ class Keystore:
return keystore return keystore
@classmethod @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) mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
__words = mnemonic.generate(strength=_ENTROPY_BITS) __words = mnemonic.generate(strength=_ENTROPY_BITS)
cls._confirm_generate(__words, force=force)
__secret = mnemonic.to_entropy(__words) __secret = mnemonic.to_entropy(__words)
path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir) path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir)
keystore = cls(keystore_path=path) keystore = cls(keystore_path=path)
return keystore 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 @property
def id(self) -> str: def id(self) -> str:
return self.__id return self.__id

View File

@ -1051,3 +1051,9 @@ def stakeholder_configuration_file_location(custom_filepath):
def mock_teacher_nodes(mocker): def mock_teacher_nodes(mocker):
mock_nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values())[0:2] 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) 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')