aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--bech32.py123
-rwxr-xr-xkeytree.py225
-rw-r--r--setup.py11
4 files changed, 362 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d1a15e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+__pycache__/
diff --git a/bech32.py b/bech32.py
new file mode 100644
index 0000000..d450080
--- /dev/null
+++ b/bech32.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2017 Pieter Wuille
+#
+# 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.
+
+"""Reference implementation for Bech32 and segwit addresses."""
+
+
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+
+
+def bech32_polymod(values):
+ """Internal function that computes the Bech32 checksum."""
+ generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1ffffff) << 5 ^ value
+ for i in range(5):
+ chk ^= generator[i] if ((top >> i) & 1) else 0
+ return chk
+
+
+def bech32_hrp_expand(hrp):
+ """Expand the HRP into values for checksum computation."""
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def bech32_verify_checksum(hrp, data):
+ """Verify a checksum given HRP and converted data characters."""
+ return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
+
+
+def bech32_create_checksum(hrp, data):
+ """Compute the checksum values given HRP and data."""
+ values = bech32_hrp_expand(hrp) + data
+ polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
+ return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
+
+
+def bech32_encode(hrp, data):
+ """Compute a Bech32 string given HRP and data values."""
+ combined = data + bech32_create_checksum(hrp, data)
+ return hrp + '1' + ''.join([CHARSET[d] for d in combined])
+
+
+def bech32_decode(bech):
+ """Validate a Bech32 string, and determine HRP and data."""
+ if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
+ (bech.lower() != bech and bech.upper() != bech)):
+ return (None, None)
+ bech = bech.lower()
+ pos = bech.rfind('1')
+ if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
+ return (None, None)
+ if not all(x in CHARSET for x in bech[pos+1:]):
+ return (None, None)
+ hrp = bech[:pos]
+ data = [CHARSET.find(x) for x in bech[pos+1:]]
+ if not bech32_verify_checksum(hrp, data):
+ return (None, None)
+ return (hrp, data[:-6])
+
+
+def convertbits(data, frombits, tobits, pad=True):
+ """General power-of-2 base conversion."""
+ acc = 0
+ bits = 0
+ ret = []
+ maxv = (1 << tobits) - 1
+ max_acc = (1 << (frombits + tobits - 1)) - 1
+ for value in data:
+ if value < 0 or (value >> frombits):
+ return None
+ acc = ((acc << frombits) | value) & max_acc
+ bits += frombits
+ while bits >= tobits:
+ bits -= tobits
+ ret.append((acc >> bits) & maxv)
+ if pad:
+ if bits:
+ ret.append((acc << (tobits - bits)) & maxv)
+ elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
+ return None
+ return ret
+
+
+def decode(hrp, addr):
+ """Decode a segwit address."""
+ hrpgot, data = bech32_decode(addr)
+ if hrpgot != hrp:
+ return (None, None)
+ decoded = convertbits(data[1:], 5, 8, False)
+ if decoded is None or len(decoded) < 2 or len(decoded) > 40:
+ return (None, None)
+ if data[0] > 16:
+ return (None, None)
+ if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
+ return (None, None)
+ return (data[0], decoded)
+
+
+def encode(hrp, witver, witprog):
+ """Encode a segwit address."""
+ ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
+ if decode(hrp, ret) == (None, None):
+ return None
+ return ret
diff --git a/keytree.py b/keytree.py
new file mode 100755
index 0000000..d1c4ad3
--- /dev/null
+++ b/keytree.py
@@ -0,0 +1,225 @@
+#! /bin/env python3
+# 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:
+# python ./keytree.py
+
+import sys
+import argparse
+import hashlib
+import hmac
+import unicodedata
+import bech32
+from getpass import getpass
+from ecdsa import SigningKey, VerifyingKey, SECP256k1
+from ecdsa.ecdsa import generator_secp256k1
+from ecdsa.ellipticcurve import INFINITY
+from base58 import b58encode
+from sha3 import keccak_256
+import re
+
+err = sys.stderr
+
+
+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 BIP32Error(Exception):
+ 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:
+ 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 BIP32Error("unsupported path format")
+ 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 BIP32Error("unsupported path format")
+ 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 BIP32Error("unsupported path format")
+
+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")
+
+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('--show-private', action='store_true', default=False, help='also show private keys')
+ 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')
+ 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)')
+
+ args = parser.parse_args()
+
+ mnemonic = getpass('Enter the mnemonic: ')
+ seed = hashlib.pbkdf2_hmac('sha512', unicodedata.normalize('NFKD', mnemonic).encode("utf-8"), b"mnemonic", 2048)
+ gen = BIP32(seed)
+ if args.start_idx < 0 or args.end_idx < 0:
+ sys.exit(1)
+ 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('avax', 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)))
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..239f572
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,11 @@
+from setuptools import setup
+
+setup(name='keytree.py',
+ version='0.2',
+ description='Derive BIP32 key pairs from BIP39 mnemonic',
+ url='http://github.com/Determinant/keytree.py',
+ author='Ted Yin',
+ author_email='[email protected]',
+ license='MIT',
+ scripts=['keytree.py'],
+ install_requires=['ecdsa', 'base58', 'pysha3'])