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 ed25519
from monero import base58
from binascii import crc32, hexlify, unhexlify
from binascii import hexlify, unhexlify
from os import urandom
from sha3 import keccak_256
@ -49,19 +49,18 @@ class Seed(object):
:rtype: :class:`Seed <monero.seed.Seed>`
"""
n = 1626
wordlist = wordlists.english.wordlist # default english for now
phrase = "" #13 or 25 word mnemonic word string
hex = "" # hexadecimal
def __init__(self, phrase_or_hex=""):
def __init__(self, phrase_or_hex="", wordlist="English"):
"""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 no seed is passed, automatically generate a new one from local system randomness.
: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:
seed_split = phrase_or_hex.split(" ")
if len(seed_split) >= 24:
@ -94,41 +93,15 @@ class Seed(object):
"""Returns True if the seed is MyMonero-style (12/13-word)."""
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):
"""Convert hexadecimal string to mnemonic word representation with checksum.
"""
out = []
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)
self.phrase = self.word_list.encode(self.hex)
def _decode_seed(self):
"""Calculate hexadecimal representation of the phrase.
"""
phrase = self.phrase.split(" ")
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
self.hex = self.word_list.decode(self.phrase)
def _validate_checksum(self):
"""Given a mnemonic word string, confirm seed checksum (last word) matches the computed checksum.
@ -136,7 +109,7 @@ class Seed(object):
:rtype: bool
"""
phrase = self.phrase.split(" ")
if get_checksum(self.phrase) == phrase[-1]:
if self.word_list.get_checksum(self.phrase) == phrase[-1]:
return True
raise ValueError("Invalid checksum")
@ -191,25 +164,6 @@ class Seed(object):
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):
"""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

View File

@ -1,4 +1,30 @@
# Copyright (c) 2014-2018, The Monero Project
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Word list originally created by dabura667 and released under The MIT License (MIT)
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Code surrounding the word list is Copyright (c) 2014-2018, The Monero Project
#
# All rights reserved.
#
@ -28,9 +54,17 @@
#
# list of words from:
# https://github.com/monero-project/monero/blob/master/src/mnemonics/english.h
# Most recent commit: 18216f19dd479466cb7fd38f52d23bddfcfd4880
# Most recent commit: 8ea3c4d54429976e9001b8cc4868b721e6747e3c
wordlist = [
from .wordlist import Wordlist
class English(Wordlist):
language_name = "English"
english_language_name = "English"
unique_prefix_length = 3
word_list = [
"abbey",
"abducts",
"ability",

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
requests
six

View File

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