Skip to content

Commit c2eb216

Browse files
committed
add support of BIP32 key derivation
1 parent d606809 commit c2eb216

File tree

1 file changed

+103
-3
lines changed

1 file changed

+103
-3
lines changed

pywallet.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
except:
3737
print("Json or simplejson package is needed")
3838

39+
import hmac
3940
import getpass
4041
import logging
4142
import struct
@@ -95,6 +96,8 @@
9596
prekeys = ["308201130201010420".decode('hex'), "308201120201010420".decode('hex')]
9697
postkeys = ["a081a530".decode('hex'), "81a530".decode('hex')]
9798

99+
KeyInfo = collections.namedtuple('KeyInfo', 'secret private_key public_key addr wif')
100+
98101
def plural(a):
99102
if a>=2:return 's'
100103
return ''
@@ -2622,6 +2625,9 @@ def parse_private_key(sec, force_compressed=None):
26222625
return (pkey, compressed)
26232626

26242627
def keyinfo(sec, network=None, print_info=False, force_compressed=None):
2628+
if sec.__class__ == Xpriv:
2629+
assert sec.ktype == 0
2630+
return keyinfo(sec.key.encode('hex'), network, print_info, True)
26252631
network = network or network_bitcoin
26262632
(pkey, compressed) = parse_private_key(sec, force_compressed)
26272633
if not pkey:
@@ -2632,6 +2638,7 @@ def keyinfo(sec, network=None, print_info=False, force_compressed=None):
26322638
public_key = GetPubKey(pkey, compressed)
26332639
addr = public_key_to_bc_address(public_key, network.p2pkh_prefix)
26342640
ser_public_key = (b'%02d%.64x'%(4 if not compressed else 2+(pkey.pubkey.point.y()&1), pkey.pubkey.point.x()) + (b'%.64x'%pkey.pubkey.point.y())*int(not compressed)).decode('hex')
2641+
wif = SecretToASecret(secret, compressed) if network.wif_prefix else None
26352642

26362643
if print_info:
26372644
print("Network: %s"%network.name)
@@ -2649,7 +2656,7 @@ def keyinfo(sec, network=None, print_info=False, force_compressed=None):
26492656
else:
26502657
print("P2WPKH unavailable: unknown network SegWit HRP")
26512658
if network.wif_prefix != None:
2652-
print("Privkey: %s"%(SecretToASecret(secret, compressed)))
2659+
print("Privkey: %s"%wif)
26532660
else:
26542661
print("Privkey unavailable: unknown network WIF prefix")
26552662
print("Hexprivkey: %s"%(secret.encode('hex')))
@@ -2660,10 +2667,14 @@ def keyinfo(sec, network=None, print_info=False, force_compressed=None):
26602667
if int(secret.encode('hex'), 16)>_r:
26612668
print('/!\\ Beware, 0x%s is equivalent to 0x%.33x'%(secret.encode('hex'), int(secret.encode('hex'), 16)-_r))
26622669

2663-
return (secret, private_key, public_key, addr)
2670+
return KeyInfo(secret, private_key, public_key, addr, wif)
26642671

26652672
def importprivkey(db, sec, label, reserve, verbose=True):
2666-
(secret, private_key, public_key, addr) = keyinfo(sec, network, verbose)
2673+
k = keyinfo(sec, network, verbose)
2674+
secret = k.secret
2675+
private_key = k.private_key
2676+
public_key = k.public_key
2677+
addr = k.addr
26672678

26682679
global crypter, passphrase, json_db
26692680
crypted = False
@@ -3228,9 +3239,93 @@ def whitepaper():
32283239
f.write(content)
32293240
print("Wrote the Bitcoin whitepaper to %s.pdf"%filename)
32303241

3242+
class Xpriv(collections.namedtuple('Xpriv', 'version depth prt_fpr childnr cc ktype key')):
3243+
xpriv_fmt = '>IB4sI32sB32s'
3244+
def __init__(self, *a, **kw):
3245+
super(Xpriv, self).__init__(*a, **kw)
3246+
self.fullpath = 'm'
3247+
@classmethod
3248+
def xpriv_version_bytes(cls):return 0x0488ADE4
3249+
@classmethod
3250+
def xpub_version_bytes(cls):return 0x0488B21E
3251+
@classmethod
3252+
def from_seed(cls, s):
3253+
I = hmac.new(b'Bitcoin seed', s, digestmod=hashlib.sha512).digest()
3254+
mk, cc = I[:32], I[32:]
3255+
return cls(cls.xpriv_version_bytes(), 0, '\x00'*4, 0, cc, 0, mk)
3256+
def clone(self):
3257+
return self.__class__.b58decode(self.b58encode())
3258+
def b58encode(self):
3259+
return EncodeBase58Check(struct.pack(self.xpriv_fmt, *self._asdict().values()))
3260+
def xpub(self):
3261+
pubk = keyinfo(self, None, False, True).public_key
3262+
xpub_content = self.clone()._replace(version=self.xpub_version_bytes(), ktype=ord(pubk[0]), key=pubk[1:])
3263+
return EncodeBase58Check(struct.pack(self.xpriv_fmt, *xpub_content))
3264+
@classmethod
3265+
def b58decode(cls, b58xpriv):
3266+
return cls(*struct.unpack(cls.xpriv_fmt, DecodeBase58Check(b58xpriv)))
3267+
def multi_ckd_xpriv(self, str_path):
3268+
str_path = str_path.lstrip('m/')
3269+
path_split = []
3270+
for j in str_path.split('/'):
3271+
if not j:continue
3272+
hardened = 0
3273+
if j.endswith("'") or j.lower().endswith("H"):
3274+
hardened = 0x80000000
3275+
j = j[:-1]
3276+
try:
3277+
path_split.append([int(j)+hardened])
3278+
except:
3279+
a, b = map(int, j.split('-'))
3280+
path_split.append(list(range(a+hardened, b+1+hardened)))
3281+
rev_path_split = path_split[::-1]
3282+
xprivs = [self]
3283+
while rev_path_split:
3284+
children_nrs = rev_path_split.pop()
3285+
xprivs = [parent.ckd_xpriv(child_nr) for parent in xprivs for child_nr in children_nrs]
3286+
return xprivs
3287+
def set_fullpath(self, base, x):
3288+
self.fullpath = base + '/' + ("%d'"%(x-0x80000000) if x>=0x80000000 else "%d"%x)
3289+
return self
3290+
def ckd_xpriv(self, *indexes):
3291+
if indexes.__class__ != tuple:
3292+
indexes = [indexes]
3293+
i = indexes[0]
3294+
if i<0:
3295+
i = 0x80000000-i
3296+
assert self.ktype == 0
3297+
par_pubk = keyinfo(self, None, False, True).public_key
3298+
seri = struct.pack('>I',i)
3299+
if i>=0x80000000:
3300+
I = hmac.new(self.cc, '\x00'+self.key+seri, digestmod=hashlib.sha512).digest()
3301+
else:
3302+
I = hmac.new(self.cc, par_pubk+seri, digestmod=hashlib.sha512).digest()
3303+
il, ir = I[:32], I[32:]
3304+
pk = hex((int(il.encode('hex'), 16) + int(self.key.encode('hex'), 16))%_r)[2:].replace('L', '').zfill(64)
3305+
child = self.__class__(self.version, self.depth+1, hash_160(par_pubk)[:4], i, ir, 0, pk.decode('hex')).set_fullpath(self.fullpath, i)
3306+
if len(indexes)>=2:
3307+
return child.ckd_xpriv(*indexes[1:])
3308+
return child
3309+
def hprivcontent(self):
3310+
return DecodeBase58Check(self.b58encode()).encode('hex')
3311+
def hpubcontent(self):
3312+
return DecodeBase58Check(self.xpub()).encode('hex')
3313+
3314+
def dump_bip32_privkeys(xpriv, paths, format):
3315+
dump_key = lambda x:x.wif
3316+
if format == 'addr':dump_key = lambda x:x.addr
3317+
for child in Xpriv.b58decode(xpriv).multi_ckd_xpriv(paths):
3318+
print('%s: %s'%(child.fullpath, dump_key(keyinfo(child))))
3319+
32313320
if __name__ == '__main__':
32323321
parser = OptionParser(usage="%prog [options]", version="%prog 1.1")
32333322

3323+
parser.add_option("--dump_bip32", nargs=2,
3324+
help="dump the keys from a xpriv and a path, usage: --dump_bip32 xprv9s21ZrQH143K m/0H/1-2/2H/2-4")
3325+
3326+
parser.add_option("--bip32_format",
3327+
help="format of dumped bip32 keys")
3328+
32343329
parser.add_option("--passphrase", dest="passphrase",
32353330
help="passphrase for the encrypted wallet")
32363331

@@ -3324,6 +3419,11 @@ def whitepaper():
33243419
# if options.forcerun is None:
33253420
# exit(0)
33263421

3422+
if options.dump_bip32:
3423+
print("Warning: single quotes (') may be parsed by your terminal, please use \"H\" for hardened keys")
3424+
dump_bip32_privkeys(*options.dump_bip32, format=options.bip32_format)
3425+
exit()
3426+
33273427
if options.whitepaper:
33283428
whitepaper()
33293429
exit()

0 commit comments

Comments
 (0)