From ce9427180b7760ec75cd9ea0603a762c747a81b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Sat, 26 May 2018 17:58:39 +0200 Subject: [PATCH] Simplify the Seed class interface, raise ValueError instead of assertions --- monero/seed.py | 66 +++++++++++++++++++++------------------------- tests/test_seed.py | 29 +++++++------------- 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/monero/seed.py b/monero/seed.py index 4f3c1de..95264b2 100644 --- a/monero/seed.py +++ b/monero/seed.py @@ -29,6 +29,11 @@ # Copied 17 February 2018 from MoneroPy, originally from Electrum: # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/mnemonic.py ch: 80cc16c39b16c55a8d052fbf7fae68644f7a5f02 # https://github.com/spesmilo/electrum/blob/master/lib/old_mnemonic.py ch:9a0aa9b4783ea03ea13c6d668e080e0cdf261c5b +# +# Significantly modified on 26 May 2018 by Michal Salaban: +# + support for 12/13-word seeds +# + simplified interface, changed exceptions (assertions -> explicit raise) +# + optimization from monero import address from monero import wordlists @@ -50,40 +55,40 @@ class Seed(object): phrase = "" #13 or 25 word mnemonic word string hex = "" # hexadecimal - def __init__(self, phrase=""): + def __init__(self, phrase_or_hex=""): """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 ` """ - if phrase: - seed_split = phrase.split(" ") + if phrase_or_hex: + seed_split = phrase_or_hex.split(" ") if len(seed_split) >= 24: # standard mnemonic - self.phrase = phrase + self.phrase = phrase_or_hex if len(seed_split) == 25: # with checksum - self.validate_checksum() - self.hex = self.decode_seed() + self._validate_checksum() + self._decode_seed() elif len(seed_split) >= 12: # mymonero mnemonic - self.phrase = phrase + self.phrase = phrase_or_hex if len(seed_split) == 13: # with checksum - self.validate_checksum() - self.hex = self.decode_seed() + self._validate_checksum() + self._decode_seed() elif len(seed_split) == 1: # single string, probably hex, but confirm - if not len(phrase) % 8 == 0: - raise ValueError("Not valid hexadecimal: {hex}".format(hex=phrase)) - self.hex = phrase - self.phrase = self.encode_seed() + if not len(phrase_or_hex) % 8 == 0: + raise ValueError("Not valid hexadecimal: {hex}".format(hex=phrase_or_hex)) + self.hex = phrase_or_hex + self._encode_seed() else: - raise ValueError("Not valid mnemonic phrase: {phrase}".format(phrase=phrase)) + raise ValueError("Not valid mnemonic phrase or hex: {arg}".format(arg=phrase_or_hex)) else: self.hex = generate_hex() - self.encode_seed() + self._encode_seed() def is_mymonero(self): """Returns True if the seed is MyMonero-style (12/13-word).""" @@ -96,13 +101,9 @@ class Seed(object): """ return "".join([word[i:i+2] for i in [6, 4, 2, 0]]) - def encode_seed(self): - """Given a hexadecimal string, return it's mnemonic word representation with checksum. - - :rtype: str + def _encode_seed(self): + """Convert hexadecimal string to mnemonic word representation with checksum. """ - assert self.hex, "Seed hex not set" - assert len(self.hex) % 8 == 0, "Not valid hexadecimal" out = [] for i in range(len(self.hex) // 8): word = self.endian_swap(self.hex[8*i:8*i+8]) @@ -114,16 +115,11 @@ class Seed(object): checksum = get_checksum(" ".join(out)) out.append(checksum) self.phrase = " ".join(out) - return self.phrase - def decode_seed(self): - """Given a mnemonic word string, return it's hexadecimal representation. - - :rtype: str + def _decode_seed(self): + """Calculate hexadecimal representation of the phrase. """ - assert self.phrase, "Seed phrase not set" phrase = self.phrase.split(" ") - assert len(phrase) >= 12, "Not valid mnemonic phrase" out = "" for i in range(len(phrase) // 3): word1, word2, word3 = phrase[3*i:3*i+3] @@ -133,19 +129,16 @@ class Seed(object): 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 - return self.hex - def validate_checksum(self): + def _validate_checksum(self): """Given a mnemonic word string, confirm seed checksum (last word) matches the computed checksum. :rtype: bool """ - assert self.phrase, "Seed phrase not set" phrase = self.phrase.split(" ") - assert len(phrase) > 12, "Not valid mnemonic phrase" - is_match = get_checksum(self.phrase) == phrase[-1] - assert is_match, "Not valid checksum" - return is_match + if get_checksum(self.phrase) == phrase[-1]: + return True + raise ValueError("Invalid checksum") def sc_reduce(self, input): integer = ed25519.decodeint(unhexlify(input)) @@ -204,7 +197,8 @@ def get_checksum(phrase): :rtype: str """ phrase_split = phrase.split(" ") - assert len(phrase_split) >= 12, "Not valid mnemonic phrase" + if len(phrase_split) < 12: + raise ValueError("Invalid mnemonic phrase") if len(phrase_split) > 13: # Standard format phrase = phrase_split[:24] diff --git a/tests/test_seed.py b/tests/test_seed.py index 1d12993..ab9b1cc 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -6,46 +6,37 @@ from monero.seed import Seed, get_checksum class SeedTestCase(unittest.TestCase): def test_mnemonic_seed(self): - # Known good 25 word seed phrases should construct a class, validate checksum, and register valid hex + # Known good 25 word seed phrases should construct a class and register valid hex seed = Seed("wedge going quick racetrack auburn physics lectures light waist axes whipped habitat square awkward together injury niece nugget guarded hive obnoxious waxing faked folding square") - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.hex, "8ffa9f586b86d294d93731765d192765311bddc76a4fa60311f8af36bbf6fb06") - # Known good 24 word seed phrases should construct a class, store phrase with valid checksum, and register valid hex + # Known good 24 word seed phrases should construct a class, store phrase, and register valid hex seed = Seed("wedge going quick racetrack auburn physics lectures light waist axes whipped habitat square awkward together injury niece nugget guarded hive obnoxious waxing faked folding") - seed.encode_seed() - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.hex, "8ffa9f586b86d294d93731765d192765311bddc76a4fa60311f8af36bbf6fb06") - # Known good 25 word hexadecimal strings should construct a class, store phrase with valid checksum, and register valid hex + # Known good 25 word hexadecimal strings should construct a class, store phrase, and register valid hex seed = Seed("8ffa9f586b86d294d93731765d192765311bddc76a4fa60311f8af36bbf6fb06") - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.phrase, "wedge going quick racetrack auburn physics lectures light waist axes whipped habitat square awkward together injury niece nugget guarded hive obnoxious waxing faked folding square") + self.assertTrue(len(seed.hex) % 8 == 0) - # Known good 13 word seed phrases should construct a class, validate checksum, and register valid hex + # Known good 13 word seed phrases should construct a class and register valid hex seed = Seed("ought knowledge upright innocent eldest nerves gopher fowls below exquisite aces basin fowls") - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.hex, "932d70711acc2d536ca11fcb79e05516") - # Known good 12 word seed phrases should construct a class, store phrase with valid checksum, and register valid hex + # Known good 12 word seed phrases should construct a class, store phrase, and register valid hex seed = Seed("ought knowledge upright innocent eldest nerves gopher fowls below exquisite aces basin") - seed.encode_seed() - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.hex, "932d70711acc2d536ca11fcb79e05516") - # Known good 13 word hexadecimal strings should construct a class, store phrase with valid checksum, and register valid hex + # Known good 13 word hexadecimal strings should construct a class, store phrase, and register valid hex seed = Seed("932d70711acc2d536ca11fcb79e05516") - self.assertTrue(seed.validate_checksum()) self.assertEqual(seed.phrase, "ought knowledge upright innocent eldest nerves gopher fowls below exquisite aces basin fowls") + self.assertTrue(len(seed.hex) % 8 == 0) - # Generated seed phrases should be 25 words, validate checksum, register valid hex, and return matching outputs for decode/encode + # Generated seed phrases should be 25 words, register valid hex seed = Seed() seed_split = seed.phrase.split(" ") self.assertTrue(len(seed_split) == 25) - self.assertTrue(seed.validate_checksum()) self.assertTrue(len(seed.hex) % 8 == 0) - self.assertEqual(seed.hex, seed.decode_seed()) - self.assertEqual(seed.phrase, seed.encode_seed()) # Invalid phrases should not be allowed with self.assertRaises(ValueError) as ts: @@ -67,7 +58,6 @@ class SeedTestCase(unittest.TestCase): "framed succeed fuzzy return demonstrate nucleus album noises peculiar virtual "\ "rowboat inorganic jester fuzzy") self.assertFalse(seed.is_mymonero()) - self.assertTrue(seed.validate_checksum()) self.assertEqual( seed.secret_spend_key(), '482700617ba810f94035d7f4d7ccc1a29878e165b4867872b705204c85406906') @@ -89,7 +79,6 @@ class SeedTestCase(unittest.TestCase): seed = Seed("dwelt idols lopped blender haggled rabbits piloted value swagger taunts toolbox upgrade swagger") self.assertTrue(seed.is_mymonero()) - self.assertTrue(seed.validate_checksum()) # the following fails, #21 addresses that self.assertEqual( seed.secret_spend_key(),