mirror of https://github.com/nucypher/nucypher.git
Default config paths; Extrapolates config utils module; further outlines blockchain management
parent
049153498f
commit
7f7dfe2337
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue