4
\$\begingroup\$

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
\$\endgroup\$
2
\$\begingroup\$

Wildcard imports are considered bad practice. From PEP8:

Wildcard imports ( from <module> import * ) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.

It would be better to add the explicit names that you want to import. It also makes the code more readable.

The code could use more vertical spacing (blank lines) to create a visual grouping of closely related lines of code. In their current form, the two methods look like large blobs of code and not very easy to read.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the clarification. I'm quite new to this. I'll fix the aforementioned issues ASAP and edit accordingly. Thanks again \$\endgroup\$ – Wizard Of Ozzie Apr 22 '15 at 11:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.