#
# Copyright (c) 2013 Pavol Rusnak
# Copyright (c) 2017 mruddy
#
# 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.
#
import hashlib
import hmac
import itertools
import os
import secrets
import unicodedata
from typing import AnyStr, List, TypeVar, Union
_T = TypeVar("_T")
PBKDF2_ROUNDS = 2048
[docs]class ConfigurationError(Exception):
pass
# Refactored code segments from <https://github.com/keis/base58>
[docs]def b58encode(v: bytes) -> str:
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
p, acc = 1, 0
for c in reversed(v):
acc += p * c
p = p << 8
string = ""
while acc:
acc, idx = divmod(acc, 58)
string = alphabet[idx : idx + 1] + string
return string
[docs]class Mnemonic(object):
def __init__(self, language: str = "english"):
self.language = language
self.radix = 2048
d = os.path.join(os.path.dirname(__file__), f"wordlist/{language}.txt")
if os.path.exists(d) and os.path.isfile(d):
with open(d, "r", encoding="utf-8") as f:
self.wordlist = [w.strip() for w in f.readlines()]
if len(self.wordlist) != self.radix:
raise ConfigurationError(
f"Wordlist should contain {self.radix} words, but it's {len(self.wordlist)} words long instead."
)
else:
raise ConfigurationError("Language not detected")
# Japanese must be joined by ideographic space
self.delimiter = "\u3000" if language == "japanese" else " "
[docs] @classmethod
def list_languages(cls) -> List[str]:
return [
f.split(".")[0]
for f in os.listdir(os.path.join(os.path.dirname(__file__), "wordlist"))
if f.endswith(".txt")
]
[docs] @staticmethod
def normalize_string(txt: AnyStr) -> str:
if isinstance(txt, bytes):
utxt = txt.decode("utf8")
elif isinstance(txt, str):
utxt = txt
else:
raise TypeError("String value expected")
return unicodedata.normalize("NFKD", utxt)
[docs] @classmethod
def detect_language(cls, code: str) -> str:
"""Scan the Mnemonic until the language becomes unambiguous."""
code = cls.normalize_string(code)
possible = set(cls(lang) for lang in cls.list_languages())
for word in code.split():
possible = set(p for p in possible if word in p.wordlist)
if not possible:
raise ConfigurationError(f"Language unrecognized for {word!r}")
if len(possible) == 1:
return possible.pop().language
raise ConfigurationError(
f"Language ambiguous between {', '.join( p.language for p in possible)}"
)
[docs] def generate(self, strength: int = 128) -> str:
"""
Create a new mnemonic using a random generated number as entropy.
As defined in BIP39, the entropy must be a multiple of 32 bits, and its size must be between 128 and 256 bits.
Therefore the possible values for `strength` are 128, 160, 192, 224 and 256.
If not provided, the default entropy length will be set to 128 bits.
The return is a list of words that encodes the generated entropy.
:param strength: Number of bytes used as entropy
:type strength: int
:return: A randomly generated mnemonic
:rtype: str
"""
if strength not in [128, 160, 192, 224, 256]:
raise ValueError(
"Invalid strength value. Allowed values are [128, 160, 192, 224, 256]."
)
return self.to_mnemonic(secrets.token_bytes(strength // 8))
# Adapted from BitcoinJ
# https://github.com/bitcoinj/bitcoinj/blob/6aa4e51de6004f7171802bbce9c1660228d05aae/core/src/main/java/org/bitcoinj/crypto/MnemonicCode.java#L147
[docs] def to_entropy(self, words: Union[List[str], str]) -> bytearray:
if not isinstance(words, list):
words = words.split(" ")
if len(words) not in [12, 15, 18, 21, 24]:
raise ValueError(
"Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)."
% len(words)
)
# Look up all the words in the list and construct the
# concatenation of the original entropy and the checksum.
concat_len_bits = len(words) * 11
concat_bits = [False] * concat_len_bits
wordindex = 0
missing_word = next((word for word in words if word not in self.wordlist), None)
if missing_word is not None:
raise LookupError('Unable to find "%s" in word list.' % missing_word)
for word in words:
# Find the words index in the wordlist
ndx = self.wordlist.index(word)
# Set the next 11 bits to the value of the index.
concat_bits[(wordindex * 11) + 0] = (ndx & 0x400) != 0
concat_bits[(wordindex * 11) + 1] = (ndx & 0x200) != 0
concat_bits[(wordindex * 11) + 2] = (ndx & 0x100) != 0
concat_bits[(wordindex * 11) + 3] = (ndx & 0x80) != 0
concat_bits[(wordindex * 11) + 4] = (ndx & 0x40) != 0
concat_bits[(wordindex * 11) + 5] = (ndx & 0x20) != 0
concat_bits[(wordindex * 11) + 6] = (ndx & 0x10) != 0
concat_bits[(wordindex * 11) + 7] = (ndx & 0x8) != 0
concat_bits[(wordindex * 11) + 8] = (ndx & 0x4) != 0
concat_bits[(wordindex * 11) + 9] = (ndx & 0x2) != 0
concat_bits[(wordindex * 11) + 10] = (ndx & 0x1) != 0
wordindex += 1
checksum_length_bits = concat_len_bits // 33
entropy_length_bits = concat_len_bits - checksum_length_bits
# Extract original entropy as bytes.
entropy = bytearray(entropy_length_bits // 8)
for ii in range(len(entropy)):
for jj in range(8):
if concat_bits[(ii * 8) + jj]:
entropy[ii] |= 1 << (7 - jj)
# Take the digest of the entropy.
hash_bytes = hashlib.sha256(entropy).digest()
hash_bits = list(
itertools.chain.from_iterable(
[c & (1 << (7 - i)) != 0 for i in range(8)] for c in hash_bytes
)
)
# Check all the checksum bits.
for i in range(checksum_length_bits):
if concat_bits[entropy_length_bits + i] != hash_bits[i]:
raise ValueError("Failed checksum.")
return entropy
[docs] def to_mnemonic(self, data: bytes) -> str:
if len(data) not in [16, 20, 24, 28, 32]:
raise ValueError(
f"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not {len(data)}."
)
h = hashlib.sha256(data).hexdigest()
b = (
bin(int.from_bytes(data, byteorder="big"))[2:].zfill(len(data) * 8)
+ bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
)
result = []
for i in range(len(b) // 11):
idx = int(b[i * 11 : (i + 1) * 11], 2)
result.append(self.wordlist[idx])
return self.delimiter.join(result)
[docs] def check(self, mnemonic: str) -> bool:
mnemonic_list = self.normalize_string(mnemonic).split(" ")
# list of valid mnemonic lengths
if len(mnemonic_list) not in [12, 15, 18, 21, 24]:
return False
try:
idx = map(
lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic_list
)
b = "".join(idx)
except ValueError:
return False
l = len(b) # noqa: E741
d = b[: l // 33 * 32]
h = b[-l // 33 :]
nd = int(d, 2).to_bytes(l // 33 * 4, byteorder="big")
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
return h == nh
[docs] def expand_word(self, prefix: str) -> str:
if prefix in self.wordlist:
return prefix
else:
matches = [word for word in self.wordlist if word.startswith(prefix)]
if len(matches) == 1: # matched exactly one word in the wordlist
return matches[0]
else:
# exact match not found.
# this is not a validation routine, just return the input
return prefix
[docs] def expand(self, mnemonic: str) -> str:
return " ".join(map(self.expand_word, mnemonic.split(" ")))
[docs] @classmethod
def to_seed(cls, mnemonic: str, passphrase: str = "") -> bytes:
mnemonic = cls.normalize_string(mnemonic)
passphrase = cls.normalize_string(passphrase)
passphrase = "mnemonic" + passphrase
mnemonic_bytes = mnemonic.encode("utf-8")
passphrase_bytes = passphrase.encode("utf-8")
stretched = hashlib.pbkdf2_hmac(
"sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
)
return stretched[:64]
[docs] @staticmethod
def to_hd_master_key(seed: bytes, testnet: bool = False) -> str:
if len(seed) != 64:
raise ValueError("Provided seed should have length of 64")
# Compute HMAC-SHA512 of seed
seed = hmac.new(b"Bitcoin seed", seed, digestmod=hashlib.sha512).digest()
# Serialization format can be found at:
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
xprv = b"\x04\x88\xad\xe4" # Version for private mainnet
if testnet:
xprv = b"\x04\x35\x83\x94" # Version for private testnet
xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number
xprv += seed[32:] # Chain code
xprv += b"\x00" + seed[:32] # Master key
# Double hash using SHA256
hashed_xprv = hashlib.sha256(xprv).digest()
hashed_xprv = hashlib.sha256(hashed_xprv).digest()
# Append 4 bytes of checksum
xprv += hashed_xprv[:4]
# Return base58
return b58encode(xprv)