diff options
Diffstat (limited to 'frozen_deps/bin')
-rwxr-xr-x | frozen_deps/bin/base58 | 8 | ||||
-rwxr-xr-x | frozen_deps/bin/keytree.py | 290 |
2 files changed, 298 insertions, 0 deletions
diff --git a/frozen_deps/bin/base58 b/frozen_deps/bin/base58 new file mode 100755 index 0000000..1e291f0 --- /dev/null +++ b/frozen_deps/bin/base58 @@ -0,0 +1,8 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from base58.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/frozen_deps/bin/keytree.py b/frozen_deps/bin/keytree.py new file mode 100755 index 0000000..1f4bedb --- /dev/null +++ b/frozen_deps/bin/keytree.py @@ -0,0 +1,290 @@ +#!/usr/bin/python +# MIT License +# +# Copyright (c) 2020 Ted Yin <[email protected]> +# +# 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: +# python3 ./keytree.py + +import os +import sys +if sys.version_info[1] < 7: + sys.write("Python should be >= 3.7") + sys.exit(1) +basedir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, basedir + "/frozen_deps") + +import re +import argparse +import hashlib +import hmac +import unicodedata +import json +from getpass import getpass + +import bech32 +import mnemonic +from ecdsa import SigningKey, VerifyingKey, SECP256k1 +from ecdsa.ecdsa import generator_secp256k1 +from ecdsa.ellipticcurve import INFINITY +from base58 import b58encode, b58decode +from sha3 import keccak_256 +from Crypto.Cipher import AES + + +def sha256(data): + h = hashlib.sha256() + h.update(data) + return h.digest() + + +def ripemd160(data): + h = hashlib.new('ripemd160') + h.update(data) + return h.digest() + + +class KeytreeError(Exception): + pass + + +class BIP32Error(KeytreeError): + pass + + +# point(p): returns the coordinate pair resulting from EC point multiplication +# (repeated application of the EC group operation) of the secp256k1 base point +# with the integer p. +def point(p): + return generator_secp256k1 * p + + +# ser32(i): serialize a 32-bit unsigned integer i as a 4-byte sequence, most +# significant byte first. +def ser32(i): + return i.to_bytes(4, byteorder='big') + + +# ser256(p): serializes the integer p as a 32-byte sequence, most significant +# byte first. +def ser256(p): + return p.to_bytes(32, byteorder='big') + + +# serP(P): serializes the coordinate pair P = (x,y) as a byte sequence using +# SEC1's compressed form: (0x02 or 0x03) || ser256(x), where the header byte +# depends on the parity of the omitted y coordinate. +def serP(P): + if P.y() & 1 == 0: + parity = b'\x02' + else: + parity = b'\x03' + return parity + ser256(P.x()) + + +def is_infinity(P): + return P == INFINITY + + +# parse256(p): interprets a 32-byte sequence as a 256-bit number, most +# significant byte first. +def parse256(p): + assert(len(p) == 32) + return int.from_bytes(p, byteorder='big') + + +def iH(x): + return x + (1 << 31) + + +n = generator_secp256k1.order() +rformat = re.compile(r"^[0-9]+'?$") + + +def ckd_pub(K_par, c_par, i): + if i >= 1 << 31: + raise BIP32Error("the child is a hardended key") + I = hmac.digest( + c_par, serP(K_par) + ser32(i), 'sha512') + I_L, I_R = I[:32], I[32:] + K_i = point(parse256(I_L)) + K_par + c_i = I_R + if parse256(I_L) >= n or is_infinity(K_i): + raise BIP32Error("invalid i") + return K_i, c_i + +def ckd_prv(k_par, c_par, i): + if i >= 1 << 31: + I = hmac.digest( + c_par, b'\x00' + ser256(k_par) + ser32(i), 'sha512') + else: + I = hmac.digest( + c_par, serP(point(k_par)) + ser32(i), 'sha512') + I_L, I_R = I[:32], I[32:] + k_i = (parse256(I_L) + k_par) % n + c_i = I_R + if parse256(I_L) >= n or k_i == 0: + raise BIP32Error("invalid i") + return k_i, c_i + +class BIP32: + path_error = BIP32Error("unsupported BIP32 path format") + + def __init__(self, seed, key="Bitcoin seed"): + I = hmac.digest(b"Bitcoin seed", seed, 'sha512') + I_L, I_R = I[:32], I[32:] + self.m = parse256(I_L) + self.M = SigningKey.from_string(I_L, curve=SECP256k1) \ + .get_verifying_key().pubkey.point + self.c = I_R + + def derive(self, path="m"): + tokens = path.split('/') + if tokens[0] == "m": + k = self.m + c = self.c + for r in tokens[1:]: + if not rformat.match(r): + raise self.path_error + if r[-1] == "'": + i = iH(int(r[:-1])) + else: + i = int(r) + k, c = ckd_prv(k, c, i) + return SigningKey.from_string(k.to_bytes(32, byteorder='big'), curve=SECP256k1) + elif tokens[0] == "M": + K = self.M + c = self.c + for r in tokens[1:]: + if not rformat.match(r): + raise self.path_error + if r[-1] == "'": + i = iH(int(r[:-1])) + else: + i = int(r) + K, c = ckd_pub(K, c, i) + return VerifyingKey.from_public_point(K, curve=SECP256k1) + else: + raise self.path_error + +def get_eth_addr(pk): + pub_key = pk.to_string() + m = keccak_256() + m.update(pub_key) + return m.hexdigest()[24:] + +def get_privkey_btc(sk): + priv_key = b'\x80' + sk.to_string() + checksum = sha256(sha256(priv_key))[:4] + return b58encode(priv_key + checksum).decode("utf-8") + +def get_btc_addr(pk): + h = b'\x00' + ripemd160(sha256(b'\x04' + pk.to_string())) + checksum = sha256(sha256(h))[:4] + h += checksum + return b58encode(h).decode("utf-8") + +def load_from_keystore(filename): + try: + with open(filename, "r") as f: + try: + parsed = json.load(f) + ciphertext = b58decode(parsed['keys'][0]['key'])[:-4] + iv = b58decode(parsed['keys'][0]['iv'])[:-4] + salt = b58decode(parsed['salt'])[:-4] + tag = b58decode(parsed['pass_hash'])[:-4] + passwd = getpass('Enter the password to unlock keystore: ').encode('utf-8') + key = hashlib.pbkdf2_hmac( + 'sha256', + sha256(passwd + salt), salt, 200000) + obj = AES.new(key, + mode=AES.MODE_GCM, + nonce=iv) + if tag != sha256(passwd + sha256(passwd + salt)): + raise KeytreeError("incorrect keystore password") + return obj.decrypt(ciphertext[:-16]).decode('utf-8') + except KeytreeError as e: + raise e + except: + raise KeytreeError("invalid or corrupted keystore file") + except FileNotFoundError: + raise KeytreeError("failed to open file") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Derive BIP32 key pairs from BIP39 mnemonic') + parser.add_argument('--from-avax-keystore', type=str, default=None, help='load mnemonic from an AVAX keystore file') + parser.add_argument('--show-private', action='store_true', default=False, help='also show private keys and the mnemonic') + parser.add_argument('--custom-words', action='store_true', default=False, help='use an arbitrary word combination as mnemonic') + parser.add_argument('--account-path', default="44'/9000'/0'/0", help="path prefix for key deriving (e.g. \"0/1'/2\")") + parser.add_argument('--gen-mnemonic', action='store_true', default=False, help='generate a mnemonic (instead of taking an input)') + parser.add_argument('--lang', type=str, default="english", help='language for mnemonic words') + parser.add_argument('--start-idx', type=int, default=0, help='the start index for keys') + parser.add_argument('--end-idx', type=int, default=1, help='the end index for keys (exclusive)') + parser.add_argument('--hrp', type=str, default="avax", help='HRP (Human Readable Prefix, defined by Bech32)') + + args = parser.parse_args() + + + try: + try: + if args.gen_mnemonic: + mgen = mnemonic.Mnemonic(args.lang) + words = mgen.generate(256) + else: + if args.from_avax_keystore: + words = load_from_keystore(args.from_avax_keystore) + else: + words = getpass('Enter the mnemonic: ').strip() + if not args.custom_words: + mchecker = mnemonic.Mnemonic(args.lang) + if not mchecker.check(words): + raise KeytreeError("invalid mnemonic") + except FileNotFoundError: + raise KeytreeError("invalid language") + if args.show_private or args.gen_mnemonic: + print("KEEP THIS PRIVATE: {}".format(words)) + seed = hashlib.pbkdf2_hmac('sha512', unicodedata.normalize('NFKD', words).encode("utf-8"), b"mnemonic", 2048) + gen = BIP32(seed) + if args.start_idx < 0 or args.end_idx < 0: + raise KeytreeError("invalid start/end index") + for i in range(args.start_idx, args.end_idx): + path = "m/{}/{}".format(args.account_path, i) + priv = gen.derive(path) + pub = priv.get_verifying_key() + cpub = pub.to_string(encoding="compressed") + if args.show_private: + print("{}.priv(raw) {}".format(i, priv.to_string().hex())) + print("{}.priv(BTC) {}".format(i, get_privkey_btc(priv))) + print("{}.addr(AVAX) X-{}".format(i, bech32.bech32_encode(args.hrp, bech32.convertbits(ripemd160(sha256(cpub)), 8, 5)))) + print("{}.addr(BTC) {}".format(i, get_btc_addr(pub))) + print("{}.addr(ETH) {}".format(i, get_eth_addr(pub))) + except KeytreeError as e: + sys.stderr.write("error: {}\n".format(str(e))) + sys.exit(1) |