staking-deposit-cli/eth2deposit/key_handling/key_derivation/mnemonic.py

94 lines
3.2 KiB
Python
Raw Normal View History

import os
2020-07-24 10:24:23 +00:00
import sys
2020-02-18 16:47:27 +00:00
from unicodedata import normalize
2020-02-18 16:44:32 +00:00
from secrets import randbits
2020-02-18 16:03:52 +00:00
from typing import (
Optional,
Sequence,
2020-05-26 09:32:20 +00:00
Tuple,
2020-02-18 16:03:52 +00:00
)
from eth2deposit.utils.crypto import (
2020-02-18 16:47:27 +00:00
SHA256,
PBKDF2,
)
2020-02-18 16:44:32 +00:00
2020-02-18 16:03:52 +00:00
2020-07-24 10:24:23 +00:00
def _resource_path(relative_path: str) -> str:
"""
Get the absolute path to a resource in a manner friendly to PyInstaller.
PyInstaller creates a temp folder and stores path in _MEIPASS which this function swaps
into a resource path so it is avaible both when building binaries and running natively.
"""
2020-07-24 10:24:23 +00:00
try:
base_path = sys._MEIPASS # type: ignore
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def _get_word_list(language: str, path: str) -> Sequence[str]:
"""
Given the language and path to the wordlist, return the list of BIP39 words.
Ref: https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
"""
2020-07-24 10:24:23 +00:00
path = _resource_path(path)
return open(os.path.join(path, '%s.txt' % language), encoding='utf-8').readlines()
2020-02-18 16:03:52 +00:00
def _get_word(*, word_list: Sequence[str], index: int) -> str:
"""
Return the corresponding word for the supplied index while stripping out '\\n' chars.
"""
if index >= 2048:
raise IndexError(f"`index` should be less than 2048. Got {index}.")
2020-02-18 17:18:01 +00:00
return word_list[index][:-1]
2020-05-26 09:32:20 +00:00
def get_seed(*, mnemonic: str, password: str) -> bytes:
2020-02-18 17:23:57 +00:00
"""
2020-05-26 09:32:20 +00:00
Derive the seed for the pre-image root of the tree.
Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
2020-02-18 17:23:57 +00:00
"""
2020-06-26 14:10:19 +00:00
encoded_mnemonic = normalize('NFKD', mnemonic).encode('utf-8')
2020-02-18 16:47:27 +00:00
salt = normalize('NFKD', 'mnemonic' + password).encode('utf-8')
2020-06-26 14:10:19 +00:00
return PBKDF2(password=encoded_mnemonic, salt=salt, dklen=64, c=2048, prf='sha512')
2020-02-18 16:47:27 +00:00
2020-05-26 09:32:20 +00:00
def get_languages(path: str) -> Tuple[str, ...]:
2020-02-18 16:03:52 +00:00
"""
Walk the `path` and list all the languages with word-lists available.
"""
2020-07-24 10:24:23 +00:00
path = _resource_path(path)
(_, _, filenames) = next(os.walk(path))
filenames = [f for f in filenames if f[-4:] == '.txt']
2020-05-26 09:32:20 +00:00
languages = tuple([name[:-4] for name in filenames])
return languages
2020-02-18 16:03:52 +00:00
def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str:
2020-02-18 16:03:52 +00:00
"""
Return a mnemonic string in a given `language` based on `entropy` via the calculated checksum.
Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
2020-02-18 16:03:52 +00:00
"""
if entropy is None:
entropy = randbits(256).to_bytes(32, 'big')
entropy_length = len(entropy) * 8
if entropy_length not in range(128, 257, 32):
raise IndexError(f"`entropy_length` should be in [128, 160, 192,224, 256]. Got {entropy_length}.")
2020-02-18 16:03:52 +00:00
checksum_length = (entropy_length // 32)
checksum = int.from_bytes(SHA256(entropy), 'big') >> 256 - checksum_length
entropy_bits = int.from_bytes(entropy, 'big') << checksum_length
entropy_bits += checksum
entropy_length += checksum_length
mnemonic = []
word_list = _get_word_list(language, words_path)
2020-02-18 16:03:52 +00:00
for i in range(entropy_length // 11 - 1, -1, -1):
index = (entropy_bits >> i * 11) & 2**11 - 1
2020-02-18 17:18:01 +00:00
word = _get_word(word_list=word_list, index=index)
mnemonic.append(word)
2020-02-18 16:03:52 +00:00
return ' '.join(mnemonic)