mirror of
https://git.wownero.com/lza_menace/wownero-python.git
synced 2024-08-15 03:25:25 +00:00
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:
parent
7c26bb62d9
commit
4c9ca07006
6 changed files with 1790 additions and 1688 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import english
|
from .wordlist import get_wordlist, list_wordlists
|
||||||
|
from .english import English
|
||||||
|
|
|
@ -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.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
@ -28,9 +54,17 @@
|
||||||
#
|
#
|
||||||
# list of words from:
|
# list of words from:
|
||||||
# https://github.com/monero-project/monero/blob/master/src/mnemonics/english.h
|
# 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",
|
"abbey",
|
||||||
"abducts",
|
"abducts",
|
||||||
"ability",
|
"ability",
|
||||||
|
|
111
monero/wordlists/wordlist.py
Normal file
111
monero/wordlists/wordlist.py
Normal 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]])
|
|
@ -1,2 +1,3 @@
|
||||||
pysha3
|
pysha3
|
||||||
requests
|
requests
|
||||||
|
six
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue