diff --git a/pybtc/bip-0039/chinese_simplified.txt b/pybtc/bip39_word_list/chinese_simplified.txt similarity index 100% rename from pybtc/bip-0039/chinese_simplified.txt rename to pybtc/bip39_word_list/chinese_simplified.txt diff --git a/pybtc/bip-0039/chinese_traditional.txt b/pybtc/bip39_word_list/chinese_traditional.txt similarity index 100% rename from pybtc/bip-0039/chinese_traditional.txt rename to pybtc/bip39_word_list/chinese_traditional.txt diff --git a/pybtc/bip-0039/english.txt b/pybtc/bip39_word_list/english.txt similarity index 100% rename from pybtc/bip-0039/english.txt rename to pybtc/bip39_word_list/english.txt diff --git a/pybtc/bip-0039/french.txt b/pybtc/bip39_word_list/french.txt similarity index 100% rename from pybtc/bip-0039/french.txt rename to pybtc/bip39_word_list/french.txt diff --git a/pybtc/bip-0039/italian.txt b/pybtc/bip39_word_list/italian.txt similarity index 100% rename from pybtc/bip-0039/italian.txt rename to pybtc/bip39_word_list/italian.txt diff --git a/pybtc/bip-0039/japanese.txt b/pybtc/bip39_word_list/japanese.txt similarity index 100% rename from pybtc/bip-0039/japanese.txt rename to pybtc/bip39_word_list/japanese.txt diff --git a/pybtc/bip-0039/korean.txt b/pybtc/bip39_word_list/korean.txt similarity index 100% rename from pybtc/bip-0039/korean.txt rename to pybtc/bip39_word_list/korean.txt diff --git a/pybtc/bip-0039/spanish.txt b/pybtc/bip39_word_list/spanish.txt similarity index 100% rename from pybtc/bip-0039/spanish.txt rename to pybtc/bip39_word_list/spanish.txt diff --git a/pybtc/constants.py b/pybtc/constants.py index 6ea3189..286ee33 100644 --- a/pybtc/constants.py +++ b/pybtc/constants.py @@ -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 diff --git a/pybtc/functions/__init__.py b/pybtc/functions/__init__.py index fa3ae57..e3136f3 100644 --- a/pybtc/functions/__init__.py +++ b/pybtc/functions/__init__.py @@ -4,4 +4,5 @@ from .script import * from .tools import * from .hash import * from .block import * -from .encode import * \ No newline at end of file +from .encode import * +from .bip39_mnemonic import * \ No newline at end of file diff --git a/pybtc/functions/mnemonic.py b/pybtc/functions/bip32.py similarity index 100% rename from pybtc/functions/mnemonic.py rename to pybtc/functions/bip32.py diff --git a/pybtc/functions/bip39_mnemonic.py b/pybtc/functions/bip39_mnemonic.py new file mode 100644 index 0000000..2e7cccf --- /dev/null +++ b/pybtc/functions/bip39_mnemonic.py @@ -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() diff --git a/pybtc/functions/key.py b/pybtc/functions/key.py index f4086aa..345a3ce 100644 --- a/pybtc/functions/key.py +++ b/pybtc/functions/key.py @@ -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): diff --git a/pybtc/functions/tools.py b/pybtc/functions/tools.py index b68aa88..189fedc 100644 --- a/pybtc/functions/tools.py +++ b/pybtc/functions/tools.py @@ -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): diff --git a/pybtc/hdwallet.py b/pybtc/hdwallet.py index a181fe4..cd8ebfc 100644 --- a/pybtc/hdwallet.py +++ b/pybtc/hdwallet.py @@ -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) - diff --git a/setup.py b/setup.py index b16511e..f33609d 100644 --- a/setup.py +++ b/setup.py @@ -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) diff --git a/tests/test.py b/tests/test.py index afb121b..7a7e9c2 100644 --- a/tests/test.py +++ b/tests/test.py @@ -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) diff --git a/tests/test/__init__.py b/tests/test/__init__.py index 7f73d41..b943a89 100644 --- a/tests/test/__init__.py +++ b/tests/test/__init__.py @@ -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 * diff --git a/tests/test/block.py b/tests/test/block.py index 6acd2d7..0179916 100644 --- a/tests/test/block.py +++ b/tests/test/block.py @@ -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) diff --git a/tests/test/mnemonic.py b/tests/test/mnemonic.py new file mode 100644 index 0000000..76f83b6 --- /dev/null +++ b/tests/test/mnemonic.py @@ -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))