Original question:
Python PBKDF2 using core modules
I'm looking for commentary on the following code, which converts from a hex seed to mnemonic (12 word phrase), and vice versa. It's for Bitcoin (BIP39 to be exact), and is going to be running in conjunction with pybitcointools, however I believe the commented URLs should be somewhat self-explanatory.
Background info:
In essence, a hex value (the seed) is converted to binary, divided into chunks of 11-bits, and the base 10 value of each binary chunk is used as an index in a list of 2048 words. So if the first chunk is 00000000001
, it's the word @ index 1 (the 2nd word); ability
. Or, 11111111111
= the last word, zoo
. This is BIP39; where a large hex value (a private key, perhaps) is encoded with an easy(ier) to remember phrase.
#!/usr/bin/python
import hashlib, re
from binascii import hexlify, unhexlify
# get the 2048 word wordlist
def get_wordlist():
try:
# https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
BIP0039_ENG_WORDLIST = make_request(
"https://raw.githubusercontent.com/bitcoin/"
"bips/master/bip-0039/english.txt").split('\n')
assert len(BIP0039_ENG_WORDLIST) == 2048
except:
raise IOError("Cannot get BIP39 word list")
return BIP0039_ENG_WORDLIST
BIP39WORDS = get_wordlist()
def bip39_seed_to_mnemonic(hexseed):
"""
Convert hex seed to mnemonic representation (BIP39)
https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
Essentially converts hex value to binary (with appended checksum),
and splits into 11-bit binary chunks, each indexing a 2048 (=2**11)
word list (in BIP39WORDS)
hexseed: hexadecimal bytes or bytearray object
>>> bip39_seed_to_mnemonic(b'eaebabb2383351fd31d703840b32e9e2')
'turtle front uncle idea crush write shrug there lottery flower risk shell'
"""
if isinstance(hexseed, (bytes, str)) and re.match('^[0-9a-fA-F]*$', hexseed):
hexseed = from_string_to_bytes(str(hexseed))
else:
raise TypeError("Enter a hex seed!")
hexseed = unhexlify(hexseed)
hexbin = changebase( hexlify(hexseed), 16, 2, len(hexseed)*8)
if len(hexseed) % 4:
raise Exception("Seed not a multiple of 4 bytes!")
elif len(hexseed) < 4:
raise Exception("Seed must be at least 32 bits of entropy")
elif len(hexseed) > 124:
raise Exception("Seed cannot exceed 992 bits of entropy")
checksum_length = int((len(hexseed) * 8) // 32)
checksum = hashlib.sha256(hexseed).hexdigest() # sha256 hexdigest
checksum_bin = changebase(
checksum, 16, 2, len(unhexlify(checksum))*8)
binstr_final = from_string_to_bytes(
str(hexbin) + str(checksum_bin)[:checksum_length])
binlist_words = [binstr_final[i:i+11] for i in
range(0, len(binstr_final), 11)]
return " ".join( [BIP39WORDS[int(x, 2)] for x in binlist_words ] )
def bip39_mnemonic_to_seed(mnemonic):
"""
Convert BIP39 mnemonic phrase to hex seed (bytes)
See BIP39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
mnemonic: single spaced, lower-case words (bytes/bytearray object)
>>>bip39_mnemonic_to_seed(b"board flee heavy tunnel powder denial science ski answer betray cargo cat")
'18ab19a9f54a9274f03e5209a2ac8a91'
"""
if isinstance(mnemonic, string_types):
mnemonic = str(from_string_to_bytes(mnemonic)).lower().strip()
elif isinstance(mnemonic, list):
mnemonic = " ".join(str(from_string_to_bytes(mnemonic))).lower()
else:
raise TypeError("Enter a lower case, single-spaced mnemonic!")
try:
mnemonic_array = str(mnemonic).split(" ")
if mnemonic_array[0] is '': mnemonic_array.pop(0)
except:
raise TypeError("Enter a lower case, single-spaced mnemonic!")
if not (93 > len(mnemonic_array) > 3):
raise TypeError("32 < entropy < 992 bits; Input too big or too small")
if len(mnemonic_array) % 3:
raise TypeError("Too many or too few words")
assert all(map(lambda x: x in BIP39WORDS, mnemonic_array)) # check all words in list
binstr = ''.join([ changebase(str(BIP39WORDS.index(x)), 10, 2, 11) for x in mnemonic_array])
num_checksum_digits = len(binstr) % 32
binary_checksum = binstr[(len(binstr) - num_checksum_digits):]
binary_no_checksum = binstr[ : (-1*num_checksum_digits)]
hexoutput = hexlify(changebase(binary_no_checksum, 2, 16,
len(binary_checksum) * 8))
assert not (len(hexoutput) % 2)
checksum_bin = changebase(hashlib.sha256(
unhexlify(hexoutput)).hexdigest(), 16, 2, 256)
assert checksum_bin[0:int(num_checksum_digits)] != binary_checksum
return unhexlify(hexoutput)
And the imported modules from pybitcointools:
#pybitcointools code
import random
try:
from urllib.request import build_opener
except:
from urllib2 import build_opener
def from_string_to_bytes(a):
return a
# return a if isinstance(a, bytes) else bytes(a, 'utf-8')
code_strings = {
2: '01',
10: '0123456789',
16: '0123456789abcdef',
256: ''.join([chr(x) for x in range(256)])
}
def lpad(msg, symbol, length):
if len(msg) >= length:
return msg
return symbol * (length - len(msg)) + msg
def get_code_string(base):
if base in code_strings:
return code_strings[base]
else:
raise ValueError("Invalid base!")
def changebase(string, frm, to, minlen=0):
if frm == to:
return lpad(string, get_code_string(frm)[0], minlen)
return encode(decode(string, frm), to, minlen)
def make_request(*args):
opener = build_opener()
opener.addheaders = [('User-agent',
'Mozilla/5.0'+str(random.randrange(1000000)))]
try:
return opener.open(*args).read().strip()
except Exception as e:
try:
p = e.read().strip()
except:
p = e
raise Exception(p)
def encode(val, base, minlen=0):
base, minlen = int(base), int(minlen)
code_string = get_code_string(base)
result = ""
while val > 0:
result = code_string[val % base] + result
val //= base
return code_string[0] * max(minlen - len(result), 0) + result
def decode(string, base):
base = int(base)
code_string = get_code_string(base)
result = 0
if base == 16:
string = string.lower()
while len(string) > 0:
result *= base
result += code_string.find(string[0])
string = string[1:]
return result