Source code for robonomicsinterface.bip39
"""BIP39 English mnemonics and the mini secret Substrate derives from them.
Substrate does not use the standard BIP39 seed. It derives a 32-byte "mini
secret" from the mnemonic's *entropy*, not from the mnemonic string, with
PBKDF2-HMAC-SHA512 over 2048 rounds (``substrate-bip39::seed_from_entropy``).
The standard seed would give a different address from the same words, which
is why the tests pin this against vectors from substrate-interface.
"""
import hashlib
import secrets
import unicodedata
from pathlib import Path
from .errors import InvalidMnemonic
__all__ = [
"entropy_to_mnemonic",
"generate_mnemonic",
"mnemonic_to_entropy",
"mnemonic_to_mini_secret",
"validate_mnemonic",
]
_WORDLIST_FILE = Path(__file__).with_name("english.txt")
# The canonical BIP39 English wordlist, checked on load so that a damaged copy
# cannot silently change derived addresses.
_WORDLIST_SHA256 = "2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda"
_WORDLIST_SIZE = 2048
_PBKDF2_ROUNDS = 2048
_MINI_SECRET_BYTES = 32
VALID_WORD_COUNTS = (12, 15, 18, 21, 24)
def _read_wordlist() -> tuple[str, ...]:
data = _WORDLIST_FILE.read_bytes()
if hashlib.sha256(data).hexdigest() != _WORDLIST_SHA256:
raise RuntimeError("the bundled BIP39 wordlist does not match its checksum")
words = tuple(data.decode("utf-8").split())
if len(words) != _WORDLIST_SIZE:
raise RuntimeError(f"the bundled BIP39 wordlist has {len(words)} words, expected 2048")
return words
# Read at import time: Home Assistant imports integrations off the event loop,
# while a mnemonic may be generated inside it, where file I/O is not allowed.
_WORDLIST = _read_wordlist()
_INDEX = {word: index for index, word in enumerate(_WORDLIST)}
def _normalize(text: str) -> str:
return unicodedata.normalize("NFKD", text)
[docs]
def generate_mnemonic(word_count: int = 12) -> str:
"""Generate a new random mnemonic from the system CSPRNG."""
if word_count not in VALID_WORD_COUNTS:
raise InvalidMnemonic(f"word count must be one of {VALID_WORD_COUNTS}")
return entropy_to_mnemonic(secrets.token_bytes(word_count * 4 // 3))
def entropy_to_mnemonic(entropy: bytes) -> str:
"""Encode 16–32 bytes of entropy as a mnemonic with its checksum."""
if len(entropy) * 8 not in (128, 160, 192, 224, 256):
raise InvalidMnemonic(f"entropy must be 16, 20, 24, 28 or 32 bytes, not {len(entropy)}")
checksum_bits = len(entropy) * 8 // 32
checksum = hashlib.sha256(entropy).digest()[0] >> (8 - checksum_bits)
bits = int.from_bytes(entropy, "big") << checksum_bits | checksum
total_bits = len(entropy) * 8 + checksum_bits
return " ".join(
_WORDLIST[(bits >> shift) & (_WORDLIST_SIZE - 1)]
for shift in range(total_bits - 11, -1, -11)
)
def mnemonic_to_entropy(mnemonic: str) -> bytes:
"""Return the entropy behind a mnemonic; a bad checksum is refused."""
words = _normalize(mnemonic).lower().split()
if len(words) not in VALID_WORD_COUNTS:
raise InvalidMnemonic(f"a mnemonic has 12, 15, 18, 21 or 24 words, not {len(words)}")
bits = 0
for position, word in enumerate(words, start=1):
index = _INDEX.get(word)
if index is None:
# The position, not the word: the word is part of a secret.
raise InvalidMnemonic(f"word #{position} is not in the BIP39 English list")
bits = bits << 11 | index
total_bits = len(words) * 11
checksum_bits = total_bits // 33
entropy = (bits >> checksum_bits).to_bytes((total_bits - checksum_bits) // 8, "big")
expected = hashlib.sha256(entropy).digest()[0] >> (8 - checksum_bits)
if bits & ((1 << checksum_bits) - 1) != expected:
raise InvalidMnemonic("the mnemonic checksum does not match")
return entropy
[docs]
def validate_mnemonic(mnemonic: str) -> bool:
"""Whether the text is a valid mnemonic, words and checksum."""
try:
mnemonic_to_entropy(mnemonic)
except InvalidMnemonic:
return False
return True
def mnemonic_to_mini_secret(mnemonic: str, password: str = "") -> bytes:
"""The 32-byte secret Substrate derives from a mnemonic and an optional password."""
entropy = mnemonic_to_entropy(mnemonic)
salt = ("mnemonic" + _normalize(password)).encode("utf-8")
return hashlib.pbkdf2_hmac("sha512", entropy, salt, _PBKDF2_ROUNDS)[:_MINI_SECRET_BYTES]