#
# 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 binascii
import bisect
import hashlib
import hmac
import itertools
import os
import sys
import unicodedata
PBKDF2_ROUNDS = 2048
class ConfigurationError(Exception):
pass
# From <https://stackoverflow.com/questions/212358/binary-search-bisection-in-python/2233940#2233940>
def binary_search(a, x, lo=0, hi=None): # can't use a to specify default for hi
hi = hi if hi is not None else len(a) # hi defaults to len(a)
pos = bisect.bisect_left(a, x, lo, hi) # find insertion position
return pos if pos != hi and a[pos] == x else -1 # don't walk off the end
# Refactored code segments from <https://github.com/keis/base58>
def b58encode(v):
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
p, acc = 1, 0
for c in reversed(v):
if sys.version < "3":
c = ord(c)
acc += p * c
p = p << 8
string = ""
while acc:
acc, idx = divmod(acc, 58)
string = alphabet[idx : idx + 1] + string
return string
class Mnemonic(object):
def __init__(self, language):
self.radix = 2048
if sys.version < "3":
with open("%s/%s.txt" % (self._get_directory(), language), "r") as f:
self.wordlist = [w.strip().decode("utf8") for w in f.readlines()]
else:
with open(
"%s/%s.txt" % (self._get_directory(), language), "r", encoding="utf-8"
) as f:
self.wordlist = [w.strip() for w in f.readlines()]
if len(self.wordlist) != self.radix:
raise ConfigurationError(
"Wordlist should contain %d words, but it contains %d words."
% (self.radix, len(self.wordlist))
)
@classmethod
def _get_directory(cls):
return os.path.join(os.path.dirname(__file__), "wordlist")
@classmethod
def list_languages(cls):
return [
f.split(".")[0]
for f in os.listdir(cls._get_directory())
if f.endswith(".txt")
]
@classmethod
def normalize_string(cls, txt):
if isinstance(txt, str if sys.version < "3" else bytes):
utxt = txt.decode("utf8")
elif isinstance(txt, unicode if sys.version < "3" else str): # noqa: F821
utxt = txt
else:
raise TypeError("String value expected")
return unicodedata.normalize("NFKD", utxt)
@classmethod
def detect_language(cls, code):
code = cls.normalize_string(code)
first = code.split(" ")[0]
languages = cls.list_languages()
for lang in languages:
mnemo = cls(lang)
if first in mnemo.wordlist:
return lang
raise ConfigurationError("Language not detected")
def generate(self, strength=128):
if strength not in [128, 160, 192, 224, 256]:
raise ValueError(
"Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d)."
% strength
)
return self.to_mnemonic(os.urandom(strength // 8))
# Adapted from <http://tinyurl.com/oxmn476>
def to_entropy(self, words):
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.
concatLenBits = len(words) * 11
concatBits = [False] * concatLenBits
wordindex = 0
if self.detect_language(" ".join(words)) == "english":
use_binary_search = True
else:
use_binary_search = False
for word in words:
# Find the words index in the wordlist
ndx = (
binary_search(self.wordlist, word)
if use_binary_search
else self.wordlist.index(word)
)
if ndx < 0:
raise LookupError('Unable to find "%s" in word list.' % word)
# Set the next 11 bits to the value of the index.
for ii in range(11):
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0
wordindex += 1
checksumLengthBits = concatLenBits // 33
entropyLengthBits = concatLenBits - checksumLengthBits
# Extract original entropy as bytes.
entropy = bytearray(entropyLengthBits // 8)
for ii in range(len(entropy)):
for jj in range(8):
if concatBits[(ii * 8) + jj]:
entropy[ii] |= 1 << (7 - jj)
# Take the digest of the entropy.
hashBytes = hashlib.sha256(entropy).digest()
if sys.version < "3":
hashBits = list(
itertools.chain.from_iterable(
(
[ord(c) & (1 << (7 - i)) != 0 for i in range(8)]
for c in hashBytes
)
)
)
else:
hashBits = list(
itertools.chain.from_iterable(
([c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)
)
)
# Check all the checksum bits.
for i in range(checksumLengthBits):
if concatBits[entropyLengthBits + i] != hashBits[i]:
raise ValueError("Failed checksum.")
return entropy
def to_mnemonic(self, data):
if len(data) not in [16, 20, 24, 28, 32]:
raise ValueError(
"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d)."
% len(data)
)
h = hashlib.sha256(data).hexdigest()
b = (
bin(int(binascii.hexlify(data), 16))[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])
if (
self.detect_language(" ".join(result)) == "japanese"
): # Japanese must be joined by ideographic space.
result_phrase = u"\u3000".join(result)
else:
result_phrase = " ".join(result)
return result_phrase
def check(self, mnemonic):
mnemonic = self.normalize_string(mnemonic).split(" ")
# list of valid mnemonic lengths
if len(mnemonic) not in [12, 15, 18, 21, 24]:
return False
try:
idx = map(lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic)
b = "".join(idx)
except ValueError:
return False
l = len(b) # noqa: E741
d = b[: l // 33 * 32]
h = b[-l // 33 :]
nd = binascii.unhexlify(hex(int(d, 2))[2:].rstrip("L").zfill(l // 33 * 8))
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
return h == nh
def expand_word(self, prefix):
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
def expand(self, mnemonic):
return " ".join(map(self.expand_word, mnemonic.split(" ")))
@classmethod
def to_seed(cls, mnemonic, passphrase=""):
mnemonic = cls.normalize_string(mnemonic)
passphrase = cls.normalize_string(passphrase)
passphrase = "mnemonic" + passphrase
mnemonic = mnemonic.encode("utf-8")
passphrase = passphrase.encode("utf-8")
stretched = hashlib.pbkdf2_hmac("sha512", mnemonic, passphrase, PBKDF2_ROUNDS)
return stretched[:64]
@classmethod
def to_hd_master_key(cls, seed):
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
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)
def main():
import binascii
import sys
if len(sys.argv) > 1:
data = sys.argv[1]
else:
data = sys.stdin.readline().strip()
data = binascii.unhexlify(data)
m = Mnemonic("english")
print(m.to_mnemonic(data))
if __name__ == "__main__":
main()