2018-02-19 17:35:30 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
# Copyright (C) 2011 thomasv@gitorious
|
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
#
|
|
|
|
# Note about US patent no 5892470: Here each word does not represent a given digit.
|
|
|
|
# Instead, the digit represented by a word is variable, it depends on the previous word.
|
|
|
|
#
|
|
|
|
# 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
|
2018-05-26 15:58:39 +00:00
|
|
|
#
|
|
|
|
# Significantly modified on 26 May 2018 by Michal Salaban:
|
|
|
|
# + support for 12/13-word seeds
|
|
|
|
# + simplified interface, changed exceptions (assertions -> explicit raise)
|
|
|
|
# + optimization
|
2018-02-19 17:35:30 +00:00
|
|
|
|
|
|
|
from monero import wordlists
|
2018-02-22 04:41:20 +00:00
|
|
|
from monero import ed25519
|
|
|
|
from monero import base58
|
2018-06-04 10:53:45 +00:00
|
|
|
from monero.address import address
|
2018-06-03 02:28:33 +00:00
|
|
|
from binascii import hexlify, unhexlify
|
2018-02-19 17:35:30 +00:00
|
|
|
from os import urandom
|
2018-02-22 04:41:20 +00:00
|
|
|
from sha3 import keccak_256
|
2018-02-19 17:35:30 +00:00
|
|
|
|
|
|
|
class Seed(object):
|
|
|
|
"""Creates a seed object either from local system randomness or an imported phrase.
|
|
|
|
|
|
|
|
:rtype: :class:`Seed <monero.seed.Seed>`
|
|
|
|
"""
|
|
|
|
|
2018-06-03 02:28:33 +00:00
|
|
|
def __init__(self, phrase_or_hex="", wordlist="English"):
|
2018-02-19 17:35:30 +00:00
|
|
|
"""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>`
|
|
|
|
"""
|
2018-06-03 02:28:33 +00:00
|
|
|
self.phrase = "" #13 or 25 word mnemonic word string
|
|
|
|
self.hex = "" # hexadecimal
|
|
|
|
|
|
|
|
self.word_list = wordlists.get_wordlist(wordlist)
|
|
|
|
|
2018-05-26 15:58:39 +00:00
|
|
|
if phrase_or_hex:
|
|
|
|
seed_split = phrase_or_hex.split(" ")
|
2018-02-19 17:35:30 +00:00
|
|
|
if len(seed_split) >= 24:
|
|
|
|
# standard mnemonic
|
2018-05-26 15:58:39 +00:00
|
|
|
self.phrase = phrase_or_hex
|
2018-02-19 17:35:30 +00:00
|
|
|
if len(seed_split) == 25:
|
|
|
|
# with checksum
|
2018-05-26 15:58:39 +00:00
|
|
|
self._validate_checksum()
|
|
|
|
self._decode_seed()
|
2018-02-19 17:35:30 +00:00
|
|
|
elif len(seed_split) >= 12:
|
|
|
|
# mymonero mnemonic
|
2018-05-26 15:58:39 +00:00
|
|
|
self.phrase = phrase_or_hex
|
2018-02-19 17:35:30 +00:00
|
|
|
if len(seed_split) == 13:
|
|
|
|
# with checksum
|
2018-05-26 15:58:39 +00:00
|
|
|
self._validate_checksum()
|
|
|
|
self._decode_seed()
|
2018-02-19 17:35:30 +00:00
|
|
|
elif len(seed_split) == 1:
|
|
|
|
# single string, probably hex, but confirm
|
2018-05-26 15:58:39 +00:00
|
|
|
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()
|
2018-02-19 17:35:30 +00:00
|
|
|
else:
|
2018-05-26 15:58:39 +00:00
|
|
|
raise ValueError("Not valid mnemonic phrase or hex: {arg}".format(arg=phrase_or_hex))
|
2018-02-19 17:35:30 +00:00
|
|
|
else:
|
|
|
|
self.hex = generate_hex()
|
2018-05-26 15:58:39 +00:00
|
|
|
self._encode_seed()
|
2018-02-19 17:35:30 +00:00
|
|
|
|
2018-05-26 15:42:55 +00:00
|
|
|
def is_mymonero(self):
|
|
|
|
"""Returns True if the seed is MyMonero-style (12/13-word)."""
|
|
|
|
return len(self.hex) == 32
|
2018-02-19 17:35:30 +00:00
|
|
|
|
2018-05-26 15:58:39 +00:00
|
|
|
def _encode_seed(self):
|
|
|
|
"""Convert hexadecimal string to mnemonic word representation with checksum.
|
2018-02-19 17:35:30 +00:00
|
|
|
"""
|
2018-06-03 02:28:33 +00:00
|
|
|
self.phrase = self.word_list.encode(self.hex)
|
2018-02-19 17:35:30 +00:00
|
|
|
|
2018-05-26 15:58:39 +00:00
|
|
|
def _decode_seed(self):
|
|
|
|
"""Calculate hexadecimal representation of the phrase.
|
2018-02-19 17:35:30 +00:00
|
|
|
"""
|
2018-06-03 02:28:33 +00:00
|
|
|
self.hex = self.word_list.decode(self.phrase)
|
2018-02-19 17:35:30 +00:00
|
|
|
|
2018-05-26 15:58:39 +00:00
|
|
|
def _validate_checksum(self):
|
2018-02-19 17:35:30 +00:00
|
|
|
"""Given a mnemonic word string, confirm seed checksum (last word) matches the computed checksum.
|
|
|
|
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
|
|
|
phrase = self.phrase.split(" ")
|
2018-06-03 02:28:33 +00:00
|
|
|
if self.word_list.get_checksum(self.phrase) == phrase[-1]:
|
2018-05-26 15:58:39 +00:00
|
|
|
return True
|
|
|
|
raise ValueError("Invalid checksum")
|
2018-02-19 17:35:30 +00:00
|
|
|
|
2018-02-22 04:41:20 +00:00
|
|
|
def sc_reduce(self, input):
|
2018-05-26 16:01:11 +00:00
|
|
|
integer = ed25519.decodeint(input)
|
2018-02-22 04:41:20 +00:00
|
|
|
modulo = integer % ed25519.l
|
2018-05-24 10:38:59 +00:00
|
|
|
return hexlify(ed25519.encodeint(modulo)).decode()
|
2018-02-22 04:41:20 +00:00
|
|
|
|
|
|
|
def hex_seed(self):
|
|
|
|
return self.hex
|
|
|
|
|
2018-05-26 15:42:55 +00:00
|
|
|
def _hex_seed_keccak(self):
|
|
|
|
h = keccak_256()
|
|
|
|
h.update(unhexlify(self.hex))
|
2018-05-26 16:01:11 +00:00
|
|
|
return h.digest()
|
2018-05-26 15:42:55 +00:00
|
|
|
|
2018-02-22 04:41:20 +00:00
|
|
|
def secret_spend_key(self):
|
2018-05-26 16:01:11 +00:00
|
|
|
a = self._hex_seed_keccak() if self.is_mymonero() else unhexlify(self.hex)
|
2018-05-26 15:42:55 +00:00
|
|
|
return self.sc_reduce(a)
|
2018-02-22 04:41:20 +00:00
|
|
|
|
|
|
|
def secret_view_key(self):
|
2018-05-26 16:01:11 +00:00
|
|
|
b = self._hex_seed_keccak() if self.is_mymonero() else unhexlify(self.secret_spend_key())
|
2018-02-22 04:41:20 +00:00
|
|
|
h = keccak_256()
|
2018-05-26 16:01:11 +00:00
|
|
|
h.update(b)
|
|
|
|
return self.sc_reduce(h.digest())
|
2018-02-22 04:41:20 +00:00
|
|
|
|
|
|
|
def public_spend_key(self):
|
2018-11-30 00:23:49 +00:00
|
|
|
return ed25519.public_from_secret_hex(self.secret_spend_key())
|
2018-02-22 04:41:20 +00:00
|
|
|
|
|
|
|
def public_view_key(self):
|
2018-11-30 00:23:49 +00:00
|
|
|
return ed25519.public_from_secret_hex(self.secret_view_key())
|
2018-02-22 04:41:20 +00:00
|
|
|
|
2018-05-24 10:38:59 +00:00
|
|
|
def public_address(self, net='mainnet'):
|
|
|
|
"""Returns the master :class:`Address <monero.address.Address>` represented by the seed.
|
|
|
|
|
|
|
|
:param net: the network, one of 'mainnet', 'testnet', 'stagenet'. Default is 'mainnet'.
|
|
|
|
|
|
|
|
:rtype: :class:`Address <monero.address.Address>`
|
|
|
|
"""
|
|
|
|
if net not in ('mainnet', 'testnet', 'stagenet'):
|
|
|
|
raise ValueError(
|
|
|
|
"Invalid net argument. Must be one of ('mainnet', 'testnet', 'stagenet').")
|
|
|
|
netbyte = 18 if net == 'mainnet' else 53 if net == 'testnet' else 24
|
|
|
|
data = "{:x}{:s}{:s}".format(netbyte, self.public_spend_key(), self.public_view_key())
|
2018-02-22 04:41:20 +00:00
|
|
|
h = keccak_256()
|
|
|
|
h.update(unhexlify(data))
|
2018-05-24 10:38:59 +00:00
|
|
|
checksum = h.hexdigest()
|
2018-06-04 10:53:45 +00:00
|
|
|
return address(base58.encode(data + checksum[0:8]))
|
2018-02-22 04:41:20 +00:00
|
|
|
|
2018-02-19 17:35:30 +00:00
|
|
|
|
|
|
|
def generate_hex(n_bytes=32):
|
|
|
|
"""Generate a secure and random hexadecimal string. 32 bytes by default, but arguments can override.
|
|
|
|
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
h = hexlify(urandom(n_bytes))
|
|
|
|
return "".join(h.decode("utf-8"))
|