mirror of https://github.com/nucypher/nucypher.git
Extract key configuration module; Fixes key caching scope, Deprecates Wallet in favor of umbral-style key handling; Addd keyring generation functionality.
parent
d00d1cd72e
commit
9e6c201993
|
@ -0,0 +1,319 @@
|
|||
import json
|
||||
import os
|
||||
from base64 import urlsafe_b64encode
|
||||
from pathlib import Path
|
||||
|
||||
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 eth_account import Account
|
||||
from nacl.secret import SecretBox
|
||||
from umbral.keys import UmbralPrivateKey
|
||||
from web3.auto import w3
|
||||
|
||||
from nkms.crypto.powers import SigningPower, EncryptingPower, CryptoPower
|
||||
from nkms.keystore.keypairs import SigningKeypair, EncryptingKeypair
|
||||
|
||||
w3.eth.enable_unaudited_features()
|
||||
|
||||
_CONFIG_ROOT = os.path.join(str(Path.home()), '.nucypher')
|
||||
|
||||
|
||||
class KMSConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_passphrase(passphrase) -> str:
|
||||
"""Validate a passphrase and return it or raise"""
|
||||
|
||||
rules = (
|
||||
(len(passphrase) >= 16, 'Passphrase is too short, must be >= 16 chars.'),
|
||||
)
|
||||
|
||||
for rule, failure_message in rules:
|
||||
if not rule:
|
||||
raise KMSConfigurationError(failure_message)
|
||||
else:
|
||||
return passphrase
|
||||
|
||||
|
||||
def _derive_master_key_from_passphrase(salt: bytes, passphrase: str) -> bytes:
|
||||
"""
|
||||
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) -> 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_umbral_key(wrapping_key: bytes, umbral_key: UmbralPrivateKey) -> dict:
|
||||
"""
|
||||
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(umbral_key.to_bytes(), nonce)
|
||||
|
||||
crypto_data = {
|
||||
'nonce': urlsafe_b64encode(nonce).decode(),
|
||||
'enc_key': urlsafe_b64encode(enc_key).decode()
|
||||
}
|
||||
|
||||
return crypto_data
|
||||
|
||||
|
||||
# TODO: Handle decryption failures
|
||||
def _decrypt_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.
|
||||
"""
|
||||
dec_key = SecretBox(wrapping_key).encrypt(enc_key_material, nonce)
|
||||
umbral_key = UmbralPrivateKey.from_bytes(dec_key)
|
||||
|
||||
return umbral_key
|
||||
|
||||
|
||||
def _generate_encryption_keys() -> tuple:
|
||||
"""Use pyUmbral keys to generate a new encrypting key pair"""
|
||||
|
||||
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() -> tuple:
|
||||
privkey = UmbralPrivateKey.gen_key()
|
||||
pubkey = privkey.get_pubkey()
|
||||
|
||||
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_keyfile(keypath: str, key_data: dict) -> None:
|
||||
"""Saves key data to a file"""
|
||||
|
||||
with open(keypath, 'w+') as keyfile:
|
||||
|
||||
# Check_if the file is empty
|
||||
keyfile.seek(0)
|
||||
check_byte = keyfile.read(1)
|
||||
|
||||
if len(check_byte) != 0:
|
||||
message = "{} is not empty. Check your key path.".format(keypath)
|
||||
raise KMSConfigurationError(message)
|
||||
|
||||
# Write the keydata to the file
|
||||
keyfile.seek(0)
|
||||
keyfile.write(json.dumps(key_data))
|
||||
|
||||
|
||||
def _generate_transacting_keys(passphrase: str) -> dict:
|
||||
"""Create a new wallet address from the provided passphrase"""
|
||||
|
||||
entropy = os.urandom(32) # max out entropy for keccak256
|
||||
account = Account.create(extra_entropy=entropy)
|
||||
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=passphrase)
|
||||
|
||||
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'])
|
||||
|
||||
umbral_key = UmbralPrivateKey.from_bytes(plain_key)
|
||||
return umbral_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'])
|
||||
|
||||
umbral_key = UmbralPrivateKey.from_bytes(plain_key)
|
||||
return umbral_key
|
||||
|
||||
|
||||
class KMSKeyring:
|
||||
"""
|
||||
Warning: This class handles private keys!
|
||||
|
||||
OS configuration and interface for ethereum and umbral keys
|
||||
|
||||
Keyring filesystem tree
|
||||
------------------------
|
||||
- keyring_root
|
||||
- .pub
|
||||
- keys
|
||||
- .priv
|
||||
|
||||
"""
|
||||
|
||||
__keyring_root = _CONFIG_ROOT
|
||||
__default_public_key_dir = __keyring_root
|
||||
__default_private_key_dir = os.path.join(_CONFIG_ROOT, 'keys') # Base Dir
|
||||
|
||||
__default_keyring_paths = {
|
||||
'root_key': os.path.join(__default_private_key_dir, 'root_key.priv'),
|
||||
'signing_key': os.path.join(__default_private_key_dir, 'signing_key.priv'),
|
||||
'wallet': os.path.join(__default_private_key_dir, 'account.json')
|
||||
}
|
||||
|
||||
def __init__(self, private_key_dir: str = None, root_keypath: str = None, signing_keypath: str = None,
|
||||
wallet_keypath: str = None):
|
||||
|
||||
# Check for a custom private key root directory
|
||||
self.__private_key_dir = private_key_dir or self.__default_private_key_dir
|
||||
|
||||
# Check for any custom key paths
|
||||
self.__root_keypath = root_keypath or self.__default_keyring_paths['root_key']
|
||||
self.__signing_keypath = signing_keypath or self.__default_keyring_paths['signing_key']
|
||||
self.__wallet_keypath = wallet_keypath or self.__default_keyring_paths['wallet']
|
||||
|
||||
# Key cache
|
||||
self.__derived_master_key = None
|
||||
self.__transacting_private_key = None
|
||||
|
||||
def __del__(self):
|
||||
self.lock()
|
||||
|
||||
# def _cache_transacting_key(self, passphrase) -> None:
|
||||
# """Decrypts and caches an ethereum key"""
|
||||
# key_data = _parse_keyfile(self.__wallet_keypath)
|
||||
# hex_bytes_privkey = Account.decrypt(keyfile_json=key_data, password=passphrase)
|
||||
# self.__transacting_privkey = hex_bytes_privkey
|
||||
|
||||
def lock(self):
|
||||
self.__derived_master_key = None
|
||||
self.__transacting_private_key = None
|
||||
return
|
||||
|
||||
def derive_crypto_power(self, power_class) -> CryptoPower:
|
||||
"""
|
||||
Takes either a SigningPower or an EncryptingPower and returns
|
||||
a either a SigningPower or EncryptingPower with the coinciding
|
||||
private key.
|
||||
"""
|
||||
if power_class is SigningPower:
|
||||
umbral_privkey = _get_signing_key(self.__derived_master_key)
|
||||
keypair = SigningKeypair(umbral_privkey)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def from_keys(cls, config_root: str = None):
|
||||
"""Generates a keyring object from existing keys on the local filesystem keys"""
|
||||
config_root = config_root or _CONFIG_ROOT
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _generate_default_keyring_tree(cls):
|
||||
os.mkdir(cls.__keyring_root)
|
||||
os.mkdir(cls.__k)
|
||||
|
||||
@classmethod
|
||||
def generate(cls, passphrase, encryption=True, transacting=True):
|
||||
"""
|
||||
Generates new encryption, signing, and transacting keys encrypted with the passphrase,
|
||||
respectively saving keyfiles on the local filesystem from default paths,
|
||||
returning the corresponding Keyring instance.
|
||||
"""
|
||||
|
||||
validate_passphrase(passphrase)
|
||||
|
||||
if not encryption and not transacting:
|
||||
raise ValueError('Either "encryption" or "transacting" must be True to generate new keys.')
|
||||
|
||||
if encryption is True:
|
||||
enc_key, _ = _generate_encryption_keys()
|
||||
sig_key, _ = _generate_signing_keys()
|
||||
|
||||
salt = b'dead sea salt' # TODO
|
||||
|
||||
der_master_key = _derive_master_key_from_passphrase(salt, passphrase)
|
||||
der_wrap_key = _derive_wrapping_key_from_master_key(salt, der_master_key)
|
||||
|
||||
enc_json = _encrypt_umbral_key(der_wrap_key, enc_key)
|
||||
sig_json = _encrypt_umbral_key(der_wrap_key, sig_key)
|
||||
|
||||
enc_json['master_salt'] = urlsafe_b64encode(salt).decode()
|
||||
sig_json['master_salt'] = urlsafe_b64encode(salt).decode()
|
||||
|
||||
enc_json['wrap_salt'] = urlsafe_b64encode(salt).decode()
|
||||
sig_json['wrap_salt'] = urlsafe_b64encode(salt).decode()
|
||||
|
||||
_save_keyfile(cls.__default_keyring_paths['root_key'], enc_json) # Write to file
|
||||
_save_keyfile(cls.__default_keyring_paths['signing_key'], sig_json)
|
||||
|
||||
if transacting is True:
|
||||
wallet = _generate_transacting_keys(passphrase)
|
||||
_save_keyfile(cls.__default_keyring_paths['wallet'], wallet)
|
||||
|
||||
keyring_instance = cls() # all defaults
|
||||
return keyring_instance
|
|
@ -1,174 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
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 eth_account import Account
|
||||
from nacl.secret import SecretBox
|
||||
from umbral.keys import UmbralPrivateKey
|
||||
from web3.auto import w3
|
||||
|
||||
w3.eth.enable_unaudited_features()
|
||||
|
||||
|
||||
class KMSConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_passphrase(passphrase) -> str:
|
||||
"""Validate a passphrase and return it or raise"""
|
||||
|
||||
rules = (
|
||||
(len(passphrase) >= 16, 'Passphrase is too short, must be >= 16 chars.'),
|
||||
)
|
||||
|
||||
for rule, failure_message in rules:
|
||||
if not rule:
|
||||
raise KMSConfigurationError(failure_message)
|
||||
else:
|
||||
return passphrase
|
||||
|
||||
|
||||
def _derive_master_key_from_passphrase(salt: bytes, passphrase: str) -> bytes:
|
||||
"""
|
||||
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) -> 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_umbral_key(wrapping_key: bytes, umbral_key: UmbralPrivateKey) -> dict:
|
||||
"""
|
||||
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(umbral_key.to_bytes(), nonce)
|
||||
|
||||
crypto_data = {
|
||||
'nonce': urlsafe_b64encode(nonce).decode(),
|
||||
'enc_key': urlsafe_b64encode(enc_key).decode()
|
||||
}
|
||||
|
||||
return crypto_data
|
||||
|
||||
|
||||
# TODO: Handle decryption failures
|
||||
def _decrypt_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.
|
||||
"""
|
||||
dec_key = SecretBox(wrapping_key).encrypt(enc_key_material, nonce)
|
||||
umbral_key = UmbralPrivateKey.from_bytes(dec_key)
|
||||
|
||||
return umbral_key
|
||||
|
||||
|
||||
def _generate_encryption_keys() -> tuple:
|
||||
"""Use pyUmbral keys to generate a new encrypting key pair"""
|
||||
|
||||
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() -> tuple:
|
||||
privkey = UmbralPrivateKey.gen_key()
|
||||
pubkey = privkey.get_pubkey()
|
||||
|
||||
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_keyfile(keypath: str, key_data: dict) -> None:
|
||||
"""Saves key data to a file"""
|
||||
|
||||
with open(keypath, 'w+') as keyfile:
|
||||
|
||||
# Check_if the file is empty
|
||||
keyfile.seek(0)
|
||||
check_byte = keyfile.read(1)
|
||||
|
||||
if len(check_byte) != 0:
|
||||
message = "{} is not empty. Check your key path.".format(keypath)
|
||||
raise KMSConfigurationError(message)
|
||||
|
||||
# Write the keydata to the file
|
||||
keyfile.seek(0)
|
||||
keyfile.write(json.dumps(key_data))
|
||||
|
||||
|
||||
def _bootstrap_config():
|
||||
"""
|
||||
Mocks user configuration, bootstraping user keys and local filesystem config
|
||||
"""
|
||||
enc_key, _ = _generate_encryption_keys()
|
||||
sig_key, _ = _generate_signing_keys()
|
||||
|
||||
der_master_key = _derive_master_key_from_passphrase(b'test', 'test')
|
||||
der_wrap_key = _derive_wrapping_key_from_master_key(b'test', der_master_key)
|
||||
|
||||
enc_json = _encrypt_umbral_key(der_wrap_key, enc_key)
|
||||
sig_json = _encrypt_umbral_key(der_wrap_key, sig_key)
|
||||
|
||||
enc_json['master_salt'] = urlsafe_b64encode(b'test').decode()
|
||||
sig_json['master_salt'] = urlsafe_b64encode(b'test').decode()
|
||||
|
||||
enc_json['wrap_salt'] = urlsafe_b64encode(b'test').decode()
|
||||
sig_json['wrap_salt'] = urlsafe_b64encode(b'test').decode()
|
||||
|
||||
_save_keyfile('root_key.priv', enc_json)
|
||||
_save_keyfile('signing_key.priv', sig_json)
|
||||
|
||||
|
||||
def create_eth_wallet(passphrase: str) -> dict:
|
||||
"""Create a new wallet address from the provided passphrase"""
|
||||
|
||||
entropy = os.urandom(32) # max out entropy for keccak256
|
||||
account = Account.create(extra_entropy=entropy)
|
||||
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=passphrase)
|
||||
|
||||
return encrypted_wallet_data
|
Loading…
Reference in New Issue