forked from chaintool/funga-eth
Initial commit
This commit is contained in:
8
funga/eth/keystore/__init__.py
Normal file
8
funga/eth/keystore/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# third-party imports
|
||||
#from eth_keys import KeyAPI
|
||||
#from eth_keys.backends import NativeECCBackend
|
||||
|
||||
#keyapi = KeyAPI(NativeECCBackend)
|
||||
|
||||
#from .postgres import ReferenceKeystore
|
||||
#from .dict import DictKeystore
|
||||
45
funga/eth/keystore/dict.py
Normal file
45
funga/eth/keystore/dict.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# external imports
|
||||
from hexathon import (
|
||||
strip_0x,
|
||||
add_0x,
|
||||
)
|
||||
|
||||
# local imports
|
||||
#from . import keyapi
|
||||
from funga.error import UnknownAccountError
|
||||
from .interface import EthKeystore
|
||||
from funga.eth.encoding import private_key_to_address
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DictKeystore(EthKeystore):
|
||||
|
||||
def __init__(self):
|
||||
super(DictKeystore, self).__init__()
|
||||
self.keys = {}
|
||||
|
||||
|
||||
def get(self, address, password=None):
|
||||
address_key = strip_0x(address).lower()
|
||||
if password != None:
|
||||
logg.debug('password ignored as dictkeystore doesnt do encryption')
|
||||
try:
|
||||
return self.keys[address_key]
|
||||
except KeyError:
|
||||
raise UnknownAccountError(address_key)
|
||||
|
||||
|
||||
def list(self):
|
||||
return list(self.keys.keys())
|
||||
|
||||
|
||||
def import_key(self, pk, password=None):
|
||||
address_hex = private_key_to_address(pk)
|
||||
address_hex_clean = strip_0x(address_hex).lower()
|
||||
self.keys[address_hex_clean] = pk.secret
|
||||
logg.debug('added key {}'.format(address_hex))
|
||||
return add_0x(address_hex)
|
||||
50
funga/eth/keystore/interface.py
Normal file
50
funga/eth/keystore/interface.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# standard imports
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
# local imports
|
||||
from funga.eth.keystore import keyfile
|
||||
from funga.eth.encoding import private_key_from_bytes
|
||||
from funga.keystore import Keystore
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def native_keygen(*args, **kwargs):
|
||||
return os.urandom(32)
|
||||
|
||||
|
||||
class EthKeystore(Keystore):
|
||||
|
||||
def __init__(self, private_key_generator=native_keygen):
|
||||
super(EthKeystore, self).__init__(private_key_generator, private_key_from_bytes, keyfile.from_some)
|
||||
|
||||
|
||||
def new(self, password=None):
|
||||
b = self.private_key_generator()
|
||||
return self.import_raw_key(b, password=password)
|
||||
|
||||
|
||||
def import_raw_key(self, b, password=None):
|
||||
pk = private_key_from_bytes(b)
|
||||
return self.import_key(pk, password)
|
||||
|
||||
|
||||
def import_key(self, pk, password=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def import_keystore_data(self, keystore_content, password=''):
|
||||
if type(keystore_content).__name__ == 'str':
|
||||
keystore_content = json.loads(keystore_content)
|
||||
elif type(keystore_content).__name__ == 'bytes':
|
||||
logg.debug('bytes {}'.format(keystore_content))
|
||||
keystore_content = json.loads(keystore_content.decode('utf-8'))
|
||||
private_key = keyfile.from_dict(keystore_content, password.encode('utf-8'))
|
||||
return self.import_raw_key(private_key, password)
|
||||
|
||||
|
||||
def import_keystore_file(self, keystore_file, password=''):
|
||||
private_key = keyfile.from_file(keystore_file, password)
|
||||
return self.import_raw_key(private_key)
|
||||
173
funga/eth/keystore/keyfile.py
Normal file
173
funga/eth/keystore/keyfile.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# standard imports
|
||||
import os
|
||||
import hashlib
|
||||
import logging
|
||||
import json
|
||||
import uuid
|
||||
|
||||
# external imports
|
||||
import coincurve
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util import Counter
|
||||
import sha3
|
||||
|
||||
# local imports
|
||||
from funga.error import (
|
||||
DecryptError,
|
||||
KeyfileError,
|
||||
)
|
||||
from funga.eth.encoding import private_key_to_address
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
algo_keywords = [
|
||||
'aes-128-ctr',
|
||||
]
|
||||
hash_keywords = [
|
||||
'scrypt'
|
||||
]
|
||||
|
||||
default_kdfparams = {
|
||||
'dklen': 32,
|
||||
'n': 1 << 18,
|
||||
'p': 1,
|
||||
'r': 8,
|
||||
'salt': os.urandom(32).hex(),
|
||||
}
|
||||
|
||||
|
||||
def to_mac(mac_key, ciphertext_bytes):
|
||||
h = sha3.keccak_256()
|
||||
h.update(mac_key)
|
||||
h.update(ciphertext_bytes)
|
||||
return h.digest()
|
||||
|
||||
|
||||
class Hashes:
|
||||
|
||||
@staticmethod
|
||||
def from_scrypt(kdfparams=default_kdfparams, passphrase=''):
|
||||
dklen = int(kdfparams['dklen'])
|
||||
n = int(kdfparams['n'])
|
||||
p = int(kdfparams['p'])
|
||||
r = int(kdfparams['r'])
|
||||
salt = bytes.fromhex(kdfparams['salt'])
|
||||
|
||||
return hashlib.scrypt(passphrase.encode('utf-8'), salt=salt,n=n, p=p, r=r, maxmem=1024*1024*1024, dklen=dklen)
|
||||
|
||||
|
||||
class Ciphers:
|
||||
|
||||
aes_128_block_size = 1 << 7
|
||||
aes_iv_len = 16
|
||||
|
||||
@staticmethod
|
||||
def decrypt_aes_128_ctr(ciphertext, decryption_key, iv):
|
||||
ctr = Counter.new(Ciphers.aes_128_block_size, initial_value=iv)
|
||||
cipher = AES.new(decryption_key, AES.MODE_CTR, counter=ctr)
|
||||
plaintext = cipher.decrypt(ciphertext)
|
||||
return plaintext
|
||||
|
||||
|
||||
@staticmethod
|
||||
def encrypt_aes_128_ctr(plaintext, encryption_key, iv):
|
||||
ctr = Counter.new(Ciphers.aes_128_block_size, initial_value=iv)
|
||||
cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr)
|
||||
ciphertext = cipher.encrypt(plaintext)
|
||||
return ciphertext
|
||||
|
||||
|
||||
def to_dict(private_key_bytes, passphrase=''):
|
||||
|
||||
private_key = coincurve.PrivateKey(secret=private_key_bytes)
|
||||
|
||||
encryption_key = Hashes.from_scrypt(passphrase=passphrase)
|
||||
|
||||
address_hex = private_key_to_address(private_key)
|
||||
iv_bytes = os.urandom(Ciphers.aes_iv_len)
|
||||
iv = int.from_bytes(iv_bytes, 'big')
|
||||
ciphertext_bytes = Ciphers.encrypt_aes_128_ctr(private_key.secret, encryption_key[:16], iv)
|
||||
|
||||
mac = to_mac(encryption_key[16:], ciphertext_bytes)
|
||||
|
||||
crypto_dict = {
|
||||
'cipher': 'aes-128-ctr',
|
||||
'ciphertext': ciphertext_bytes.hex(),
|
||||
'cipherparams': {
|
||||
'iv': iv_bytes.hex(),
|
||||
},
|
||||
'kdf': 'scrypt',
|
||||
'kdfparams': default_kdfparams,
|
||||
'mac': mac.hex(),
|
||||
}
|
||||
|
||||
uu = uuid.uuid1()
|
||||
o = {
|
||||
'address': address_hex,
|
||||
'version': 3,
|
||||
'crypto': crypto_dict,
|
||||
'id': str(uu),
|
||||
}
|
||||
return o
|
||||
|
||||
|
||||
def from_dict(o, passphrase=''):
|
||||
|
||||
cipher = o['crypto']['cipher']
|
||||
if cipher not in algo_keywords:
|
||||
raise NotImplementedError('cipher "{}" not implemented'.format(cipher))
|
||||
|
||||
kdf = o['crypto']['kdf']
|
||||
if kdf not in hash_keywords:
|
||||
raise NotImplementedError('kdf "{}" not implemented'.format(kdf))
|
||||
|
||||
m = getattr(Hashes, 'from_{}'.format(kdf.replace('-', '_')))
|
||||
decryption_key = m(o['crypto']['kdfparams'], passphrase)
|
||||
|
||||
control_mac = bytes.fromhex(o['crypto']['mac'])
|
||||
iv_bytes = bytes.fromhex(o['crypto']['cipherparams']['iv'])
|
||||
iv = int.from_bytes(iv_bytes, "big")
|
||||
ciphertext_bytes = bytes.fromhex(o['crypto']['ciphertext'])
|
||||
|
||||
# check mac
|
||||
calculated_mac = to_mac(decryption_key[16:], ciphertext_bytes)
|
||||
if control_mac != calculated_mac:
|
||||
raise DecryptError('mac mismatch when decrypting passphrase')
|
||||
|
||||
m = getattr(Ciphers, 'decrypt_{}'.format(cipher.replace('-', '_')))
|
||||
|
||||
try:
|
||||
pk = m(ciphertext_bytes, decryption_key[:16], iv)
|
||||
except AssertionError as e:
|
||||
raise DecryptError('could not decrypt keyfile: {}'.format(e))
|
||||
logg.debug('bar')
|
||||
|
||||
return pk
|
||||
|
||||
|
||||
def from_file(filepath, passphrase=''):
|
||||
|
||||
f = open(filepath, 'r')
|
||||
try:
|
||||
o = json.load(f)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
f.close()
|
||||
raise KeyfileError(e)
|
||||
f.close()
|
||||
|
||||
return from_dict(o, passphrase)
|
||||
|
||||
|
||||
def from_some(v, passphrase=''):
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode('utf-8')
|
||||
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
return from_file(v, passphrase)
|
||||
except Exception:
|
||||
logg.debug('keyfile parse as file fail')
|
||||
pass
|
||||
v = json.loads(v)
|
||||
|
||||
return from_dict(v, passphrase)
|
||||
108
funga/eth/keystore/sql.py
Normal file
108
funga/eth/keystore/sql.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# standard imports
|
||||
import logging
|
||||
import base64
|
||||
|
||||
# external imports
|
||||
from cryptography.fernet import Fernet
|
||||
#import psycopg2
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import sha3
|
||||
from hexathon import (
|
||||
strip_0x,
|
||||
add_0x,
|
||||
)
|
||||
|
||||
# local imports
|
||||
from .interface import EthKeystore
|
||||
#from . import keyapi
|
||||
from funga.error import UnknownAccountError
|
||||
from funga.eth.encoding import private_key_to_address
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def to_bytes(x):
|
||||
return x.encode('utf-8')
|
||||
|
||||
|
||||
class SQLKeystore(EthKeystore):
|
||||
|
||||
schema = [
|
||||
"""CREATE TABLE IF NOT EXISTS ethereum (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
key_ciphertext VARCHAR(256) NOT NULL,
|
||||
wallet_address_hex CHAR(40) NOT NULL
|
||||
);
|
||||
""",
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS ethereum_address_idx ON ethereum ( wallet_address_hex );
|
||||
""",
|
||||
]
|
||||
|
||||
def __init__(self, dsn, **kwargs):
|
||||
super(SQLKeystore, self).__init__()
|
||||
logg.debug('starting db session with dsn {}'.format(dsn))
|
||||
self.db_engine = create_engine(dsn)
|
||||
self.db_session = sessionmaker(bind=self.db_engine)()
|
||||
for s in self.schema:
|
||||
self.db_session.execute(s)
|
||||
self.db_session.commit()
|
||||
self.symmetric_key = kwargs.get('symmetric_key')
|
||||
|
||||
|
||||
def __del__(self):
|
||||
logg.debug('closing db session')
|
||||
self.db_session.close()
|
||||
|
||||
|
||||
def get(self, address, password=None):
|
||||
safe_address = strip_0x(address).lower()
|
||||
s = text('SELECT key_ciphertext FROM ethereum WHERE wallet_address_hex = :a')
|
||||
r = self.db_session.execute(s, {
|
||||
'a': safe_address,
|
||||
},
|
||||
)
|
||||
try:
|
||||
k = r.first()[0]
|
||||
except TypeError:
|
||||
self.db_session.rollback()
|
||||
raise UnknownAccountError(safe_address)
|
||||
self.db_session.commit()
|
||||
a = self._decrypt(k, password)
|
||||
return a
|
||||
|
||||
|
||||
def import_key(self, pk, password=None):
|
||||
address_hex = private_key_to_address(pk)
|
||||
address_hex_clean = strip_0x(address_hex).lower()
|
||||
|
||||
c = self._encrypt(pk.secret, password)
|
||||
s = text('INSERT INTO ethereum (wallet_address_hex, key_ciphertext) VALUES (:a, :c)') #%s, %s)')
|
||||
self.db_session.execute(s, {
|
||||
'a': address_hex_clean,
|
||||
'c': c.decode('utf-8'),
|
||||
},
|
||||
)
|
||||
self.db_session.commit()
|
||||
logg.info('added private key for address {}'.format(address_hex_clean))
|
||||
return add_0x(address_hex)
|
||||
|
||||
|
||||
def _encrypt(self, private_key, password):
|
||||
f = self._generate_encryption_engine(password)
|
||||
return f.encrypt(private_key)
|
||||
|
||||
|
||||
def _generate_encryption_engine(self, password):
|
||||
h = sha3.keccak_256()
|
||||
h.update(self.symmetric_key)
|
||||
if password != None:
|
||||
password_bytes = to_bytes(password)
|
||||
h.update(password_bytes)
|
||||
g = h.digest()
|
||||
return Fernet(base64.b64encode(g))
|
||||
|
||||
|
||||
def _decrypt(self, c, password):
|
||||
f = self._generate_encryption_engine(password)
|
||||
return f.decrypt(c.encode('utf-8'))
|
||||
Reference in New Issue
Block a user