Initial commit. Basic keypair utils, BIP32, initial BIP39, ecdsa functions, and base58 encode/decode.
This commit is contained in:
commit
fc0bb21e7f
|
@ -0,0 +1,28 @@
|
|||
Copyright (c) 2019, Slipyx (Josh K)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# base58
|
||||
from hashlib import sha256
|
||||
|
||||
from util import *
|
||||
|
||||
B58_STR = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
# encode int as base58 string
|
||||
def encodeInt( i ):
|
||||
outstr = ''
|
||||
while i > 0:
|
||||
outstr += B58_STR[i % 58]
|
||||
i = i // 58
|
||||
return outstr[::-1]
|
||||
|
||||
# return base58 encoding of version + payload + checksum bytes, leading zeros are removed and
|
||||
# reinserted as leading 1's in the final output
|
||||
def encodeCheck( ver, payload ):
|
||||
bs = ver + payload
|
||||
cksha = sha256( sha256( bs ).digest() ).digest()
|
||||
bs = bs + cksha[:4]
|
||||
|
||||
# strip and count leading zeros
|
||||
lzc = 0
|
||||
while bs[0] == 0: bs = bs[1:]; lzc += 1
|
||||
|
||||
b58str = encodeInt( int( bs.hex(), 16 ) )
|
||||
|
||||
# reinsert leading 1's
|
||||
for z in range( lzc ): b58str = '1' + b58str
|
||||
|
||||
return b58str
|
||||
|
||||
# decode base58Check string to bytes
|
||||
def decodeCheck( bstr ):
|
||||
lzc = 0
|
||||
while bstr[0] == '1': bstr = bstr[1:]; lzc += 1
|
||||
|
||||
outi = 0
|
||||
for c in bstr:
|
||||
outi = outi * 58 + B58_STR.index( c )
|
||||
|
||||
outb = intBytes( outi )
|
||||
|
||||
for z in range( lzc ): outb = (b'\x00' + outb)
|
||||
|
||||
return outb
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# BIP32
|
||||
from hashlib import sha512
|
||||
import hmac
|
||||
|
||||
from util import *
|
||||
import base58
|
||||
import ecdsa
|
||||
import keys
|
||||
|
||||
# get master (key, chain) of seed bytes
|
||||
def getMaster( seed ):
|
||||
l = hmac.new( b'Bitcoin seed', seed, sha512 ).digest()
|
||||
m = l[:32]
|
||||
c = l[32:]
|
||||
return (m, c)
|
||||
|
||||
# serialized version byte codes
|
||||
# 0488ADE4 - bitcoin prv
|
||||
# 02fac398 - dogecoin prv
|
||||
PRV_VER = bytes.fromhex( '02fac398' if DOGE_MODE else '0488ADE4' )
|
||||
# 0488B21E - bitcoin pub
|
||||
# 02facafd - dogecoin pub
|
||||
PUB_VER = bytes.fromhex( '02facafd' if DOGE_MODE else '0488B21E' )
|
||||
# return serialized master private key string from master private (key, chain) bytes pair
|
||||
def serializeMasterPrvKey( master ):
|
||||
depth = 0
|
||||
parFP = intBytes( 0, 4 ) # parent fingerprint
|
||||
childNum = 0
|
||||
prvroot = base58.encodeCheck( PRV_VER, intBytes( depth, 1 ) +
|
||||
parFP + intBytes( childNum, 4 ) + master[1] +
|
||||
b'\x00' + master[0] )
|
||||
return prvroot
|
||||
|
||||
# return serialized master public key string from master private (key, chain) bytes pair
|
||||
def serializeMasterPubKey( master ):
|
||||
depth = 0
|
||||
parFP = intBytes( 0, 4 ) # parent fingerprint
|
||||
childNum = 0
|
||||
pubroot = base58.encodeCheck( PUB_VER, intBytes( depth, 1 ) +
|
||||
parFP + intBytes( childNum, 4 ) + master[1] +
|
||||
keys.getPubKey( master[0] ) )
|
||||
return pubroot
|
||||
|
||||
# public parent -> public child.
|
||||
# pub is (key, chain) where key is the compressed pubkey bytes
|
||||
# returns (key, chain) of new key, key being compressed pubkey bytes too
|
||||
def ckdPub( pub, i ):
|
||||
pp = ecdsa.N
|
||||
while pp >= ecdsa.N:
|
||||
l = hmac.new( pub[1], pub[0] + intBytes( i, 4 ), sha512 ).digest()
|
||||
c = l[32:] # new chain
|
||||
pp = int( l[:32].hex(), 16 ) # new pseudo-private key
|
||||
kp = ecdsa.ecAdd( ecdsa.getPoint( pp ), ecdsa.decompressPoint( pub[0] ) )
|
||||
k = ecdsa.compressPoint( kp ) # new compressed pubkey
|
||||
|
||||
return (k, c)
|
||||
|
||||
# private parent -> private child.
|
||||
# prv is (key, chain) where key is prvkey bytes
|
||||
# returns (key, chain) of new key, key being prvkey bytes too
|
||||
def ckdPrv( prv, i ):
|
||||
k = 0
|
||||
pp = ecdsa.N
|
||||
while k == 0 or pp >= ecdsa.N:
|
||||
l = hmac.new( prv[1], keys.getPubKey( prv[0] ) + intBytes( i, 4 ), sha512 ).digest()
|
||||
c = l[32:] # new chain
|
||||
pp = int( l[:32].hex(), 16 )
|
||||
k = (pp + int( prv[0].hex(), 16 )) % ecdsa.N
|
||||
|
||||
return (intBytes( k, 32 ), c)
|
||||
|
Binary file not shown.
|
@ -0,0 +1,37 @@
|
|||
# bip39
|
||||
from hashlib import sha256, sha512, pbkdf2_hmac
|
||||
import hmac
|
||||
from os import path
|
||||
import gzip
|
||||
|
||||
# gen phrase from given entropy bytes
|
||||
def genPhrase( ent ):
|
||||
assert type( ent ) == bytes, 'Entropy must be a bytes object'
|
||||
|
||||
entbits = bin( int( ent.hex(), 16 ) )[2:].zfill( len( ent ) * 8 )
|
||||
entlen = len( entbits )
|
||||
|
||||
assert entlen % 32 == 0, 'Entropy must be multiple of 32 bits'
|
||||
#assert entlen >= 128 and entlen <= 256, 'Entropy must be in range 128-256 bits'
|
||||
|
||||
cs = bin( int( sha256( ent ).hexdigest(), 16 ) )[2:].zfill( 256 )[0:entlen // 32]
|
||||
entbits += cs
|
||||
|
||||
with gzip.open( path.join( path.dirname( __file__ ), 'bip39-en.txt.gz' ), 'rt' ) as wf:
|
||||
wlist = wf.readlines()
|
||||
|
||||
mphrase = ''
|
||||
# group of 11 bits for each word!
|
||||
while len( entbits ) > 0:
|
||||
wbits = entbits[:11]
|
||||
entbits = entbits[11:] # lpop it off
|
||||
mphrase += wlist[int( wbits, 2 )].strip() + ' '
|
||||
mphrase = mphrase.rstrip()
|
||||
|
||||
return mphrase
|
||||
|
||||
# gen seed bytes from phrase and optional password
|
||||
def genSeed( phrase, passwd='' ):
|
||||
s = pbkdf2_hmac( 'sha512', phrase.encode('utf-8'), b'mnemonic' + passwd.encode('utf-8'), 2048, 64 )
|
||||
return s
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# ECDSA
|
||||
from math import sqrt
|
||||
|
||||
# secp256k1 constants
|
||||
P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
|
||||
A = 0x0
|
||||
B = 0x7
|
||||
GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
||||
GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 # even
|
||||
GP = (int(GX), int(GY))
|
||||
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
|
||||
def modinv( a, n=P ):
|
||||
lm, hm = 1, 0
|
||||
low, high = a % n, n
|
||||
while low > 1:
|
||||
r = high // low
|
||||
nm, new = hm - lm * r, high - low * r
|
||||
lm, low, hm, high = nm, new, lm, low
|
||||
return lm % n
|
||||
|
||||
def ecAdd( a, b ):
|
||||
ladd = ((b[1] - a[1]) * modinv( b[0] - a[0], P )) % P
|
||||
x = (ladd * ladd - a[0] - b[0]) % P
|
||||
y = (ladd * (a[0] - x) - a[1]) % P
|
||||
return (x, y)
|
||||
|
||||
def ecDouble( a ):
|
||||
lam = ((3 * a[0] * a[0] + A) * modinv( (2 * a[1]), P )) % P
|
||||
x = (lam * lam - 2 * a[0]) % P
|
||||
y = (lam * (a[0] - x) - a[1]) % P
|
||||
return (x, y)
|
||||
|
||||
# multiply generator point by scalar
|
||||
def ecMult( gen, sc ):
|
||||
assert sc != 0 and sc < N, 'Invalid scalar'
|
||||
scbin = bin( sc )[2:]
|
||||
Q = gen
|
||||
for i in range( 1, len( scbin ) ):
|
||||
Q = ecDouble( Q )
|
||||
if scbin[i] == '1':
|
||||
Q = ecAdd( Q, gen )
|
||||
return Q
|
||||
|
||||
# helper for returning point result of gen point multiplied by int p
|
||||
def getPoint( p ): return ecMult( GP, p )
|
||||
|
||||
# return bytes of point p in compressed format. bip32 calls this serialization
|
||||
def compressPoint( p ):
|
||||
return bytes.fromhex( ('02' if p[1] % 2 == 0 else '03') + hex( p[0] )[2:].zfill( 64 ) )
|
||||
|
||||
# return point coord pair of compressed point bytes
|
||||
def decompressPoint( p ):
|
||||
x = int( p[1:].hex(), 16 )
|
||||
ys = (pow( x, 3, P ) + (A * x) + B) % P
|
||||
y = pow( ys, (P + 1) // 4, P )
|
||||
|
||||
if (y % 2) != (p[0] - 2):
|
||||
y = (-y) % P
|
||||
|
||||
return (x, y)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
# public and private key utilities
|
||||
# also addresses and hashes
|
||||
|
||||
from hashlib import new as newhash
|
||||
from hashlib import sha256
|
||||
from random import getrandbits
|
||||
|
||||
import base58
|
||||
import ecdsa
|
||||
from util import *
|
||||
|
||||
# configurable wif version byte
|
||||
# 0x80 - bitcoin
|
||||
# 0x9e - dogecoin
|
||||
WIF_VER = b'\x9e' if DOGE_MODE else b'\x80'
|
||||
|
||||
# configurable pub addr version byte
|
||||
# 0x00 - bitcoin
|
||||
# 0x1e - dogecoin
|
||||
ADDR_VER = b'\x1e' if DOGE_MODE else b'\x00'
|
||||
|
||||
# gen random 256bit privkey as bytes
|
||||
def genRandPrvKey():
|
||||
# replace with os.urandom for the real deal
|
||||
rawpk = intBytes( getrandbits( 256 ) )
|
||||
return rawpk
|
||||
|
||||
# get base58 wif privkey from raw privkey bytes
|
||||
def getPrvKeyWIF( rawpk ):
|
||||
return base58.encodeCheck( WIF_VER, rawpk + b'\x01' )
|
||||
|
||||
# get hash160 bytes of pubkey bytes. base58Check this for addr
|
||||
# also used for bip32 ext key identifier. first 32bits of id is the fingerprint
|
||||
def getPubKeyHash( pub ):
|
||||
rmd = newhash( 'ripemd160' )
|
||||
rmd.update( sha256( pub ).digest() )
|
||||
return rmd.digest()
|
||||
|
||||
# pubkey bytes to address, aka base58Check( pubKeyHash )
|
||||
def getPubKeyAddr( pub ):
|
||||
return base58.encodeCheck( ADDR_VER, getPubKeyHash( pub ) )
|
||||
|
||||
# get the compressed point pubkey bytes from prvkey bytes
|
||||
def getPubKey( prv ):
|
||||
# multiplies ecdsa generator point by prvkey
|
||||
pub = ecdsa.getPoint( int( prv.hex(), 16 ) )
|
||||
return ecdsa.compressPoint( pub )
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/python3 -tt
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from random import getrandbits
|
||||
|
||||
from util import *
|
||||
import base58
|
||||
import keys
|
||||
import bip32
|
||||
import bip39
|
||||
|
||||
if __name__ == '__main__':
|
||||
# test bip39
|
||||
wlen = 12
|
||||
b39phrase = bip39.genPhrase( intBytes( getrandbits( wlen // 3 * 32 ) ) )
|
||||
b39seed = bip39.genSeed( b39phrase )
|
||||
|
||||
# test bip32
|
||||
# master (key, chain) bytes
|
||||
b32master = bip32.getMaster( b39seed )
|
||||
# serialize root master prv and pub key
|
||||
b32masterxprv = bip32.serializeMasterPrvKey( b32master )
|
||||
b32masterxpub = bip32.serializeMasterPubKey( b32master )
|
||||
|
||||
print( 'BIP39\n', b39phrase + '\n', b39seed.hex() + '\nBIP32\n' +
|
||||
f'{b32masterxprv}\n{b32masterxpub}\n{base58.decodeCheck( b32masterxprv ).hex()}\n{base58.decodeCheck( b32masterxpub ).hex()}' )
|
||||
print()
|
||||
|
||||
prvmaster = (b32master[0], b32master[1])
|
||||
prvroot0 = bip32.ckdPrv( prvmaster, 0 ) # m/0
|
||||
for i in range( 20 ):
|
||||
# pubkeys
|
||||
pk = bip32.ckdPrv( prvroot0, i )[0]
|
||||
print( 'm/0/{}: {} - {}'.format( i, keys.getPubKey( pk ).hex(), keys.getPrvKeyWIF( pk ) ) )
|
||||
|
||||
"""
|
||||
pp = b'poop' #input( 'Phrase: ' ).encode()
|
||||
prvkey = b32master[0]
|
||||
pubkey = keys.getPubKey( prvkey )
|
||||
pubaddr = keys.getPubKeyAddr( pubkey )
|
||||
print( f'prvkey hex: {prvkey.hex()}' )
|
||||
print( f'b58 wif pk: {keys.getPrvKeyWIF( prvkey )}' )
|
||||
print( f'pubkey: {pubkey.hex()}\n\thash: {keys.getPubKeyHash( pubkey ).hex()}\n\taddr: {pubaddr}\n\tdecoded: {base58.decodeCheck( pubaddr ).hex()}' )
|
||||
"""
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# UTIL
|
||||
import sys
|
||||
|
||||
DOGE_MODE = False
|
||||
if len(sys.argv) > 0 and '-doge' in sys.argv: DOGE_MODE = True
|
||||
|
||||
# int to bytes object, optional bytelen param for forcing a specific byte length
|
||||
def intBytes( i, bytelen=None ):
|
||||
bl = (i.bit_length() + 7) // 8
|
||||
if bytelen and bytelen > bl: bl = bytelen
|
||||
return i.to_bytes( bl, 'big' )
|
||||
|
Loading…
Reference in New Issue