#! /bin/env python3 # MIT License # # Copyright (c) 2018 Ted Yin # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # # This little script offers decryption and verification of the existing # Ethereum wallets, as well as generation of a new wallet. You can use any # utf-8 string as the password, which could provide with better security # against the brute-force attack. # Use at your own risk. # # Example: # python ./ethy.py --verify-key # unlock the wallet and verify whether the # # encrypted private key matches the address # python ./ethy.py --show-key # reveal the private key (secp256k1) # # python ./ethy.py --gen > mywallet.json # generate a regular wallet (1s) # python ./ethy.py --gen --light > mywallet.json # generate a wallet (fast) import os, sys, json, argparse from hashlib import scrypt from uuid import uuid4 from getpass import getpass as _getpass from collections import Counter as Histogram from math import log from Crypto.Cipher import AES from Crypto.Util import Counter from sha3 import keccak_256 from ecdsa import SigningKey, SECP256k1 from base58 import b58encode err = sys.stderr import collections def entropy(data): n = len(data) return sum([-p * log(p, 2) for p in [c / n for _,c in Histogram(data).items()]]) def getpass(): return _getpass().encode('utf-8') def getpass2(): passwd = _getpass('Enter your wallet password (utf-8): ') rpasswd = _getpass('Repeat the password: ') if passwd != rpasswd: err.write("Mismatching passwords.") sys.exit(1) return passwd.encode('utf-8') def getseed(): passwd = _getpass('Enter some arbitrary seed (utf-8): ') return passwd.encode('utf-8') def generate_mac(derived_key, encrypted_private_key): m = keccak_256() m.update(derived_key[len(derived_key) >> 1:]) m.update(encrypted_private_key) return m.digest() def generate_addr(pub_key): m = keccak_256() m.update(pub_key) return m.hexdigest()[24:] def decrypt(passwd=None, salt=None, n=None, r=None, p=None, dklen=None, iv=None, enc_pk=None): # decrypt the ase-128-ctr to have the secp256k1 private key # use scrypt for key derivation m = 128 * r * (n + p + 2) dk = scrypt(passwd, salt=salt, n=n, r=r, p=p, dklen=dklen, maxmem=m) obj = AES.new(dk[:dklen >> 1], mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=int.from_bytes(iv, 'big'))) priv_key = obj.decrypt(enc_pk) # generate the mac mac = generate_mac(dk, enc_pk) return priv_key, mac def encrypt(passwd=None, salt=None, n=None, r=None, p=None, dklen=None, iv=None, priv_key=None): # decrypt the ase-128-ctr to have the secp256k1 private key # use scrypt for key derivation m = 128 * r * (n + p + 2) dk = scrypt(passwd, salt=salt, n=n, r=r, p=p, dklen=dklen, maxmem=m) obj = AES.new(dk[:dklen >> 1], mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=int.from_bytes(iv, 'big'))) enc_pk = obj.encrypt(priv_key) # generate the mac mac = generate_mac(dk, enc_pk) return enc_pk, mac def show_entropy(bytes, prompt="pass"): pwd_len = len(bytes) pwd_ent = entropy(bytes) err.write("{0} length = {1} bytes\n" "{0} entropy = {2}, {3:.2f}%\n".format(prompt, pwd_len, pwd_ent, pwd_ent / log(pwd_len, 2) * 100)) def save_to_file(sk, priv_key, passwd, fs): pub_key = sk.get_verifying_key().to_string() iv = os.urandom(16) salt = os.urandom(16) if args.light: n = 1 << 12 p = 6 else: n = 1 << 18 p = 1 r = 8 show_entropy(passwd) enc_pk, mac = encrypt(passwd=passwd, iv=iv, priv_key=priv_key, salt=salt, n=n, r=r, p=p, dklen=32) addr = generate_addr(pub_key) if args.show_key: err.write("> private key: {}\n".format(priv_key.hex())) err.write("> public key: {}\n".format(pub_key.hex())) err.write("> address: {}\n".format(addr)) crypto = { 'ciphertext': enc_pk.hex(), 'cipherparams': {'iv': iv.hex()}, 'cipher': 'aes-128-ctr', 'kdf': 'scrypt', 'kdfparams': {'dklen': 32, 'salt': salt.hex(), 'n': n, 'r': r, 'p': p}, 'mac': mac.hex() } output = {'version': 3, 'id': str(uuid4()), 'address': addr, 'Crypto': crypto} fs.write(json.dumps(output)) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Decrypt/verify the Ethereum UTC JSON keystore file') parser.add_argument('input', metavar='INPUT', type=str, nargs='?', help='the keystore file') parser.add_argument('--verify-key', action='store_true', default=False) parser.add_argument('--show-key', action='store_true', default=False) parser.add_argument('--use-new-iv', action='store_true', default=False) parser.add_argument('--gen', action='store_true', default=False, help='generate a new wallet') parser.add_argument('--gen-batch', action='store', default=None, type=int, help='generate some wallets') parser.add_argument('--with-key', action='store_true', default=False, help='use the specified key') parser.add_argument('--light', action='store_true', default=False, help='use n = 4096 for --gen') parser.add_argument('--force', action='store_true', default=False, help='bypass the password check') args = parser.parse_args() if args.gen_batch: if args.gen_batch < 1: err.write("invalid argument") sys.exit(1) seed = getseed() show_entropy(seed, "seed") h = keccak_256() h.update(seed) h2 = keccak_256() h2.update(h.digest()) wid = h2.hexdigest()[32:] for i in range(args.gen_batch): h = keccak_256() h.update(seed) h.update("\0".encode("utf-8")) h.update(str(i).encode("utf-8")) password = b58encode(h.digest()) sk = SigningKey.generate(curve=SECP256k1) priv_key = sk.to_string() wname = "wallet-{}-{:03d}".format(wid, i) f = open("{}.json".format(wname), "w") save_to_file(sk, priv_key, password, f) sys.stdout.write("password({}) = {}\n".format(wname, password.decode("ascii"))) sys.exit(0) elif args.gen: if args.with_key: err.write('Please enter the private key (hex): ') hex_key = input().strip() if len(hex_key) == 66: hex_prefix = hex_key[:2] if hex_prefix == '0x' or hex_prefix == '0X': hex_key = hex_key[2:] if len(hex_key) != 64: err.write("invalid private key hex length\n") sys.exit(1) try: priv_key = bytes.fromhex(hex_key) except ValueError: err.write("not a valid hex key\n") sys.exit(1) sk = SigningKey.from_string(priv_key, curve=SECP256k1) else: sk = SigningKey.generate(curve=SECP256k1) priv_key = sk.to_string() save_to_file(sk, priv_key, getpass2(), sys.stdout) sys.exit(0) if args.input: with open(args.input, "r") as f: parsed = json.load(f) else: parsed = json.load(sys.stdin) c = parsed['Crypto'] enc_pk = bytes.fromhex(c['ciphertext']) iv = bytes.fromhex(c['cipherparams']['iv']) kdf = c['kdfparams'] salt = bytes.fromhex(kdf['salt']) dklen = int(kdf['dklen']) n = int(kdf['n']) r = int(kdf['r']) p = int(kdf['p']) passwd = getpass() priv_key, mac = decrypt(passwd=passwd, iv=iv, enc_pk=enc_pk, salt=salt, n=n, r=r, p=p, dklen=dklen) if c['mac'] == mac.hex(): print("-- password is correct --") else: print("!! possibly WRONG password !!") if not args.force: sys.exit(1) if args.show_key: print("> private key: {}".format(priv_key.hex())) if args.verify_key: # derive public key and address from the decrypted private key sk = SigningKey.from_string(priv_key, curve=SECP256k1) pub_key = sk.get_verifying_key().to_string() print("> public key: {}".format(pub_key.hex())) addr = generate_addr(pub_key) print("> address: {}".format(addr)) if parsed['address'] == addr: print("-- private key matches address --") else: print("!! private key does NOT match the address !!") if not args.force: sys.exit(1) # generate a new encrypted wallet if args.use_new_iv: iv = os.urandom(16) enc_pk2, mac2 = encrypt(passwd=passwd, iv=iv, priv_key=priv_key, salt=salt, n=n, r=r, p=p, dklen=32) parsed['id'] = str(uuid4()) c['ciphertext'] = enc_pk2.hex() c['cipherparams']['iv'] = iv.hex() kdf['dklen'] = 32 c['mac'] = mac2.hex() print(json.dumps(parsed))