Browse Source

reformat using black

master
Pavol Rusnak 8 months ago
parent
commit
493e324df9
No account linked to committer's email address
5 changed files with 180 additions and 116 deletions
  1. 2
    2
      .travis.yml
  2. 17
    15
      generate_vectors.py
  3. 98
    53
      mnemonic/mnemonic.py
  4. 13
    13
      setup.py
  5. 50
    33
      test_mnemonic.py

+ 2
- 2
.travis.yml View File

@@ -18,11 +18,11 @@ install:
# Optimisation: build requirements as wheels, which get cached by Travis
- pip install "pip>=7.0" wheel
- pip install tox-travis
- pip install flake8
- pip install black

script:
- python setup.py install
- flake8
- black --check .
- tox

notifications:

+ 17
- 15
generate_vectors.py View File

@@ -6,49 +6,51 @@ import json
import sys
from binascii import hexlify, unhexlify
from random import choice, seed
from bip32utils import BIP32Key

from bip32utils import BIP32Key
from mnemonic import Mnemonic


def b2h(b):
h = hexlify(b)
return h if sys.version < '3' else h.decode('utf8')
return h if sys.version < "3" else h.decode("utf8")


def process(data, lst):
code = mnemo.to_mnemonic(unhexlify(data))
seed = Mnemonic.to_seed(code, passphrase='TREZOR')
seed = Mnemonic.to_seed(code, passphrase="TREZOR")
xprv = BIP32Key.fromEntropy(seed).ExtendedKey()
seed = b2h(seed)
print('input : %s (%d bits)' % (data, len(data) * 4))
print('mnemonic : %s (%d words)' % (code, len(code.split(' '))))
print('seed : %s (%d bits)' % (seed, len(seed) * 4))
print('xprv : %s' % xprv)
print("input : %s (%d bits)" % (data, len(data) * 4))
print("mnemonic : %s (%d words)" % (code, len(code.split(" "))))
print("seed : %s (%d bits)" % (seed, len(seed) * 4))
print("xprv : %s" % xprv)
print()
lst.append((data, code, seed, xprv))


if __name__ == '__main__':
if __name__ == "__main__":
out = {}
seed(1337)

for lang in ['english']: # Mnemonic.list_languages():
for lang in ["english"]: # Mnemonic.list_languages():
mnemo = Mnemonic(lang)
out[lang] = []

# Generate corner cases
data = []
for l in range(16, 32 + 1, 8):
for b in ['00', '7f', '80', 'ff']:
for b in ["00", "7f", "80", "ff"]:
process(b * l, out[lang])

# Generate random seeds
for i in range(12):
data = ''.join(chr(choice(range(0, 256))) for _ in range(8 * (i % 3 + 2)))
if sys.version >= '3':
data = data.encode('latin1')
data = "".join(chr(choice(range(0, 256))) for _ in range(8 * (i % 3 + 2)))
if sys.version >= "3":
data = data.encode("latin1")
process(b2h(data), out[lang])

with open('vectors.json', 'w') as f:
json.dump(out, f, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)
with open("vectors.json", "w") as f:
json.dump(
out, f, sort_keys=True, indent=4, separators=(",", ": "), ensure_ascii=False
)

+ 98
- 53
mnemonic/mnemonic.py View File

@@ -28,6 +28,7 @@ import itertools
import os
import sys
import unicodedata

from pbkdf2 import PBKDF2

PBKDF2_ROUNDS = 2048
@@ -38,65 +39,74 @@ class ConfigurationError(Exception):


# From <https://stackoverflow.com/questions/212358/binary-search-bisection-in-python/2233940#2233940>
def binary_search(a, x, lo=0, hi=None): # can't use a to specify default for hi
hi = hi if hi is not None else len(a) # hi defaults to len(a)
pos = bisect.bisect_left(a, x, lo, hi) # find insertion position
return (pos if pos != hi and a[pos] == x else -1) # don't walk off the end
def binary_search(a, x, lo=0, hi=None): # can't use a to specify default for hi
hi = hi if hi is not None else len(a) # hi defaults to len(a)
pos = bisect.bisect_left(a, x, lo, hi) # find insertion position
return pos if pos != hi and a[pos] == x else -1 # don't walk off the end


# Refactored code segments from <https://github.com/keis/base58>
def b58encode(v):
alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

p, acc = 1, 0
for c in reversed(v):
if sys.version < '3':
if sys.version < "3":
c = ord(c)
acc += p * c
p = p << 8

string = ''
string = ""
while acc:
acc, idx = divmod(acc, 58)
string = alphabet[idx:idx + 1] + string
string = alphabet[idx : idx + 1] + string
return string


class Mnemonic(object):
def __init__(self, language):
self.radix = 2048
if sys.version < '3':
with open('%s/%s.txt' % (self._get_directory(), language), 'r') as f:
self.wordlist = [w.strip().decode('utf8') for w in f.readlines()]
if sys.version < "3":
with open("%s/%s.txt" % (self._get_directory(), language), "r") as f:
self.wordlist = [w.strip().decode("utf8") for w in f.readlines()]
else:
with open('%s/%s.txt' % (self._get_directory(), language), 'r', encoding='utf-8') as f:
with open(
"%s/%s.txt" % (self._get_directory(), language), "r", encoding="utf-8"
) as f:
self.wordlist = [w.strip() for w in f.readlines()]
if len(self.wordlist) != self.radix:
raise ConfigurationError('Wordlist should contain %d words, but it contains %d words.' % (self.radix, len(self.wordlist)))
raise ConfigurationError(
"Wordlist should contain %d words, but it contains %d words."
% (self.radix, len(self.wordlist))
)

@classmethod
def _get_directory(cls):
return os.path.join(os.path.dirname(__file__), 'wordlist')
return os.path.join(os.path.dirname(__file__), "wordlist")

@classmethod
def list_languages(cls):
return [f.split('.')[0] for f in os.listdir(cls._get_directory()) if f.endswith('.txt')]
return [
f.split(".")[0]
for f in os.listdir(cls._get_directory())
if f.endswith(".txt")
]

@classmethod
def normalize_string(cls, txt):
if isinstance(txt, str if sys.version < '3' else bytes):
utxt = txt.decode('utf8')
elif isinstance(txt, unicode if sys.version < '3' else str): # noqa: F821
if isinstance(txt, str if sys.version < "3" else bytes):
utxt = txt.decode("utf8")
elif isinstance(txt, unicode if sys.version < "3" else str): # noqa: F821
utxt = txt
else:
raise TypeError("String value expected")

return unicodedata.normalize('NFKD', utxt)
return unicodedata.normalize("NFKD", utxt)

@classmethod
def detect_language(cls, code):
code = cls.normalize_string(code)
first = code.split(' ')[0]
first = code.split(" ")[0]
languages = cls.list_languages()

for lang in languages:
@@ -108,27 +118,37 @@ class Mnemonic(object):

def generate(self, strength=128):
if strength not in [128, 160, 192, 224, 256]:
raise ValueError('Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d).' % strength)
raise ValueError(
"Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d)."
% strength
)
return self.to_mnemonic(os.urandom(strength // 8))

# Adapted from <http://tinyurl.com/oxmn476>
def to_entropy(self, words):
if not isinstance(words, list):
words = words.split(' ')
words = words.split(" ")
if len(words) not in [12, 15, 18, 21, 24]:
raise ValueError('Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d).' % len(words))
raise ValueError(
"Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)."
% len(words)
)
# Look up all the words in the list and construct the
# concatenation of the original entropy and the checksum.
concatLenBits = len(words) * 11
concatBits = [False] * concatLenBits
wordindex = 0
if self.detect_language(' '.join(words)) == 'english':
if self.detect_language(" ".join(words)) == "english":
use_binary_search = True
else:
use_binary_search = False
for word in words:
# Find the words index in the wordlist
ndx = binary_search(self.wordlist, word) if use_binary_search else self.wordlist.index(word)
ndx = (
binary_search(self.wordlist, word)
if use_binary_search
else self.wordlist.index(word)
)
if ndx < 0:
raise LookupError('Unable to find "%s" in word list.' % word)
# Set the next 11 bits to the value of the index.
@@ -145,47 +165,65 @@ class Mnemonic(object):
entropy[ii] |= 1 << (7 - jj)
# Take the digest of the entropy.
hashBytes = hashlib.sha256(entropy).digest()
if sys.version < '3':
hashBits = list(itertools.chain.from_iterable(([ord(c) & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)))
if sys.version < "3":
hashBits = list(
itertools.chain.from_iterable(
(
[ord(c) & (1 << (7 - i)) != 0 for i in range(8)]
for c in hashBytes
)
)
)
else:
hashBits = list(itertools.chain.from_iterable(([c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)))
hashBits = list(
itertools.chain.from_iterable(
([c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)
)
)
# Check all the checksum bits.
for i in range(checksumLengthBits):
if concatBits[entropyLengthBits + i] != hashBits[i]:
raise ValueError('Failed checksum.')
raise ValueError("Failed checksum.")
return entropy

def to_mnemonic(self, data):
if len(data) not in [16, 20, 24, 28, 32]:
raise ValueError('Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d).' % len(data))
raise ValueError(
"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d)."
% len(data)
)
h = hashlib.sha256(data).hexdigest()
b = bin(int(binascii.hexlify(data), 16))[2:].zfill(len(data) * 8) + \
bin(int(h, 16))[2:].zfill(256)[:len(data) * 8 // 32]
b = (
bin(int(binascii.hexlify(data), 16))[2:].zfill(len(data) * 8)
+ bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
)
result = []
for i in range(len(b) // 11):
idx = int(b[i * 11:(i + 1) * 11], 2)
idx = int(b[i * 11 : (i + 1) * 11], 2)
result.append(self.wordlist[idx])
if self.detect_language(' '.join(result)) == 'japanese': # Japanese must be joined by ideographic space.
result_phrase = u'\u3000'.join(result)
if (
self.detect_language(" ".join(result)) == "japanese"
): # Japanese must be joined by ideographic space.
result_phrase = u"\u3000".join(result)
else:
result_phrase = ' '.join(result)
result_phrase = " ".join(result)
return result_phrase

def check(self, mnemonic):
mnemonic = self.normalize_string(mnemonic).split(' ')
mnemonic = self.normalize_string(mnemonic).split(" ")
# list of valid mnemonic lengths
if len(mnemonic) not in [12, 15, 18, 21, 24]:
return False
try:
idx = map(lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic)
b = ''.join(idx)
b = "".join(idx)
except ValueError:
return False
l = len(b) # noqa: E741
d = b[:l // 33 * 32]
h = b[-l // 33:]
nd = binascii.unhexlify(hex(int(d, 2))[2:].rstrip('L').zfill(l // 33 * 8))
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[:l // 33]
d = b[: l // 33 * 32]
h = b[-l // 33 :]
nd = binascii.unhexlify(hex(int(d, 2))[2:].rstrip("L").zfill(l // 33 * 8))
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
return h == nh

def expand_word(self, prefix):
@@ -201,27 +239,33 @@ class Mnemonic(object):
return prefix

def expand(self, mnemonic):
return ' '.join(map(self.expand_word, mnemonic.split(' ')))
return " ".join(map(self.expand_word, mnemonic.split(" ")))

@classmethod
def to_seed(cls, mnemonic, passphrase=''):
def to_seed(cls, mnemonic, passphrase=""):
mnemonic = cls.normalize_string(mnemonic)
passphrase = cls.normalize_string(passphrase)
return PBKDF2(mnemonic, u'mnemonic' + passphrase, iterations=PBKDF2_ROUNDS, macmodule=hmac, digestmodule=hashlib.sha512).read(64)
return PBKDF2(
mnemonic,
u"mnemonic" + passphrase,
iterations=PBKDF2_ROUNDS,
macmodule=hmac,
digestmodule=hashlib.sha512,
).read(64)

@classmethod
def to_hd_master_key(cls, seed):
if len(seed) != 64:
raise ValueError('Provided seed should have length of 64')
raise ValueError("Provided seed should have length of 64")

# Computer HMAC-SHA512 of seed
seed = hmac.new(b'Bitcoin seed', seed, digestmod=hashlib.sha512).digest()
seed = hmac.new(b"Bitcoin seed", seed, digestmod=hashlib.sha512).digest()

# Serialization format can be found at: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Serialization_format
xprv = b'\x04\x88\xad\xe4' # Version for private mainnet
xprv += b'\x00' * 9 # Depth, parent fingerprint, and child number
xprv += seed[32:] # Chain code
xprv += b'\x00' + seed[:32] # Master key
xprv = b"\x04\x88\xad\xe4" # Version for private mainnet
xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number
xprv += seed[32:] # Chain code
xprv += b"\x00" + seed[:32] # Master key

# Double hash using SHA256
hashed_xprv = hashlib.sha256(xprv).digest()
@@ -237,14 +281,15 @@ class Mnemonic(object):
def main():
import binascii
import sys

if len(sys.argv) > 1:
data = sys.argv[1]
else:
data = sys.stdin.readline().strip()
data = binascii.unhexlify(data)
m = Mnemonic('english')
m = Mnemonic("english")
print(m.to_mnemonic(data))


if __name__ == '__main__':
if __name__ == "__main__":
main()

+ 13
- 13
setup.py View File

@@ -2,20 +2,20 @@
from setuptools import setup

setup(
name='mnemonic',
version='0.18',
author='Bitcoin TREZOR',
author_email='info@trezor.io',
description='Implementation of Bitcoin BIP-0039',
url='https://github.com/trezor/python-mnemonic',
packages=['mnemonic', ],
package_data={'mnemonic': ['wordlist/*.txt']},
name="mnemonic",
version="0.18",
author="Bitcoin TREZOR",
author_email="info@trezor.io",
description="Implementation of Bitcoin BIP-0039",
url="https://github.com/trezor/python-mnemonic",
packages=["mnemonic"],
package_data={"mnemonic": ["wordlist/*.txt"]},
zip_safe=False,
install_requires=['pbkdf2'],
install_requires=["pbkdf2"],
classifiers=[
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS :: MacOS X',
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
],
)

+ 50
- 33
test_mnemonic.py View File

@@ -33,48 +33,57 @@ from mnemonic import Mnemonic


class MnemonicTest(unittest.TestCase):

def _check_list(self, language, vectors):
mnemo = Mnemonic(language)
for v in vectors:
code = mnemo.to_mnemonic(unhexlify(v[0]))
seed = hexlify(Mnemonic.to_seed(code, passphrase='TREZOR'))
seed = hexlify(Mnemonic.to_seed(code, passphrase="TREZOR"))
xprv = Mnemonic.to_hd_master_key(unhexlify(seed))
if sys.version >= '3':
seed = seed.decode('utf8')
if sys.version >= "3":
seed = seed.decode("utf8")
self.assertIs(mnemo.check(v[1]), True)
self.assertEqual(v[1], code)
self.assertEqual(v[2], seed)
self.assertEqual(v[3], xprv)

def test_vectors(self):
with open('vectors.json', 'r') as f:
with open("vectors.json", "r") as f:
vectors = json.load(f)
for lang in vectors.keys():
self._check_list(lang, vectors[lang])

def test_failed_checksum(self):
code = 'bless cloud wheel regular tiny venue bird web grief security dignity zoo'
mnemo = Mnemonic('english')
code = (
"bless cloud wheel regular tiny venue bird web grief security dignity zoo"
)
mnemo = Mnemonic("english")
self.assertFalse(mnemo.check(code))

def test_detection(self):
self.assertEqual('english', Mnemonic.detect_language('security'))
self.assertEqual("english", Mnemonic.detect_language("security"))

with self.assertRaises(Exception):
Mnemonic.detect_language('xxxxxxx')
Mnemonic.detect_language("xxxxxxx")

def test_utf8_nfkd(self):
# The same sentence in various UTF-8 forms
words_nfkd = u'Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a'
words_nfc = u'P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f'
words_nfkc = u'P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f'
words_nfd = u'Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a'

passphrase_nfkd = u'Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko'
passphrase_nfc = u'Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko'
passphrase_nfkc = u'Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko'
passphrase_nfd = u'Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko'
words_nfkd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"
words_nfc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"
words_nfkc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"
words_nfd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"

passphrase_nfkd = (
u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
)
passphrase_nfc = (
u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko"
)
passphrase_nfkc = (
u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko"
)
passphrase_nfd = (
u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
)

seed_nfkd = Mnemonic.to_seed(words_nfkd, passphrase_nfkd)
seed_nfc = Mnemonic.to_seed(words_nfc, passphrase_nfc)
@@ -86,27 +95,35 @@ class MnemonicTest(unittest.TestCase):
self.assertEqual(seed_nfkd, seed_nfd)

def test_to_entropy(self):
data = [bytearray((random.getrandbits(8) for _ in range(32))) for _ in range(1024)]
data.append(b'Lorem ipsum dolor sit amet amet.')
m = Mnemonic('english')
data = [
bytearray((random.getrandbits(8) for _ in range(32))) for _ in range(1024)
]
data.append(b"Lorem ipsum dolor sit amet amet.")
m = Mnemonic("english")
for d in data:
self.assertEqual(m.to_entropy(m.to_mnemonic(d).split()), d)

def test_expand_word(self):
m = Mnemonic('english')
self.assertEqual('', m.expand_word(''))
self.assertEqual(' ', m.expand_word(' '))
self.assertEqual('access', m.expand_word('access')) # word in list
self.assertEqual('access', m.expand_word('acce')) # unique prefix expanded to word in list
self.assertEqual('acb', m.expand_word('acb')) # not found at all
self.assertEqual('acc', m.expand_word('acc')) # multi-prefix match
self.assertEqual('act', m.expand_word('act')) # exact three letter match
self.assertEqual('action', m.expand_word('acti')) # unique prefix expanded to word in list
m = Mnemonic("english")
self.assertEqual("", m.expand_word(""))
self.assertEqual(" ", m.expand_word(" "))
self.assertEqual("access", m.expand_word("access")) # word in list
self.assertEqual(
"access", m.expand_word("acce")
) # unique prefix expanded to word in list
self.assertEqual("acb", m.expand_word("acb")) # not found at all
self.assertEqual("acc", m.expand_word("acc")) # multi-prefix match
self.assertEqual("act", m.expand_word("act")) # exact three letter match
self.assertEqual(
"action", m.expand_word("acti")
) # unique prefix expanded to word in list

def test_expand(self):
m = Mnemonic('english')
self.assertEqual('access', m.expand('access'))
self.assertEqual('access access acb acc act action', m.expand('access acce acb acc act acti'))
m = Mnemonic("english")
self.assertEqual("access", m.expand("access"))
self.assertEqual(
"access access acb acc act action", m.expand("access acce acb acc act acti")
)


def __main__():

Loading…
Cancel
Save