bip39 refactoring

This commit is contained in:
4tochka 2018-07-01 02:54:01 +04:00
parent 3dee268133
commit e9add2530d
20 changed files with 208 additions and 146 deletions

View File

@ -3,7 +3,7 @@ import random
import os
ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
BIP0039_DIR = os.path.normpath(os.path.join(ROOT_DIR, 'bip-0039'))
BIP0039_DIR = os.path.normpath(os.path.join(ROOT_DIR, 'bip39_word_list'))
MAX_AMOUNT = 2100000000000000
SIGHASH_ALL = 0x00000001

View File

@ -4,4 +4,5 @@ from .script import *
from .tools import *
from .hash import *
from .block import *
from .encode import *
from .encode import *
from .bip39_mnemonic import *

View File

@ -0,0 +1,157 @@
import os
import sys
import time
import random
from secp256k1 import ffi
parentPath = os.path.abspath("../..")
if parentPath not in sys.path:
sys.path.insert(0, parentPath)
from pybtc.constants import *
from .hash import *
from hashlib import pbkdf2_hmac
def generate_entropy(strength=256, hex=True):
"""
Generate 128-256 bits entropy bytes string
:param int strength: entropy bits strength, by default is 256 bit.
:param boolean hex: return HEX encoded string result flag, by default True.
:return: HEX encoded or bytes entropy string.
"""
if strength not in [128, 160, 192, 224, 256]:
raise ValueError('strength should be one of the following [128, 160, 192, 224, 256]')
a = random.SystemRandom().randint(0, MAX_INT_PRIVATE_KEY)
i = int((time.time() % 0.01 ) * 100000)
h = a.to_bytes(32, byteorder="big")
# more entropy from system timer and sha256 derivation
while i:
h = hashlib.sha256(h).digest()
i -= 1
if not i and int.from_bytes(h, byteorder="big") > MAX_INT_PRIVATE_KEY:
i += 1
return h[:int(strength/8)] if not hex else h[:int(strength/8)].hex()
def load_word_list(language='english', word_list_dir=None):
"""
Load the word list from local file.
:param str language: (optional) uses word list language (chinese_simplified, chinese_traditional, english, french,
italian, japanese, korean, spanish), by default is english.
:param str word_list_dir: (optional) path to a directory containing a list of words,
by default None (use BIP39 standard list)
:return: list of words.
"""
if not word_list_dir:
word_list_dir = BIP0039_DIR
path = os.path.join(word_list_dir, '.'.join((language, 'txt')))
if not os.path.exists(path):
raise ValueError("word list not exist")
with open(path) as f:
word_list = f.read().rstrip('\n').split('\n')
if len(word_list) != 2048:
raise ValueError("word list invalid, should contain 2048 words")
return word_list
def entropy_to_mnemonic(entropy, language='english', word_list_dir=None, word_list=None):
"""
Convert entropy to mnemonic words string.
:param str,bytes entropy: random entropy HEX encoded or bytes string.
:param str language: (optional) uses word list language (chinese_simplified, chinese_traditional, english, french,
italian, japanese, korean, spanish), by default is english.
:param str word_list_dir: (optional) path to a directory containing a list of words,
by default None (use BIP39 standard list)
:param list word_list: (optional) already loaded word list, by default None
:return: mnemonic words string.
"""
if isinstance(entropy, str):
entropy = bytes.fromhex(entropy)
if not isinstance(entropy, bytes):
raise TypeError("entropy should be bytes or hex encoded string")
if len(entropy) not in [16, 20, 24, 28, 32]:
raise ValueError(
'entropy length should be one of the following: [16, 20, 24, 28, 32]')
if word_list is None:
word_list = load_word_list(language, word_list_dir)
elif not isinstance(word_list, list) or len(word_list) != 2048:
raise TypeError("invalid wordl ist type")
# checksum
mask = 0b10000000
data_int = int.from_bytes(entropy, byteorder="big")
data_bit_len = len(entropy) * 8 // 32
fbyte_hash = sha256(entropy)[0]
while data_bit_len:
data_bit_len -= 1
data_int = (data_int << 1) | 1 if fbyte_hash & mask else data_int << 1
mask = mask >> 1
mnemonic = []
while data_int:
mnemonic.append(word_list[data_int & 0b11111111111])
data_int = data_int >> 11
return " ".join(mnemonic[::-1])
def mnemonic_to_entropy(mnemonic, language='english', word_list_dir=None,
word_list=None, hex=True):
"""
Converting mnemonic words to entropy.
:param str mnemonic: mnemonic words string (space separated)
:param str language: (optional) uses word list language (chinese_simplified, chinese_traditional, english, french,
italian, japanese, korean, spanish), by default is english.
:param str word_list_dir: (optional) path to a directory containing a list of words,
by default None (use BIP39 standard list)
:param list word_list: (optional) already loaded word list, by default None
:param boolean hex: return HEX encoded string result flag, by default True.
:return: bytes string.
"""
if word_list is None:
word_list = load_word_list(language, word_list_dir)
elif not isinstance(word_list, list) or len(word_list) != 2048:
raise TypeError("invalid word list type")
mnemonic = mnemonic.split()
word_count = len(mnemonic)
if word_count not in [12, 15, 18, 21, 24]:
raise ValueError('Number of words must be one of the following: [12, 15, 18, 21, 24]')
codes = {w: c for c, w in enumerate(word_list)}
entropy_int = 0
bit_size = word_count * 11
chk_sum_bit_len = word_count * 11 % 32
for w in mnemonic:
entropy_int = (entropy_int << 11) | codes[w]
chk_sum = entropy_int & (2 ** chk_sum_bit_len - 1)
entropy_int = entropy_int >> chk_sum_bit_len
entropy = entropy_int.to_bytes((bit_size - chk_sum_bit_len) // 8, byteorder="big")
fb = sha256(entropy)[0]
assert (fb >> (8 - chk_sum_bit_len)) == chk_sum
return entropy if not hex else entropy.hex()
def mnemonic_to_seed(mnemonic, passphrase="", hex=True):
"""
Converting mnemonic words string to seed for uses in key derivation (BIP-0032).
:param str mnemonic: mnemonic words string (space separated)
:param str passphrase: (optional) passphrase to get ability use 2FA approach for
creating seed, by default empty string.
:param boolean hex: return HEX encoded string result flag, by default True.
:return: HEX encoded or bytes string.
"""
if not isinstance(mnemonic, str):
raise TypeError("mnemonic should be string")
if not isinstance(passphrase, str):
raise TypeError("mnemonic should be string")
seed = pbkdf2_hmac('sha512', mnemonic.encode(), ("mnemonic"+passphrase).encode(), 2048)
return seed if not hex else seed.hex()

View File

@ -11,6 +11,7 @@ from pybtc.constants import *
from .hash import *
from .encode import *
from .hash import *
from .bip39_mnemonic import generate_entropy
def create_private_key(compressed=True, testnet=False, wif=True, hex=False):
@ -27,20 +28,11 @@ def create_private_key(compressed=True, testnet=False, wif=True, hex=False):
raw bytes string in case wif and hex flags set to False.
"""
a = random.SystemRandom().randint(0, MAX_INT_PRIVATE_KEY)
i = int((time.time() % 0.01 ) * 100000)
h = a.to_bytes(32, byteorder="big")
# more entropy from system timer and sha256 derivation
while i:
h = hashlib.sha256(h).digest()
i -= 1
if not i and int.from_bytes(h, byteorder="big") > MAX_INT_PRIVATE_KEY:
i += 1
if wif:
return private_key_to_wif(h, compressed=compressed, testnet=testnet)
return private_key_to_wif(generate_entropy(hex=False), compressed=compressed, testnet=testnet)
elif hex:
return h.hex()
return h
return generate_entropy()
return generate_entropy(hex=False)
def private_key_to_wif(h, compressed=True, testnet=False):

View File

@ -146,8 +146,7 @@ def read_var_int(stream):
:return: bytes.
"""
l = stream.read(1)
bytes_length = get_var_int_len(l)
return b"%s%s" % (l, stream.read(bytes_length - 1))
return b"".join((l, stream.read(get_var_int_len(l) - 1)))
def read_var_list(stream, data_type):

View File

@ -402,122 +402,3 @@ def deserialize_xkey(encode_key):
return None
# Mnemonic code for generating deterministic keys
# BIP-0039
def create_passphrase(bits=256, language='english'):
"""
Creating the passphrase.
:param int bits: size of entropy is 128-256 bits, by default is 256.
:param str language: uses wordlist language (chinese_simplified, chinese_traditional, english, french, italian, japanese, korean, spanish), by default is english.
:return: string is passphrase.
"""
if bits in [128, 160, 192, 224, 256]:
entropy = os.urandom(bits // 8)
mnemonic = create_mnemonic(entropy, language)
return ' '.join(mnemonic)
else:
raise ValueError('Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d).' % bits)
def create_mnemonic(entropy, language='english'):
"""
Generating the mnemonic.
:param bytes entropy: random entropy bytes.
:param str language: uses wordlist language (chinese_simplified, chinese_traditional, english, french, italian, japanese, korean, spanish), by default is english.
:return: list of words.
"""
mnemonic = []
wordlist = create_wordlist(language)
entropy_int = int.from_bytes(entropy, byteorder="big")
entropy_bit_len = len(entropy) * 8
chk_sum_bit_len = entropy_bit_len // 32
fbyte_hash = sha256(entropy)[0]
entropy_int = add_checksum_ent(entropy)
while entropy_int:
mnemonic.append(wordlist[entropy_int & 0b11111111111])
entropy_int = entropy_int >> 11
return mnemonic[::-1]
def create_wordlist(language='english', wordlist_dir=None):
"""
Creating the wordlist.
:param str language: uses wordlist language (chinese_simplified, chinese_traditional, english, french, italian, japanese, korean, spanish), by default is english.
:param str wordlist_dir: path to a file containing a list of words.
:return: list of words.
"""
if not wordlist_dir:
wordlist_dir = BIP0039_DIR
f = None
path = os.path.join(wordlist_dir, '.'.join((language, 'txt')))
assert os.path.exists(path)
f = open(path)
content = f.read().rstrip('\n')
assert content
f.close()
return content.split('\n')
def add_checksum_ent(data):
"""
Adding a checksum of a entropy to a entropy.
:param bytes data: random entropy bytes.
:return: bytes string.
"""
mask = 0b10000000
data_int = int.from_bytes(data, byteorder="big")
data_bit_len = len(data) * 8 // 32
fbyte_hash = sha256(data)[0]
while data_bit_len:
data_bit_len -= 1
data_int = (data_int << 1) | 1 if fbyte_hash & mask else data_int << 1
mask = mask >> 1
return data_int
def mnemonic_to_entropy(passphrase, language):
"""
Converting passphrase to entropy.
:param str passphrase: key passphrase.
:param str language: uses wordlist language.
:return: bytes string.
"""
mnemonic = passphrase.split()
if len(mnemonic) in [12, 15, 18, 21, 24]:
wordlist = create_wordlist(language)
codes = dict()
for code, word in enumerate(wordlist):
codes[word] = code
word_count = len(mnemonic)
entropy_int = None
bit_size = word_count * 11
chk_sum_bit_len = word_count * 11 % 32
for word in mnemonic:
entropy_int = (entropy_int << 11) | codes[word] if entropy_int else codes[word]
chk_sum = entropy_int & (2 ** chk_sum_bit_len - 1)
entropy_int = entropy_int >> chk_sum_bit_len
entropy = entropy_int.to_bytes((bit_size - chk_sum_bit_len) // 8, byteorder="big")
fb = sha256(entropy)[0]
assert (fb >> (8 - chk_sum_bit_len)) & chk_sum
return entropy
else:
raise ValueError('Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d).' % len(mnemonic))
def mnemonic_to_seed(passphrase, password):
"""
Converting passphrase to seed for uses in key derivation (BIP-0032).
:param str passphrase: key passphrase.
:param str password: password for key passphrase.
:return: bytes string.
"""
return pbkdf2_hmac('sha512', passphrase.encode(), (passphrase + password).encode(), 2048)

View File

@ -16,6 +16,6 @@ setup(name='pybtc',
install_requires=[ 'secp256k1'],
include_package_data=True,
package_data={
'pybtc': ['bip-0039/*.txt'],
'pybtc': ['bip39_word_list/*.txt'],
},
zip_safe=False)

View File

@ -5,5 +5,5 @@ import test
testLoad = unittest.TestLoader()
suites = testLoad.loadTestsFromModule(test)
runner = unittest.TextTestRunner(verbosity=2)
runner = unittest.TextTestRunner(verbosity=3)
runner.run(suites)

View File

@ -1,12 +1,13 @@
from .hash_functions import *
from .integer import *
from .address_functions import *
from .address_class import *
from .ecdsa import *
from .transaction_deserialize import *
from .transaction_constructor import *
from .sighash import *
from .block import *
# from .hash_functions import *
# from .integer import *
# from .address_functions import *
# from .address_class import *
# from .ecdsa import *
# from .transaction_deserialize import *
# from .transaction_constructor import *
# from .sighash import *
# from .block import *
from .mnemonic import *
# from .script_deserialize import *
# from .create_transaction import *

View File

@ -609,6 +609,10 @@ class BlockDeserializeTests(unittest.TestCase):
"f = open('./test/raw_block.txt');"
"fc = f.readline();"
"pybtc.Block(fc[:-1], format='decoded')")
cProfile.run("import pybtc;"
"f = open('./test/raw_block.txt');"
"fc = f.readline();"
"pybtc.Block(fc[:-1], format='raw')")
# print(">>>",block.bits)
# print(">>>",block.hash)
# print(">>>",block.timestamp)

27
tests/test/mnemonic.py Normal file
View File

@ -0,0 +1,27 @@
import unittest
import os, sys
parentPath = os.path.abspath("..")
if parentPath not in sys.path:
sys.path.insert(0, parentPath)
from pybtc import *
class BlockDeserializeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("\nTesting Block class deserialization:\n")
def test_mnemonic_functions(self):
mnemonic = 'young crime force door joy subject situate hen pen sweet brisk snake nephew sauce ' \
'point skate life truly hockey scout assault lab impulse boss'
entropy = "ff46716c20b789aff26b59a27b74716699457f29d650815d2db1e0a0d8f81c88"
seed = "a870edd6272a4f0962a7595612d96645f683a3378fd9b067340eb11ebef45cb3d28fb64678cadc43969846" \
"0a3d48bd57b2ae562b6d2b3c9fb5462d21e474191c"
self.assertEqual(entropy_to_mnemonic(entropy), mnemonic)
self.assertEqual(mnemonic_to_entropy(mnemonic), entropy)
self.assertEqual(mnemonic_to_seed(mnemonic), seed)
print(generate_entropy())
print(generate_entropy(128))