Source code for bitcoinlib.mnemonic

# -*- coding: utf-8 -*-
#
#    BitcoinLib - Python Cryptocurrency Library
#    MNEMONIC class for BIP0039 Mnemonic Key management
#    © 2016 - 2024 June - 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 = bytes(words, 'utf8') password = bytes(password, 'utf8') 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)