# -*- coding: utf-8 -*-
#
# BitcoinLib - Python Cryptocurrency Library
# MNEMONIC class for BIP0039 Mnemonic Key management
# © 2016 - 2022 October - 1200 Web Development <http://1200wd.com/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from bitcoinlib.encoding import *
from bitcoinlib.config.secp256k1 import secp256k1_n
[docs]class Mnemonic(object):
"""
Class to convert, generate and parse Mnemonic sentences
Implementation of BIP0039 for Mnemonics passphrases
Took some parts from Pavol Rusnak Trezors implementation, see https://github.com/trezor/python-mnemonic
"""
def __init__(self, language=DEFAULT_LANGUAGE):
"""
Init Mnemonic class and read wordlist of specified language
:param language: use specific wordlist, i.e. chinese, dutch (in development), english, french, italian, japanese or spanish. Leave empty for default 'english'
:type language: str
"""
self._wordlist = []
with Path(BCL_INSTALL_DIR, 'wordlist', '%s.txt' % language).open() as f:
self._wordlist = [w.strip() for w in f.readlines()]
[docs] @staticmethod
def checksum(data):
"""
Calculates checksum for given data key
:param data: key string
:type data: bytes, hexstring
:return str: Checksum of key in bits
"""
data = to_bytes(data)
if len(data) % 4 > 0:
raise ValueError('Data length in bits should be divisible by 32, but it is not (%d bytes = %d bits).' %
(len(data), len(data) * 8))
key_hash = hashlib.sha256(data).digest()
return change_base(key_hash, 256, 2, 256)[:len(data) * 8 // 32]
[docs] def to_seed(self, words, password='', validate=True):
"""
Use Mnemonic words and optionally a password to create a PBKDF2 seed (Password-Based Key Derivation Function 2)
First use 'sanitize_mnemonic' to determine language and validate and check words
>>> Mnemonic().to_seed('chunk gun celery million wood kite tackle twenty story episode raccoon dutch').hex()
'6969ed4666db67fc74fae7869e2acf3c766b5ef95f5e31eb2fcebd93d76069c6de971225f700042b0b513f0ad6c8562277fc4b5ee1344b720f1686dc2dccc220'
:param words: Mnemonic passphrase as string with space separated words
:type words: str
:param password: A password to protect key, leave empty to disable
:type password: str
:param validate: Validate checksum for given word phrase, default is True
:type validate: bool
:return bytes: PBKDF2 seed
"""
words = self.sanitize_mnemonic(words)
# Check if passphrase is valid
if validate:
self.to_entropy(words)
mnemonic = to_bytes(words)
password = to_bytes(password)
return hashlib.pbkdf2_hmac(hash_name='sha512', password=mnemonic, salt=b'mnemonic' + password,
iterations=2048)
[docs] def word(self, index):
"""
Get word from wordlist
:param index: word index ID
:type index: int
:return str: A word from the dictionary
"""
return self._wordlist[index]
[docs] def wordlist(self):
"""
Get full selected wordlist. A wordlist is selected when initializing Mnemonic class
:return list: Full list with 2048 words
"""
return self._wordlist
[docs] def generate(self, strength=128, add_checksum=True):
"""
Generate a random Mnemonic key
Uses cryptographically secure os.urandom() function to generate data. Then creates a Mnemonic sentence with
the 'to_mnemonic' method.
:param strength: Key strength in number of bits as multiply of 32, default is 128 bits. It advised to specify 128 bits or more, i.e.: 128, 256, 512 or 1024
:type strength: int
:param add_checksum: Included a checksum? Default is True
:type add_checksum: bool
:return str: Mnemonic passphrase consisting of a space seperated list of words
"""
if strength % 32 > 0:
raise ValueError("Strength should be divisible by 32")
data = os.urandom(strength // 8)
return self.to_mnemonic(data, add_checksum=add_checksum)
[docs] def to_mnemonic(self, data, add_checksum=True, check_on_curve=True):
"""
Convert key data entropy to Mnemonic sentence
>>> Mnemonic().to_mnemonic('28acfc94465fd2f6774759d6897ec122')
'chunk gun celery million wood kite tackle twenty story episode raccoon dutch'
:param data: Key data entropy
:type data: bytes, hexstring
:param add_checksum: Included a checksum? Default is True
:type add_checksum: bool
:param check_on_curve: Check if data integer value is on secp256k1 curve. Should be enabled when not testing and working with crypto
:type check_on_curve: bool
:return str: Mnemonic passphrase consisting of a space seperated list of words
"""
data = to_bytes(data)
data_int = int.from_bytes(data, 'big')
if check_on_curve and not 0 < data_int < secp256k1_n:
raise ValueError("Integer value of data should be in secp256k1 domain between 1 and secp256k1_n-1")
if add_checksum:
binresult = change_base(data_int, 10, 2, len(data) * 8) + self.checksum(data)
wi = change_base(binresult, 2, 2048)
else:
wi = change_base(data_int, 10, 2048, len(data) // 1.375 + len(data) % 1.375 > 0)
return normalize_string(' '.join([self._wordlist[i] for i in wi]))
[docs] def to_entropy(self, words, includes_checksum=True):
"""
Convert Mnemonic words back to key data entropy
>>> Mnemonic().to_entropy('chunk gun celery million wood kite tackle twenty story episode raccoon dutch').hex()
'28acfc94465fd2f6774759d6897ec122'
:param words: Mnemonic words as string of list of words
:type words: str
:param includes_checksum: Boolean to specify if checksum is used. Default is True
:type includes_checksum: bool
:return bytes: Entropy seed
"""
words = self.sanitize_mnemonic(words)
if isinstance(words, TYPE_TEXT):
words = words.split(' ')
wi = []
for word in words:
wi.append(self._wordlist.index(word))
ent_length = int(len(words) * 4/3)
ent = change_base(wi, 2048, 256, ent_length, output_even=False)
if includes_checksum:
binresult = change_base(ent, 256, 2, len(ent) * 4)
ent = change_base(binresult[:-len(binresult) // 33], 2, 256, ent_length)
# Check checksum
checksum = binresult[-len(binresult) // 33:]
if checksum != self.checksum(ent):
raise ValueError("Invalid checksum %s for entropy %s" % (checksum, ent))
return ent
[docs] @staticmethod
def detect_language(words):
"""
Detect language of given phrase
>>> Mnemonic().detect_language('chunk gun celery million wood kite tackle twenty story episode raccoon dutch')
'english'
:param words: List of space separated words
:type words: str
:return str: Language
"""
words = normalize_string(words)
if isinstance(words, TYPE_TEXT):
words = words.split(' ')
wlcount = {}
for fn in Path(BCL_INSTALL_DIR, 'wordlist').iterdir():
if fn.suffix == ".txt":
with open(os.path.join(str(BCL_INSTALL_DIR), 'wordlist', fn), encoding='utf-8') as f:
wordlist = [w.strip() for w in f.readlines()]
language = fn.name.split('.')[0]
wlcount[language] = 0
for word in words:
if word in wordlist:
wlcount[language] += 1
detlang = max(wlcount.keys(), key=(lambda key: wlcount[key]))
if not wlcount[detlang]:
raise Warning("Could not detect language of Mnemonic sentence %s" % words)
return detlang
[docs] def sanitize_mnemonic(self, words):
"""
Check and convert list of words to utf-8 encoding.
Raises an error if unrecognised word is found
:param words: List of space separated words
:type words: str
:return str: Sanitized list of words
"""
words = normalize_string(words)
language = self.detect_language(words)
if isinstance(words, TYPE_TEXT):
words = words.split(' ')
with Path(BCL_INSTALL_DIR, 'wordlist', '%s.txt' % language).open() as f:
wordlist = [w.strip() for w in f.readlines()]
for word in words:
if word not in wordlist:
raise Warning("Unrecognised word %s in mnemonic sentence" % word.encode('utf8'))
return ' '.join(words)