From c14b910e51b645ae5f196eb33c54ed80dfb44805 Mon Sep 17 00:00:00 2001 From: Determinant Date: Sat, 24 Mar 2018 18:31:06 -0400 Subject: init --- .gitignore | 2 + README.rst | 41 +++++++++++++ ethy.py | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 15 +++++ 4 files changed, 258 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100644 ethy.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52e4e61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.pyo diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3029188 --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ +eth.py +------ + +This little script offers decryption and verification of some 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. + +Feature +------- + +- Decrypt the UTC JSON wallet and see the private key +- Run secp256k1 to make sure the private is intact and really corresponds to the wallet address +- Generate a wallet + +Dependencies +------------ + +- pycrypto +- pysha3 +- ecdsa + +Install +------- +:: + + git clone https://github.com/Determinant/ethy.py + pip install --user ethy.py + +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) diff --git a/ethy.py b/ethy.py new file mode 100644 index 0000000..b6eb735 --- /dev/null +++ b/ethy.py @@ -0,0 +1,200 @@ +#! /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 some 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 Crypto.Cipher import AES +from Crypto.Util import Counter +from sha3 import keccak_256 +from ecdsa import SigningKey, SECP256k1 + +err = sys.stderr +def getpass(): + 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 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 + +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('--force', action='store_true', default=False) + parser.add_argument('--gen', action='store_true', default=False, help='generate a new wallet') + parser.add_argument('--light', action='store_true', default=False, help='use n = 4096 for --gen') + + args = parser.parse_args() + + if args.gen: + sk = SigningKey.generate(curve=SECP256k1) + priv_key = sk.to_string() + pub_key = sk.get_verifying_key().to_string() + + iv = os.urandom(16) + salt = os.urandom(16) + n = 1 << (12 if args.light else 18) + r = 8 + p = 1 + passwd = getpass() + err.write("your plain-text password has {} bytes\n".format(len(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} + sys.stdout.write(json.dumps(output)) + 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)) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c50a783 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup(name='ethy.py', + version='0.1', + description='Decryption/verification/generation of Ethereum wallets', + url='http://github.com/Determinant/ethy.py', + author='Ted Yin', + author_email='tederminant@gmail.com', + license='MIT', + scripts=['ethy.py'], + install_requires=[ + 'pycrypto', + 'pysha3', + 'ecdsa' + ]) -- cgit v1.2.3