Add support for multiple mnemonic wordlist

Added Wordlist class and registry
Moved mnemonic encoding/decoding logic from Seed to Wordlist
This commit is contained in:
Adam Ward 2018-06-03 12:28:33 +10:00
parent 7c26bb62d9
commit 4c9ca07006
6 changed files with 1790 additions and 1688 deletions

View File

@ -39,7 +39,7 @@ from monero import address
from monero import wordlists from monero import wordlists
from monero import ed25519 from monero import ed25519
from monero import base58 from monero import base58
from binascii import crc32, hexlify, unhexlify from binascii import hexlify, unhexlify
from os import urandom from os import urandom
from sha3 import keccak_256 from sha3 import keccak_256
@ -49,19 +49,18 @@ class Seed(object):
:rtype: :class:`Seed <monero.seed.Seed>` :rtype: :class:`Seed <monero.seed.Seed>`
""" """
n = 1626 def __init__(self, phrase_or_hex="", wordlist="English"):
wordlist = wordlists.english.wordlist # default english for now
phrase = "" #13 or 25 word mnemonic word string
hex = "" # hexadecimal
def __init__(self, phrase_or_hex=""):
"""If user supplied a seed string to the class, break it down and determine """If user supplied a seed string to the class, break it down and determine
if it's hexadecimal or mnemonic word string. Gather the values and store them. if it's hexadecimal or mnemonic word string. Gather the values and store them.
If no seed is passed, automatically generate a new one from local system randomness. If no seed is passed, automatically generate a new one from local system randomness.
:rtype: :class:`Seed <monero.seed.Seed>` :rtype: :class:`Seed <monero.seed.Seed>`
""" """
self.phrase = "" #13 or 25 word mnemonic word string
self.hex = "" # hexadecimal
self.word_list = wordlists.get_wordlist(wordlist)
if phrase_or_hex: if phrase_or_hex:
seed_split = phrase_or_hex.split(" ") seed_split = phrase_or_hex.split(" ")
if len(seed_split) >= 24: if len(seed_split) >= 24:
@ -94,41 +93,15 @@ class Seed(object):
"""Returns True if the seed is MyMonero-style (12/13-word).""" """Returns True if the seed is MyMonero-style (12/13-word)."""
return len(self.hex) == 32 return len(self.hex) == 32
def endian_swap(self, word):
"""Given any string, swap bits and return the result.
:rtype: str
"""
return "".join([word[i:i+2] for i in [6, 4, 2, 0]])
def _encode_seed(self): def _encode_seed(self):
"""Convert hexadecimal string to mnemonic word representation with checksum. """Convert hexadecimal string to mnemonic word representation with checksum.
""" """
out = [] self.phrase = self.word_list.encode(self.hex)
for i in range(len(self.hex) // 8):
word = self.endian_swap(self.hex[8*i:8*i+8])
x = int(word, 16)
w1 = x % self.n
w2 = (x // self.n + w1) % self.n
w3 = (x // self.n // self.n + w2) % self.n
out += [self.wordlist[w1], self.wordlist[w2], self.wordlist[w3]]
checksum = get_checksum(" ".join(out))
out.append(checksum)
self.phrase = " ".join(out)
def _decode_seed(self): def _decode_seed(self):
"""Calculate hexadecimal representation of the phrase. """Calculate hexadecimal representation of the phrase.
""" """
phrase = self.phrase.split(" ") self.hex = self.word_list.decode(self.phrase)
out = ""
for i in range(len(phrase) // 3):
word1, word2, word3 = phrase[3*i:3*i+3]
w1 = self.wordlist.index(word1)
w2 = self.wordlist.index(word2) % self.n
w3 = self.wordlist.index(word3) % self.n
x = w1 + self.n *((w2 - w1) % self.n) + self.n * self.n * ((w3 - w2) % self.n)
out += self.endian_swap("%08x" % x)
self.hex = out
def _validate_checksum(self): def _validate_checksum(self):
"""Given a mnemonic word string, confirm seed checksum (last word) matches the computed checksum. """Given a mnemonic word string, confirm seed checksum (last word) matches the computed checksum.
@ -136,7 +109,7 @@ class Seed(object):
:rtype: bool :rtype: bool
""" """
phrase = self.phrase.split(" ") phrase = self.phrase.split(" ")
if get_checksum(self.phrase) == phrase[-1]: if self.word_list.get_checksum(self.phrase) == phrase[-1]:
return True return True
raise ValueError("Invalid checksum") raise ValueError("Invalid checksum")
@ -191,25 +164,6 @@ class Seed(object):
return base58.encode(data + checksum[0:8]) return base58.encode(data + checksum[0:8])
def get_checksum(phrase):
"""Given a mnemonic word string, return a string of the computed checksum.
:rtype: str
"""
phrase_split = phrase.split(" ")
if len(phrase_split) < 12:
raise ValueError("Invalid mnemonic phrase")
if len(phrase_split) > 13:
# Standard format
phrase = phrase_split[:24]
else:
# MyMonero format
phrase = phrase_split[:12]
wstr = "".join(word[:3] for word in phrase)
z = ((crc32(wstr.encode()) & 0xffffffff) ^ 0xffffffff ) >> 0
z2 = ((z ^ 0xffffffff) >> 0) % len(phrase)
return phrase_split[z2]
def generate_hex(n_bytes=32): def generate_hex(n_bytes=32):
"""Generate a secure and random hexadecimal string. 32 bytes by default, but arguments can override. """Generate a secure and random hexadecimal string. 32 bytes by default, but arguments can override.

View File

@ -1 +1,2 @@
from . import english from .wordlist import get_wordlist, list_wordlists
from .english import English

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from binascii import crc32
from six import with_metaclass
WORDLISTS = {}
_log = logging.getLogger(__name__)
class WordlistType(type):
def __new__(cls, name, bases, attrs):
if bases:
if 'language_name' not in attrs:
raise TypeError("Missing language_name for {0}".format(name))
if 'unique_prefix_length' not in attrs:
raise TypeError("Missing 'unique_prefix_length' for {0}".format(name))
if 'word_list' not in attrs:
raise TypeError("Missing 'word_list' for {0}".format(name))
if 'english_language_name' not in attrs:
_log.warn("No 'english_language_name' for {0} using '{1}'".format(name, language_name))
attrs['english_language_name'] = attrs['language_name']
if len(attrs['word_list']) != 1626:
raise TypeError("Wrong word list length for {0}".format(name))
new_cls = super(WordlistType, cls).__new__(cls, name, bases, attrs)
if bases:
WORDLISTS[new_cls.english_language_name] = new_cls
return new_cls
class Wordlist(with_metaclass(WordlistType)):
n = 1626
@classmethod
def encode(cls, hex):
"""Convert hexadecimal string to mnemonic word representation with checksum.
"""
out = []
for i in range(len(hex) // 8):
word = endian_swap(hex[8*i:8*i+8])
x = int(word, 16)
w1 = x % cls.n
w2 = (x // cls.n + w1) % cls.n
w3 = (x // cls.n // cls.n + w2) % cls.n
out += [cls.word_list[w1], cls.word_list[w2], cls.word_list[w3]]
checksum = cls.get_checksum(" ".join(out))
out.append(checksum)
return " ".join(out)
@classmethod
def decode(cls, phrase):
"""Calculate hexadecimal representation of the phrase.
"""
phrase = phrase.split(" ")
out = ""
for i in range(len(phrase) // 3):
word1, word2, word3 = phrase[3*i:3*i+3]
w1 = cls.word_list.index(word1)
w2 = cls.word_list.index(word2) % cls.n
w3 = cls.word_list.index(word3) % cls.n
x = w1 + cls.n *((w2 - w1) % cls.n) + cls.n * cls.n * ((w3 - w2) % cls.n)
out += endian_swap("%08x" % x)
return out
@classmethod
def get_checksum(cls, phrase):
"""Given a mnemonic word string, return a string of the computed checksum.
:rtype: str
"""
phrase_split = phrase.split(" ")
if len(phrase_split) < 12:
raise ValueError("Invalid mnemonic phrase")
if len(phrase_split) > 13:
# Standard format
phrase = phrase_split[:24]
else:
# MyMonero format
phrase = phrase_split[:12]
wstr = "".join(word[:cls.unique_prefix_length] for word in phrase)
wstr = bytearray(wstr.encode('utf-8'))
z = ((crc32(wstr) & 0xffffffff) ^ 0xffffffff ) >> 0
z2 = ((z ^ 0xffffffff) >> 0) % len(phrase)
return phrase_split[z2]
def get_wordlist(name):
try:
return WORDLISTS[name]
except KeyError:
raise ValueError("No such word list")
def list_wordlists():
return WORDLISTS.keys()
def endian_swap(word):
"""Given any string, swap bits and return the result.
:rtype: str
"""
return "".join([word[i:i+2] for i in [6, 4, 2, 0]])

View File

@ -1,2 +1,3 @@
pysha3 pysha3
requests requests
six

View File

@ -1,7 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest import unittest
from monero.seed import Seed, get_checksum from monero.seed import Seed
class SeedTestCase(unittest.TestCase): class SeedTestCase(unittest.TestCase):