diff --git a/nkms/config/config.py b/nkms/config/config.py index 0d382931f..3c54c16ae 100644 --- a/nkms/config/config.py +++ b/nkms/config/config.py @@ -1,4 +1,4 @@ -import base64 +import json import os import web3 @@ -7,60 +7,82 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.kdf.scrypt import Scrypt from nacl.secret import SecretBox +import web3 -class Wallet: - def accounts(self): - return web3.personal.listAccounts() +from nkms.config.utils import _derive_wrapping_key_from_master_key, _decrypt_key + + +class EthAccount: + """http://eth-account.readthedocs.io/en/latest/eth_account.html#eth-account""" + + def __init__(self, address): + self.__address = address + + def __del__(self): + self.lock() + + @property + def address(self): + return self.__address @classmethod - def create(self): - pass + def create(self, passphrase): + """Create a new wallet address""" @classmethod - def import_existing(self): - pass + def import_existing(self, private_key, passphrase): + """Instantiate a wallet from an existing wallet address""" + + def unlock(self, passphrase, duration): + """Unlock the account for a specified duration""" + + def lock(self): + """Lock the account and make efforts to remove the key from memory""" + + def transact(self, txhash, passphrase): + """Sign and transact without unlocking""" class KMSConfig: """Warning: This class handles private keys!""" - _default_config_path = None - __root_name = '.nucypher' - __default_key_dir = os.path.join('~', __root_name, 'keys') # TODO: Change by actor + __config_root = os.path.join('~', '.nucypher') - class KMSConfigrationError(Exception): + __default_yaml_path = os.path.join(__config_root, 'conf.yml') + __keys_dir = os.path.join(__config_root, 'keys') + __transacting_key_path = os.path.join('.ethereum') + + class KMSConfigurationError(Exception): pass - def __init__(self, blockchain_address: str, enc_key_path: str=None, - sig_key_path: str=None, config_path: str=None): - - if self._default_config_path is None: - pass # TODO: no default config path set + def __init__(self, blockchain_address: str, keys_dir: str=None, + yaml_config_path: str=None): self.__config_path = config_path or self._default_config_path self.__enc_key_path = enc_key_path self.__sig_key_path = sig_key_path + self.__yaml_config_path = yaml_config_path or self.__default_yaml_path + self.__keys_dir = keys_dir + # Blockchain self.address = blockchain_address @classmethod - def from_config_file(cls, config_path=None): + def from_yaml_config(cls, config_path=None): """Reads the config file and instantiates a KMSConfig instance""" - with open(config_path or cls._default_config_path, 'r') as f: + + with open(config_path or cls.__default_yaml_path, 'r') as conf_file: # Get data from the config file - data = f.read() #TODO: Parse + data = conf_file.read() #TODO: Parse instance = cls() return instance - def get_transacting_key(self): - """ - - """ - with open(self.transacting_key_path) as keyfile: + def get_transacting_key(self, passphrase): + with open(self.__transacting_key_path) as keyfile: encrypted_key = keyfile.read() - private_key = web3.eth.account.decrypt(encrypted_key, 'correcthorsebatterystaple') + private_key = web3.eth.account.decrypt(encrypted_key, passphrase) # WARNING: do not save the key or password anywhere def get_decrypting_key(self, master_key: bytes=None): @@ -99,22 +121,22 @@ class KMSConfig: """ Parses a keyfile and returns key metadata as a dict. """ - keyfile_path = os.path.join(self.__key_dir, path) + keyfile_path = os.path.join(self.__keys_dir, path) with open(keyfile_path, 'r') as keyfile: try: key_metadata = json.loads(keyfile) - except json.JSONDecodeError: - raise KMSConfigurationError("Invalid data in keyfile {}".format(path)) - - return key_metadata + except json.JSONDecodeError: + raise self.KMSConfigurationError("Invalid data in keyfile {}".format(path)) + else: + return key_metadata def _save_keyfile(self, path: str, key_data: dict): """ Saves key data to a file. """ - keyfile_path = os.path.join(self.__key_dir, path) - with open(keyfile_path), 'w+') as keyfile: - f.seek(0) + keyfile_path = os.path.join(self.__keys_dir, path) + with open(keyfile_path, 'w+') as keyfile: + keyfile.seek(0) check_byte = keyfile.read(1) if len(check_byte) != 0: raise self.KMSConfigurationError("Keyfile is not empty! Check your key path.") @@ -122,78 +144,3 @@ class KMSConfig: keyfile.seek(0) keyfile.write(json.dumps(key_data)) - -def _derive_master_key_from_passphrase(salt: bytes, passphrase: str): - """ - Uses Scrypt derivation to derive a master key for encrypting key material. - See RFC 7914 for n, r, and p value selections. - This takes around ~5 seconds to perform. - """ - master_key = Scrypt( - salt=salt, - length=32, - n=2**20, - r=8, - p=1, - backend=default_backend() - ).derive(passphrase.encode()) - - return master_key - - -def _derive_wrapping_key_from_master_key(salt: bytes, master_key: bytes): - """ - Uses HKDF to derive a 32 byte wrapping key to encrypt key material with. - """ - wrapping_key = HKDF( - algorithm=hashes.SHA512(), - length=32, - salt=salt, - info=b'NuCypher-KMS-KeyWrap', - backend=default_backend() - ).derive(master_key) - - return wrapping_key - - -def _encrypt_key(wrapping_key: bytes, key_material: bytes): - """ - Encrypts a key with nacl's XSalsa20-Poly1305 algorithm (SecretBox). - Returns an encrypted key as bytes with the nonce appended. - """ - nonce = os.urandom(24) - enc_key = SecretBox(wrapping_key).encrypt(key_material, nonce) - - crypto_data = { - 'nonce': nonce, - 'enc_key': enc_key - } - - return crypto_data - - -# TODO: Handle decryption failures -def _decrypt_key(wrapping_key: bytes, nonce: bytes, enc_key_material: bytes): - """ - Decrypts an encrypted key with nacl's XSalsa20-Poly1305 algorithm (SecretBox). - Returns a decrypted key as bytes. - """ - dec_key = SecretBox(wrapping_key).encrypt(enc_key_material, nonce) - - return dec_key - - -def _generate_encryption_keys(): - privkey = UmbralPrivateKey.gen_key() - pubkey = priv_key.get_pubkey() - - return (privkey, pubkey) - - -# TODO: Do we really want to use Umbral keys for signing? -# TODO: Perhaps we can use Curve25519/EdDSA for signatures? -def _generate_signing_keys(): - privkey = UmbralPrivateKey.gen_key() - pubkey = priv_key.get_pubkey() - - return (privkey, pubkey) diff --git a/nkms/config/interface.py b/nkms/config/interface.py index 99d1e511a..0cec0c395 100644 --- a/nkms/config/interface.py +++ b/nkms/config/interface.py @@ -1,32 +1,49 @@ -import curses -from curses.textpad import rectangle, Textbox +import sys +from .configs import validate_passphrase, KMSConfig + +# Configurator Text # + +title = "NuCypher KMS Configurator" +welcome = "Welcome to the NuCypher KMS Config Tool" +description = "Use this tool to manage keypairs node operation." + +newlines = '\n' * 2 +press_any = "Press any key to continue..." + +enter_new_passphrase = 'Enter new passphrase: ' +confirm_passphrase = "Confirm passphrase" +did_not_match = "Passwords did not match" + +loading = "loading..." +keygen_success = "Keys generated and written to keyfile!" -class ConfigText: - title = "NuCypher KMS Configurator" - welcome = "Welcome to the NuCypher KMS Config Tool" - description = "Use this tool to manage keypairs node operation." - loading = "loading..." - keygen_success = "Keys generated and written to keyfile!" +def close(): + sys.exit() -def main(screen): +def gather_passphrase(): + user_passphrase = input(enter_new_passphrase) - height, width = 40, 1 - editwin = curses.newwin(width, height, 2, 1) - textarea = rectangle(screen, 1, 0, 1+width+1, 1+height+1) # 1s for padding + try: # Validate + validate_passphrase(user_passphrase) + except KMSConfig.KMSConfigrationError: + close() + else: + confirm_passphrase = input(enter_new_passphrase) + if user_passphrase != confirm_passphrase: + print(did_not_match) + del user_passphrase + del confirm_passphrase + gather_passphrase() - screen.addstr(1, 1, ConfigText.welcome) - screen.refresh() - - screen.addstr(1, 1, ConfigText.title, curses.A_BOLD) - screen.refresh() - - box = Textbox(editwin) - box.edit() # Let the user edit until Ctrl-G is struck. - message = box.gather() # Get resulting contents + return user_passphrase -if __name__ == "__main__": - curses.wrapper(main) - curses.beep() +def start(): + print(title, welcome, description, newlines, sep='\n') + input(press_any) + gather_passphrase() + + +start() diff --git a/nkms/config/utils.py b/nkms/config/utils.py new file mode 100644 index 000000000..31c4171a2 --- /dev/null +++ b/nkms/config/utils.py @@ -0,0 +1,102 @@ +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.kdf.scrypt import Scrypt +from nacl.secret import SecretBox +from umbral.keys import UmbralPrivateKey + +from nkms.config.config import KMSConfig + + +def validate_passphrase(passphrase): + """Validate a passphrase and return it or raise""" + + rules = ( + (len(passphrase) >= 16, 'Too short'), + ) + + for rule, failure_message in rules: + if not rule: + raise KMSConfig.KMSConfigurationError(failure_message) + else: + return passphrase + + +def _derive_master_key_from_passphrase(salt: bytes, passphrase: str): + """ + Uses Scrypt derivation to derive a master key for encrypting key material. + See RFC 7914 for n, r, and p value selections. + This takes around ~5 seconds to perform. + """ + master_key = Scrypt( + salt=salt, + length=32, + n=2**20, + r=8, + p=1, + backend=default_backend() + ).derive(passphrase.encode()) + + return master_key + + +def _derive_wrapping_key_from_master_key(salt: bytes, master_key: bytes): + """ + Uses HKDF to derive a 32 byte wrapping key to encrypt key material with. + """ + wrapping_key = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=salt, + info=b'NuCypher-KMS-KeyWrap', + backend=default_backend() + ).derive(master_key) + + return wrapping_key + + +def _encrypt_key(wrapping_key: bytes, key_material: bytes): + """ + Encrypts a key with nacl's XSalsa20-Poly1305 algorithm (SecretBox). + Returns an encrypted key as bytes with the nonce appended. + """ + nonce = os.urandom(24) + enc_key = SecretBox(wrapping_key).encrypt(key_material, nonce) + + crypto_data = { + 'nonce': nonce, + 'enc_key': enc_key + } + + return crypto_data + + +# TODO: Handle decryption failures +def _decrypt_key(wrapping_key: bytes, nonce: bytes, enc_key_material: bytes): + """ + Decrypts an encrypted key with nacl's XSalsa20-Poly1305 algorithm (SecretBox). + Returns a decrypted key as bytes. + """ + dec_key = SecretBox(wrapping_key).encrypt(enc_key_material, nonce) + + return dec_key + + +def _generate_encryption_keys(): + privkey = UmbralPrivateKey.gen_key() + pubkey = privkey.get_pubkey() + + return privkey, pubkey + + +# TODO: Do we really want to use Umbral keys for signing? +# TODO: Perhaps we can use Curve25519/EdDSA for signatures? +def _generate_signing_keys(): + privkey = UmbralPrivateKey.gen_key() + pubkey = privkey.get_pubkey() + + return privkey, pubkey + +