Merge pull request #23 from atward/mnemonics

Add support for multiple mnemonic wordlist
This commit is contained in:
Michał Sałaban 2018-06-04 12:40:20 +02:00 committed by GitHub
commit a76cfa03fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 20633 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,14 @@
from . import english
from .wordlist import get_wordlist, list_wordlists
from .english import English
from .chinese_simplified import ChineseSimplified
from .dutch import Dutch
from .esperanto import Esperanto
from .french import French
from .german import German
from .italian import Italian
from .japanese import Japanese
from .lojban import Lojban
from .portuguese import Portuguese
from .russian import Russian
from .spanish import Spanish

File diff suppressed because it is too large Load Diff

1693
monero/wordlists/dutch.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1694
monero/wordlists/french.py Normal file

File diff suppressed because it is too large Load Diff

1693
monero/wordlists/german.py Normal file

File diff suppressed because it is too large Load Diff

1694
monero/wordlists/italian.py Normal file

File diff suppressed because it is too large Load Diff

1694
monero/wordlists/japanese.py Normal file

File diff suppressed because it is too large Load Diff

1693
monero/wordlists/lojban.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1694
monero/wordlists/russian.py Normal file

File diff suppressed because it is too large Load Diff

1694
monero/wordlists/spanish.py Normal file

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

View File

@ -1,7 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from monero.seed import Seed, get_checksum
from monero.seed import Seed
from monero.wordlists import list_wordlists
class SeedTestCase(unittest.TestCase):
@ -92,6 +94,205 @@ class SeedTestCase(unittest.TestCase):
seed.public_address(),
'47dwi1w9it69yZyTBBRD52ctQqw3B2FZx79bCEgVUKGHH2m7MjmaXrjeQfchMMkarG6AF9a36JvBWCyRaqEcUixpKLQRxdj')
def test_languages(self):
for wordlist in list_wordlists():
print("Language: {}".format(wordlist))
# Generate random seed
seed = Seed(wordlist=wordlist)
# Convert it from phrase
seed_from_phrase = Seed(seed.phrase, wordlist=wordlist)
self.assertEqual(seed.hex, seed_from_phrase.hex)
self.assertEqual(seed.phrase, seed_from_phrase.phrase)
# Convert it from hex
seed_from_hex = Seed(seed.hex, wordlist=wordlist)
self.assertEqual(seed.hex, seed_from_hex.hex)
self.assertEqual(seed.phrase, seed_from_hex.phrase)
def test_chinese_simplified(self):
seed = Seed(u"遭 牲 本 点 司 司 仲 吉 虎 只 绝 生 指 纯 伟 破 夫 惊 群 楚 祥 旋 暗 骨 伟", "Chinese (simplified)")
self.assertEqual(
seed.secret_spend_key(),
'2ec46011b23b0c00468946f1d9a64995bf0a89f9ee0bbf4f64058a3acd81a70e')
self.assertEqual(
seed.secret_view_key(),
'aa141796baa24539583306300b44a72495bb7823a0cc6ad856de6d372288d10f')
self.assertEqual(
seed.public_spend_key(),
'76cc3b927e70fee85a43a6141d019b53c77f46bbcd6c4dc6d814dfc271af361c')
self.assertEqual(
seed.public_view_key(),
'91ef3783492e173ca366a818ae7ee37f062daea909fd9ed9ca40d41e7d572dd4')
self.assertEqual(
seed.public_address(),
'468Dewci4TPfs7TATZ2nf4F1mKAEMp6RraG37wiSU4uT5nAbBwGz5LaB9GWHG23o6ANFJ1Q9cBYk5dRqWNNkmFN4Qx3RqBD')
def test_dutch(self):
seed = Seed(u"ralf tolvrij copier roon ossuarium wedstrijd splijt debbie bomtapijt occlusie oester noren hiaat scenario geshockt veeteler rotten symboliek jarig bock yoghurt plegen weert zeeblauw wedstrijd", "Dutch")
self.assertEqual(
seed.secret_spend_key(),
'600d3c5022e1844dd2df02f178a074fc2e566793e99d9e1465926adcbfa9b508')
self.assertEqual(
seed.secret_view_key(),
'bb8984647124dafcb8682f1c257b5232bb12b96d682bfc320b4f8ce935e2d303')
self.assertEqual(
seed.public_spend_key(),
'df4be25f7ccaf632f1525b06fd9b0d7e9f64b21ebfb609353d643a24de16221b')
self.assertEqual(
seed.public_view_key(),
'2fcd275e4337152ea77ac68ec02f166a243f4917ebd53b2a381ab27b84d24065')
self.assertEqual(
seed.public_address(),
'4A5uCL4cXoB9XD3WjTrEwvNBQ6JRPTHaY9uVaxfWmcLy5YkE81tW7B28oc42XGzAeRJkhyHjKAxSE84aZnihjVBVCQf15mw')
def test_esperanto(self):
seed = Seed(u"knedi aspekti boli asbesto pterido aparta muro sandalo hufumo porcelana degeli utopia ebono lifto dutaga hundo vejno ebono higieno nikotino orkestro arlekeno insekto jaguaro hundo", "Esperanto")
self.assertEqual(
seed.secret_spend_key(),
'a8e8a30d3638cc4d09d1fa9f4de12ac0096c69a77896774793627c0cc6a28703')
self.assertEqual(
seed.secret_view_key(),
'8b4dcbcbafaf3d195af5bd54aa386d767a8de3b45236c9842cb876212427f103')
self.assertEqual(
seed.public_spend_key(),
'32c8a782c05db039018caa150bef1f66621831b3cb591401381e1dfc3c3d423e')
self.assertEqual(
seed.public_view_key(),
'047963206a0267649657936d268824e35e59e3426c63b9f3b04788b14af1d85f')
self.assertEqual(
seed.public_address(),
'43YjCQcHm8TAY2kKbSMHz6J8FDZQwjPxw1Cq1vQ7SsQVBNeYEUMwGTQHppi5ffwg3df2m56DYexj2hm5uaQDtqpTBnUVzmD')
def test_french(self):
seed = Seed(u"sauce exprimer chasse asile larve tacler digestion muguet rondeur sept clore narrer fluor arme torse dans glace tant salon sanguin globe quiche ficher flaque clore", "French")
self.assertEqual(
seed.secret_spend_key(),
'597703dd73d0da6b3996b83c3e1e2f602be4f0de453e15846171aa9076901603')
self.assertEqual(
seed.secret_view_key(),
'f6e448dbbeaa7682a541b3b5b7e2e8ebb614fac032f1c3dff659ca26ab430f09')
self.assertEqual(
seed.public_spend_key(),
'10b42e100196ef2a68eeec191a46d8dc5c83d73c0861c185e5244202cd432087')
self.assertEqual(
seed.public_view_key(),
'34c4c479d53b10d3e9c0a3d11432fd13611b12dc5b721c8ff3802329b7bac328')
self.assertEqual(
seed.public_address(),
'42FpfU7DfLi86RtY3ajKUKdrnKvXTx41WPPx6wsyp9XVPcfnrLDXxhucSphpzt3mDv4F1DMiCrfHmR5WPZq1erzn5bs4eA7')
def test_german(self):
seed = Seed(u"Erdgas Gesuch beeilen Chiffon Abendrot Alter Helium Salz Almweide Ampel Dichter Rotglut Dialekt Akkord Rampe Gesöff Ziege Boykott keuchen Krach Anbau Labor Esel Ferien Ampel", "German")
self.assertEqual(
seed.secret_spend_key(),
'193152abe15c5e0a0ff56e3020229398769cd7c6ca5a4e30e439d6702c4f320a')
self.assertEqual(
seed.secret_view_key(),
'cdb967c501195827d78a791e1173d4b8826a5ae73b0885984898c84b6c9dd80c')
self.assertEqual(
seed.public_spend_key(),
'32eac115ca4b072c18198966c7ac9cb63b9f701a691eb52bfa18345d0fbcd90f')
self.assertEqual(
seed.public_view_key(),
'06a2119dfa7c48bdc03ad251026fc509bd01f3a4f7521802ca31b93cf06539ac')
self.assertEqual(
seed.public_address(),
'43Z2BHsCkU68NmZrxzfZuuXUtUHCXWttt8MdcnNyDMkC3WmfoFb9byqYjpeBaC4Xtx2dUUv8YPv1d1U4krZCLzyWLUFif2E')
def test_italian(self):
seed = Seed(u"tramonto spuntare ruota afrodite binocolo riferire moneta assalire tuta firmare malattia flagello paradiso tacere sindrome spuntare sogliola volare follia versare insulto diagnosi lapide meteo malattia", "Italian")
self.assertEqual(
seed.secret_spend_key(),
'29c8d9e91c1cb59e059bddd901e011db85f8d4f00f967226ffb5e185bd10e70d')
self.assertEqual(
seed.secret_view_key(),
'1f224a0330ee358428fe91fa48b6986941030c34f2d1efecc4eb26ea9f838b02')
self.assertEqual(
seed.public_spend_key(),
'149bdad48fd1ca40e1eb3e323b676132e2cae1eedbd715ac131b97c2c749c6b4')
self.assertEqual(
seed.public_view_key(),
'efc1a3382c33ac58ecfdd3a71497b0d0aeef061d0af94e5c49278d653167d643')
self.assertEqual(
seed.public_address(),
'42QQUPDR9PoBrSc9rB5VvG9Wf7KmtjXhEVnLhGKif9rDXGK3n1e6rsVFsh62YDqDf5buVQXuL6oLHGSHg4ANgQUu8beDd9R')
def test_japanese(self):
seed = Seed(u"いもり すあな いきる しちょう うったえる ちひょう けなみ たいちょう うぶごえ しかい しなぎれ いっせい つかれる しなん ばあさん たいまつばな しひょう おいかける あんがい ていへん せんもん きこく せんく そそぐ つかれる", "Japanese")
self.assertFalse(seed.is_mymonero())
self.assertEqual(
seed.secret_spend_key(),
'a047598095d2ada065af73758f7082900b9b0d721b5f99a541a78bd461ffc607')
self.assertEqual(
seed.secret_view_key(),
'080c6135edf93233176d41c8535caef0f13d596dc5093b5a5afa4279339dbc00')
self.assertEqual(
seed.public_spend_key(),
'85d849793fce4d0238d991d3aab7ac790cee73e5732d378c216f11bd3b873e43')
self.assertEqual(
seed.public_view_key(),
'19dc462a6074a26fa7788b45e542a71ffdbd48502e41ae8790c46fd6de556de3')
self.assertEqual(
seed.public_address(),
'46hHs9s3boi1NZJHGSwMgfMFLpCBaKwdQQSSf7fqVjWdCDxudsDmqqbKgBkpYDX6JA6MMZG8o5yrMPg9ztrXHdEkSfUA131')
def test_portuguese(self):
seed = Seed(u"rebuscar mefistofelico luto isca vulva ontologico autuar epiteto jarro invulneravel inquisitorial vietnamita voile potro mamute giroscopio scherzo cheroqui gueto loquaz fissurar fazer violoncelo viquingue vulva", "Portuguese")
self.assertFalse(seed.is_mymonero())
self.assertEqual(
seed.secret_spend_key(),
'60916cfcb10fa0b2b0648e36ecd7037f5c1972d36b2e6d56c2f4feca613a4200')
self.assertEqual(
seed.secret_view_key(),
'b23941e3f4da76e0fab171d94a36fe70031fb501f1f80e0cb3b4b4638b5f7106')
self.assertEqual(
seed.public_spend_key(),
'340c89026a03637e8b0abda566ac99b98a7c85b30a81281be19af869c3631dfb')
self.assertEqual(
seed.public_view_key(),
'23bb38c5e34867c49a65f0e7192138483361d419febbd429f256088e5e62a55e')
self.assertEqual(
seed.public_address(),
'43bWUqKAoYWNAdMtuaSF2pY2yptw7zfCB5fV2fXLkYTvj1NNYUKM4aaZtJCVYJunHuD5SNE2CPTCo81wDhZc8bReBidbX1w')
def test_russian(self):
seed = Seed(u"дощатый ателье мыло паек азот ружье домашний уныние уплата торговля шкаф кекс газета тревога улица армия лазерный иголка друг хищник пашня дневник кричать лыжный иголка", "Russian")
self.assertEqual(
seed.secret_spend_key(),
'6dc31f6ebcf834ab375a69006cb19c66fcccfa0732dfb3ea1b0662b455226b0d')
self.assertEqual(
seed.secret_view_key(),
'5467825ef0148a11582115f80b01c9af90fe31216a9cf6fb2d6b3c78698ce80a')
self.assertEqual(
seed.public_spend_key(),
'200657c6d14ab19cd3fccd8634e8f23e81290a559b8eb5e58dda3696553ddffc')
self.assertEqual(
seed.public_view_key(),
'f7563d9efb1c03a299b9c91a604caf7fd0c5a6998fdeedf18b58a63930958a24')
self.assertEqual(
seed.public_address(),
'42qVnaWnHSGTERsT6diSvdBTNbHfQZauSfPxpc5EuHc2jK699E28uwpUCRrHr9aaZ4NNyJ9ABdxX6hQHPHv2YcW55A26UbQ')
def test_spanish(self):
seed = Seed(u"riesgo lápiz martes fuerza dinero pupila pago mensaje guion libro órgano juntar imperio puñal historia pasión nación posible paso límite don afirmar receta reposo fuerza", "Spanish")
self.assertFalse(seed.is_mymonero())
self.assertEqual(
seed.secret_spend_key(),
'5973d91299466a9a51ddfcd20d1710c776aa1399279b292b264ab6b7ab608105')
self.assertEqual(
seed.secret_view_key(),
'5f7a66cf32120515870f89e3a156ec2024154334a3b43af1da05244ec4cf250d')
self.assertEqual(
seed.public_spend_key(),
'42161417635c6bd31a8dce8c2bd3b5f4879369fb732073d9f6fa82b18329c7f7')
self.assertEqual(
seed.public_view_key(),
'6acc984fecb5894b5661d446954ffcfe302cd1d2cf0e5177c2553aafb1dc3d2a')
self.assertEqual(
seed.public_address(),
'448MxehQwbgcJyJ3fKnTYYhuF7g7cs7AJdTXoybMu8UEiPFtFpEVNTaDbsK5vatPHVjWwjvJfyWKiM2pBKXJrg4U5qeGXjZ')
if __name__ == "__main__":
unittest.main()