Initial commit. Basic keypair utils, BIP32, initial BIP39, ecdsa functions, and base58 encode/decode.

This commit is contained in:
Josh K 2019-07-25 16:00:22 -04:00
commit fc0bb21e7f
9 changed files with 354 additions and 0 deletions

28
LICENSE.txt Normal file
View File

@ -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.

48
base58.py Normal file
View File

@ -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

71
bip32.py Normal file
View File

@ -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)

BIN
bip39-en.txt.gz Normal file

Binary file not shown.

37
bip39.py Normal file
View File

@ -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

62
ecdsa.py Normal file
View File

@ -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)

49
keys.py Normal file
View File

@ -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 )

47
main.py Executable file
View File

@ -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()}' )
"""

12
util.py Normal file
View File

@ -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' )