From ec88557dfad9a4cbd8f0b76e02bcddaeca2a1439 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Tue, 10 Apr 2018 14:41:44 -0700 Subject: [PATCH] Implements protected key caching methods for keyring. --- nkms/config/keys.py | 125 +++++++++++++------------------------------ nkms/config/utils.py | 50 ++++++++++++++++- 2 files changed, 87 insertions(+), 88 deletions(-) diff --git a/nkms/config/keys.py b/nkms/config/keys.py index e884c0c48..fdfff0ac2 100644 --- a/nkms/config/keys.py +++ b/nkms/config/keys.py @@ -16,6 +16,7 @@ from web3.auto import w3 from nkms.config import utils from nkms.config.configs import _DEFAULT_CONFIGURATION_DIR, KMSConfigurationError +from nkms.config.utils import _parse_keyfile, _save_private_keyfile from nkms.crypto.powers import SigningPower, EncryptingPower, CryptoPower from nkms.keystore.keypairs import SigningKeypair, EncryptingKeypair @@ -85,7 +86,7 @@ def _encrypt_umbral_key(wrapping_key: bytes, umbral_key: UmbralPrivateKey) -> di # TODO: Handle decryption failures -def _decrypt_key(wrapping_key: bytes, nonce: bytes, enc_key_material: bytes) -> UmbralPrivateKey: +def _decrypt_umbral_key(wrapping_key: bytes, nonce: bytes, enc_key_material: bytes) -> UmbralPrivateKey: """ Decrypts an encrypted key with nacl's XSalsa20-Poly1305 algorithm (SecretBox). Returns a decrypted key as an UmbralPrivateKey. @@ -112,53 +113,6 @@ def _generate_signing_keys() -> tuple: return privkey, pubkey -def _parse_keyfile(keypath: str): - """Parses a keyfile and returns key metadata as a dict.""" - - with open(keypath, 'r') as keyfile: - try: - key_metadata = json.loads(keyfile) - except json.JSONDecodeError: - raise KMSConfigurationError("Invalid data in keyfile {}".format(keypath)) - else: - return key_metadata - - -def _save_private_keyfile(keypath: str, key_data: dict) -> str: - """ - Creates a permissioned keyfile and save it to the local filesystem. - The file must be created in this call, and will fail if the path exists. - Returns the filepath string used to write the keyfile. - - Note: getting and setting the umask is not thread-safe! - - See linux open docs: http://man7.org/linux/man-pages/man2/open.2.html - --------------------------------------------------------------------- - O_CREAT - If pathname does not exist, create it as a regular file. - - O_EXCL - Ensure that this call creates the file: if this flag is - specified in conjunction with O_CREAT, and pathname already - exists, then open() fails with the error EEXIST. - --------------------------------------------------------------------- - """ - - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL # Write, Create, Non-Existing - mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 - - try: - keyfile_descriptor = os.open(path=keypath, flags=flags, mode=mode) - finally: - os.umask(0) # Set the umask to 0 after opening - - # Write and destroy file descriptor reference - with os.fdopen(keyfile_descriptor, 'w') as keyfile: - keyfile.write(json.dumps(key_data)) - output_path = keyfile.name - - del keyfile_descriptor - return output_path - - def _generate_transacting_keys(passphrase: str) -> dict: """Create a new wallet address and private "transacting" key from the provided passphrase""" entropy = os.urandom(32) # max out entropy for keccak256 @@ -167,37 +121,6 @@ def _generate_transacting_keys(passphrase: str) -> dict: return encrypted_wallet_data -# TODO: Make these one function -def _get_decrypting_key(self, master_key: bytes = None) -> UmbralPrivateKey: - """Returns plaintext version of decrypting key.""" - - key_data = _parse_keyfile(self.__private_key_dir) - - # TODO: Prompt user for password? - if master_key is None: - return - - wrap_key = _derive_wrapping_key_from_master_key(key_data['wrap_salt'], master_key) - plain_key = _decrypt_key(wrap_key, key_data['nonce'], key_data['enc_key']) - - return plain_key - - -def _get_signing_key(self, master_key: bytes = None) -> UmbralPrivateKey: - """Returns plaintext version of private signature ("decrypting") key.""" - - key_data = _parse_keyfile(self.__signing_keypath) - - # TODO: Prompt user for password? - if master_key is None: - return - - wrap_key = _derive_wrapping_key_from_master_key(key_data['wrap_salt'], master_key) - plain_key = _decrypt_key(wrap_key, key_data['nonce'], key_data['enc_key']) - - return plain_key - - class KMSKeyring: """ Warning: This class handles private keys! @@ -225,6 +148,12 @@ class KMSKeyring: 'transacting': os.path.join(__default_private_key_dir, 'wallet.json') } + class KeyringError(Exception): + pass + + class KeyringLocked(KeyringError): + pass + def __init__(self, root_key_path: str=None, signing_key_path: str=None, transacting_key_path: str=None): """ Generates a KMSKeyring instance with the provided key paths, @@ -251,7 +180,29 @@ class KMSKeyring: def __del__(self): self.lock() + def __decrypt_keyfile(self, key_path: str) -> UmbralPrivateKey: + """Returns plaintext version of decrypting key.""" + + # Checks for cached key + if self.__derived_master_key is None: + message = 'The keyring cannot be used when it is locked. Call .unlock first.' + raise self.KeyringLocked(message) + + key_data = _parse_keyfile(key_path) + wrap_key = _derive_wrapping_key_from_master_key(key_data['wrap_salt'], self.__derived_master_key) + plain_umbral_key = _decrypt_umbral_key(wrap_key, key_data['nonce'], key_data['enc_key']) + + return plain_umbral_key + + def unlock(self, passphrase: bytes) -> None: + if self.__derived_master_key is not None: + raise Exception('Keyring already unlocked') + + derived_key = _derive_master_key_from_passphrase(passphrase=passphrase) + self.__derived_master_key = derived_key + def lock(self) -> None: + """Make efforts to remove references to the cached key data""" self.__derived_master_key = None self.__transacting_private_key = None @@ -261,23 +212,23 @@ class KMSKeyring: a either a SigningPower or EncryptingPower with the coinciding private key. + TODO: Derive a key from the root_key. TODO: TransactingPower """ + if power_class is SigningPower: - umbral_privkey = _get_signing_key(self.__derived_master_key) - keypair = SigningKeypair(umbral_privkey) + key_path = self.__signing_keypath elif power_class is EncryptingPower: - # TODO: Derive a key from the root_key. - umbral_privkey = _get_decrypting_key(self.__derived_master_key) - keypair = EncryptingKeypair(umbral_privkey) - + key_path = self.__root_keypath else: failure_message = "{} is an invalid type for deriving a CryptoPower.".format(type(power_class)) raise ValueError(failure_message) - new_power = power_class(keypair=keypair) - return new_power + umbral_privkey = self.__decrypt_keyfile(key_path) + keypair = power_class._keypair_class(umbral_privkey) + new_cryptopower = power_class(keypair=keypair) + return new_cryptopower @classmethod def generate(cls, passphrase: str, encryption: bool=True, transacting: bool=True, output_path: str=None) -> 'KMSKeyring': diff --git a/nkms/config/utils.py b/nkms/config/utils.py index 1545150c8..65ab0829a 100644 --- a/nkms/config/utils.py +++ b/nkms/config/utils.py @@ -1,7 +1,55 @@ +import json import os from nkms.config.keys import KMSKeyring -from .configs import _DEFAULT_CONFIGURATION_DIR +from .configs import _DEFAULT_CONFIGURATION_DIR, KMSConfigurationError + + +def _save_private_keyfile(keypath: str, key_data: dict) -> str: + """ + Creates a permissioned keyfile and save it to the local filesystem. + The file must be created in this call, and will fail if the path exists. + Returns the filepath string used to write the keyfile. + + Note: getting and setting the umask is not thread-safe! + + See linux open docs: http://man7.org/linux/man-pages/man2/open.2.html + --------------------------------------------------------------------- + O_CREAT - If pathname does not exist, create it as a regular file. + + O_EXCL - Ensure that this call creates the file: if this flag is + specified in conjunction with O_CREAT, and pathname already + exists, then open() fails with the error EEXIST. + --------------------------------------------------------------------- + """ + + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL # Write, Create, Non-Existing + mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 + + try: + keyfile_descriptor = os.open(path=keypath, flags=flags, mode=mode) + finally: + os.umask(0) # Set the umask to 0 after opening + + # Write and destroy file descriptor reference + with os.fdopen(keyfile_descriptor, 'w') as keyfile: + keyfile.write(json.dumps(key_data)) + output_path = keyfile.name + + del keyfile_descriptor + return output_path + + +def _parse_keyfile(keypath: str): + """Parses a keyfile and returns key metadata as a dict.""" + + with open(keypath, 'r') as keyfile: + try: + key_metadata = json.loads(keyfile) + except json.JSONDecodeError: + raise KMSConfigurationError("Invalid data in keyfile {}".format(keypath)) + else: + return key_metadata def generate_confg_dir(path: str=None,) -> None: