Initial commit

This commit is contained in:
nolash
2021-10-10 18:14:34 +02:00
commit 7753247afb
34 changed files with 1859 additions and 0 deletions

View 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

View 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)

View 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)

View 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
View 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'))