├── MANIFEST.in ├── .gitignore ├── setup.cfg ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── shamir_mnemonic ├── __init__.py ├── rs1024.py ├── wordlist.py ├── utils.py ├── constants.py ├── cipher.py ├── recovery.py ├── share.py ├── cli.py ├── wordlist.txt └── shamir.py ├── pyproject.toml ├── LICENSE ├── Makefile ├── CHANGELOG.rst ├── README.rst ├── test_shamir.py ├── generate_vectors.py └── vectors.json /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | .mypy_cache 3 | .pytest_cache 4 | __pycache__ 5 | *.egg-info 6 | 7 | /build 8 | /dist 9 | 10 | *.swp 11 | *.py[co] 12 | poetry.lock 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E501, E741, W503 4 | extend-ignore = E203 5 | 6 | [isort] 7 | combine_as_imports = True 8 | force_grid_wrap = 0 9 | include_trailing_comma = True 10 | line_length = 88 11 | multi_line_output = 3 12 | profile = black 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Install poetry 11 | run: pipx install poetry 12 | - name: Setup python for ${{ matrix.python-version }} 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: Install dependencies 17 | run: poetry install --only=dev 18 | - name: Run style check 19 | run: poetry run make style_check 20 | -------------------------------------------------------------------------------- /shamir_mnemonic/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .cipher import decrypt, encrypt 4 | from .shamir import ( 5 | EncryptedMasterSecret, 6 | combine_mnemonics, 7 | decode_mnemonics, 8 | generate_mnemonics, 9 | recover_ems, 10 | split_ems, 11 | ) 12 | from .share import Share 13 | from .utils import MnemonicError 14 | 15 | __all__ = [ 16 | "encrypt", 17 | "decrypt", 18 | "combine_mnemonics", 19 | "decode_mnemonics", 20 | "generate_mnemonics", 21 | "split_ems", 22 | "recover_ems", 23 | "EncryptedMasterSecret", 24 | "MnemonicError", 25 | "Share", 26 | ] 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ ubuntu-latest ] 11 | python-version: ['3.7.13', '3.8.12', '3.9.12', '3.10.4'] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Install poetry 15 | run: pipx install poetry 16 | - name: Setup python for ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: poetry install 22 | - name: Run tests for Python-${{ matrix.python-version }} with ${{ matrix.os }} 23 | run: poetry run pytest 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "shamir-mnemonic" 3 | version = "0.3.1" 4 | description = "SLIP-39 Shamir Mnemonics" 5 | authors = ["Trezor "] 6 | license = "MIT" 7 | readme = [ 8 | "README.rst", 9 | "CHANGELOG.rst", 10 | ] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.6,<4.0" 14 | dataclasses = { version = "*", python = "<=3.6" } 15 | click = { version = ">=7,<9", optional = true } 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | bip32utils = "^0.3.post4" 19 | pytest = "*" 20 | black = ">=20" 21 | isort = "^5" 22 | 23 | [tool.poetry.extras] 24 | cli = ["click"] 25 | 26 | [tool.poetry.scripts] 27 | shamir = "shamir_mnemonic.cli:cli" 28 | 29 | [build-system] 30 | requires = ["poetry-core"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 SatoshiLabs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following 8 | conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 15 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT 17 | OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 18 | OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON=python3 2 | POETRY=poetry 3 | 4 | 5 | build: 6 | $(POETRY) build 7 | 8 | install: 9 | $(POETRY) install 10 | 11 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 12 | 13 | clean-build: ## remove build artifacts 14 | rm -fr build/ 15 | rm -fr dist/ 16 | rm -fr .eggs/ 17 | find . -name '*.egg-info' -exec rm -fr {} + 18 | find . -name '*.egg' -exec rm -f {} + 19 | 20 | clean-pyc: ## remove Python file artifacts 21 | find . -name '*.pyc' -exec rm -f {} + 22 | find . -name '*.pyo' -exec rm -f {} + 23 | find . -name '*~' -exec rm -f {} + 24 | find . -name '__pycache__' -exec rm -fr {} + 25 | 26 | clean-test: ## remove test and coverage artifacts 27 | rm -fr .tox/ 28 | rm -f .coverage 29 | rm -fr htmlcov/ 30 | rm -fr .pytest_cache 31 | 32 | test: 33 | pytest 34 | 35 | style_check: 36 | isort --check-only shamir_mnemonic/ *.py 37 | black shamir_mnemonic/ *.py --check 38 | 39 | style: 40 | black shamir_mnemonic/ *.py 41 | isort shamir_mnemonic/ *.py 42 | 43 | 44 | .PHONY: clean clean-build clean-pyc clean-test test style_check style 45 | -------------------------------------------------------------------------------- /shamir_mnemonic/rs1024.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List 2 | 3 | from .constants import CHECKSUM_LENGTH_WORDS 4 | 5 | 6 | def _polymod(values: Iterable[int]) -> int: 7 | GEN = ( 8 | 0xE0E040, 9 | 0x1C1C080, 10 | 0x3838100, 11 | 0x7070200, 12 | 0xE0E0009, 13 | 0x1C0C2412, 14 | 0x38086C24, 15 | 0x3090FC48, 16 | 0x21B1F890, 17 | 0x3F3F120, 18 | ) 19 | chk = 1 20 | for v in values: 21 | b = chk >> 20 22 | chk = (chk & 0xFFFFF) << 10 ^ v 23 | for i in range(10): 24 | chk ^= GEN[i] if ((b >> i) & 1) else 0 25 | return chk 26 | 27 | 28 | def create_checksum(data: Iterable[int], customization_string: bytes) -> List[int]: 29 | values = list(customization_string) + list(data) + [0] * CHECKSUM_LENGTH_WORDS 30 | polymod = _polymod(values) ^ 1 31 | return [(polymod >> 10 * i) & 1023 for i in reversed(range(CHECKSUM_LENGTH_WORDS))] 32 | 33 | 34 | def verify_checksum(data: Iterable[int], customization_string: bytes) -> bool: 35 | return _polymod(list(customization_string) + list(data)) == 1 36 | -------------------------------------------------------------------------------- /shamir_mnemonic/wordlist.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from typing import Dict, Iterable, List, Sequence, Tuple 3 | 4 | from .constants import RADIX 5 | from .utils import MnemonicError 6 | 7 | 8 | def _load_wordlist() -> Tuple[List[str], Dict[str, int]]: 9 | with open(os.path.join(os.path.dirname(__file__), "wordlist.txt"), "r") as f: 10 | wordlist = [word.strip() for word in f] 11 | 12 | if len(wordlist) != RADIX: 13 | raise ImportError( 14 | f"The wordlist should contain {RADIX} words, but it contains {len(wordlist)} words." 15 | ) 16 | 17 | word_index_map = {word: i for i, word in enumerate(wordlist)} 18 | 19 | return wordlist, word_index_map 20 | 21 | 22 | WORDLIST, WORD_INDEX_MAP = _load_wordlist() 23 | 24 | 25 | def words_from_indices(indices: Iterable[int]) -> Iterable[str]: 26 | return (WORDLIST[i] for i in indices) 27 | 28 | 29 | def mnemonic_from_indices(indices: Iterable[int]) -> str: 30 | return " ".join(words_from_indices(indices)) 31 | 32 | 33 | def mnemonic_to_indices(mnemonic: str) -> Sequence[int]: 34 | try: 35 | return [WORD_INDEX_MAP[word.lower()] for word in mnemonic.split()] 36 | except KeyError as key_error: 37 | raise MnemonicError(f"Invalid mnemonic word {key_error}.") from None 38 | -------------------------------------------------------------------------------- /shamir_mnemonic/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | 4 | class MnemonicError(Exception): 5 | pass 6 | 7 | 8 | def _round_bits(n: int, radix_bits: int) -> int: 9 | """Get the number of `radix_bits`-sized digits required to store a `n`-bit value.""" 10 | return (n + radix_bits - 1) // radix_bits 11 | 12 | 13 | def bits_to_bytes(n: int) -> int: 14 | """Round up bit count to whole bytes.""" 15 | return _round_bits(n, 8) 16 | 17 | 18 | def bits_to_words(n: int) -> int: 19 | """Round up bit count to a multiple of word size.""" 20 | # XXX 21 | # In order to properly functionally decompose the original 1-file implementation, 22 | # function bits_to_words can only exist if it knows the value of RADIX_BITS (which 23 | # informs us of the word size). However, constants.py make use of the function, 24 | # because some constants count word-size of things. 25 | # 26 | # I considered the "least evil" solution to define bits_to_words in utils where it 27 | # logically belongs, and import constants only inside the function. This will work 28 | # as long as calls to bits_to_words only happens *after* RADIX_BITS are declared. 29 | # 30 | # An alternative is to have a private implementation of bits_to_words in constants 31 | from . import constants 32 | 33 | assert hasattr(constants, "RADIX_BITS"), "Declare RADIX_BITS *before* calling this" 34 | 35 | return _round_bits(n, constants.RADIX_BITS) 36 | 37 | 38 | def int_to_indices(value: int, length: int, radix_bits: int) -> Iterable[int]: 39 | """Convert an integer value to indices in big endian order.""" 40 | mask = (1 << radix_bits) - 1 41 | return ((value >> (i * radix_bits)) & mask for i in reversed(range(length))) 42 | -------------------------------------------------------------------------------- /shamir_mnemonic/constants.py: -------------------------------------------------------------------------------- 1 | from .utils import bits_to_words 2 | 3 | RADIX_BITS = 10 4 | """The length of the radix in bits.""" 5 | 6 | RADIX = 2 ** RADIX_BITS 7 | """The number of words in the wordlist.""" 8 | 9 | ID_LENGTH_BITS = 15 10 | """The length of the random identifier in bits.""" 11 | 12 | EXTENDABLE_FLAG_LENGTH_BITS = 1 13 | """The length of the extendable backup flag in bits.""" 14 | 15 | ITERATION_EXP_LENGTH_BITS = 4 16 | """The length of the iteration exponent in bits.""" 17 | 18 | ID_EXP_LENGTH_WORDS = bits_to_words( 19 | ID_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS 20 | ) 21 | """The length of the random identifier, extendable backup flag and iteration exponent in words.""" 22 | 23 | MAX_SHARE_COUNT = 16 24 | """The maximum number of shares that can be created.""" 25 | 26 | CHECKSUM_LENGTH_WORDS = 3 27 | """The length of the RS1024 checksum in words.""" 28 | 29 | DIGEST_LENGTH_BYTES = 4 30 | """The length of the digest of the shared secret in bytes.""" 31 | 32 | CUSTOMIZATION_STRING_ORIG = b"shamir" 33 | """The customization string used in the RS1024 checksum and in the PBKDF2 salt for 34 | shares _without_ the extendable backup flag.""" 35 | 36 | CUSTOMIZATION_STRING_EXTENDABLE = b"shamir_extendable" 37 | """The customization string used in the RS1024 checksum for 38 | shares _with_ the extendable backup flag.""" 39 | 40 | GROUP_PREFIX_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 1 41 | """The length of the prefix of the mnemonic that is common to a share group.""" 42 | 43 | METADATA_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 2 + CHECKSUM_LENGTH_WORDS 44 | """The length of the mnemonic in words without the share value.""" 45 | 46 | MIN_STRENGTH_BITS = 128 47 | """The minimum allowed entropy of the master secret.""" 48 | 49 | MIN_MNEMONIC_LENGTH_WORDS = METADATA_LENGTH_WORDS + bits_to_words(MIN_STRENGTH_BITS) 50 | """The minimum allowed length of the mnemonic in words.""" 51 | 52 | BASE_ITERATION_COUNT = 10000 53 | """The minimum number of iterations to use in PBKDF2.""" 54 | 55 | ROUND_COUNT = 4 56 | """The number of rounds to use in the Feistel cipher.""" 57 | 58 | SECRET_INDEX = 255 59 | """The index of the share containing the shared secret.""" 60 | 61 | DIGEST_INDEX = 254 62 | """The index of the share containing the digest of the shared secret.""" 63 | -------------------------------------------------------------------------------- /shamir_mnemonic/cipher.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from .constants import ( 4 | BASE_ITERATION_COUNT, 5 | CUSTOMIZATION_STRING_ORIG, 6 | ID_LENGTH_BITS, 7 | ROUND_COUNT, 8 | ) 9 | from .utils import bits_to_bytes 10 | 11 | 12 | def _xor(a: bytes, b: bytes) -> bytes: 13 | return bytes(x ^ y for x, y in zip(a, b)) 14 | 15 | 16 | def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes: 17 | """The round function used internally by the Feistel cipher.""" 18 | return hashlib.pbkdf2_hmac( 19 | "sha256", 20 | bytes([i]) + passphrase, 21 | salt + r, 22 | (BASE_ITERATION_COUNT << e) // ROUND_COUNT, 23 | dklen=len(r), 24 | ) 25 | 26 | 27 | def _get_salt(identifier: int, extendable: bool) -> bytes: 28 | if extendable: 29 | return bytes() 30 | identifier_len = bits_to_bytes(ID_LENGTH_BITS) 31 | return CUSTOMIZATION_STRING_ORIG + identifier.to_bytes(identifier_len, "big") 32 | 33 | 34 | def encrypt( 35 | master_secret: bytes, 36 | passphrase: bytes, 37 | iteration_exponent: int, 38 | identifier: int, 39 | extendable: bool, 40 | ) -> bytes: 41 | if len(master_secret) % 2 != 0: 42 | raise ValueError( 43 | "The length of the master secret in bytes must be an even number." 44 | ) 45 | 46 | l = master_secret[: len(master_secret) // 2] 47 | r = master_secret[len(master_secret) // 2 :] 48 | salt = _get_salt(identifier, extendable) 49 | for i in range(ROUND_COUNT): 50 | f = _round_function(i, passphrase, iteration_exponent, salt, r) 51 | l, r = r, _xor(l, f) 52 | return r + l 53 | 54 | 55 | def decrypt( 56 | encrypted_master_secret: bytes, 57 | passphrase: bytes, 58 | iteration_exponent: int, 59 | identifier: int, 60 | extendable: bool, 61 | ) -> bytes: 62 | if len(encrypted_master_secret) % 2 != 0: 63 | raise ValueError( 64 | "The length of the encrypted master secret in bytes must be an even number." 65 | ) 66 | 67 | l = encrypted_master_secret[: len(encrypted_master_secret) // 2] 68 | r = encrypted_master_secret[len(encrypted_master_secret) // 2 :] 69 | salt = _get_salt(identifier, extendable) 70 | for i in reversed(range(ROUND_COUNT)): 71 | f = _round_function(i, passphrase, iteration_exponent, salt, r) 72 | l, r = r, _xor(l, f) 73 | return r + l 74 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. default-role:: code 5 | 6 | All notable changes to this project will be documented in this file. 7 | 8 | The format is based on `Keep a Changelog`_, and this project adheres to 9 | `Semantic Versioning`_. 10 | 11 | `0.3.1`_ - Unreleased 12 | --------------------- 13 | 14 | (no changes yet) 15 | 16 | .. _0.3.1: https://github.com/trezor/python-shamir-mnemonic/compare/v0.3.0...HEAD 17 | 18 | `0.3.0`_ - 2024-05-15 19 | --------------------- 20 | 21 | Incompatible 22 | ~~~~~~~~~~~~ 23 | 24 | - The `shamir` command no longer works out of the box. It is necessary to install the 25 | `cli` extra while installing the package. See README for instructions. 26 | 27 | Added 28 | ~~~~~ 29 | 30 | - Added BIP32 master extended private key to test vectors. 31 | - Added support for extendable backup flag. 32 | 33 | Changed 34 | ~~~~~~~ 35 | 36 | - The `shamir_mnemonic` package now has zero extra dependencies on Python 3.7 and up, 37 | making it more suitable as a dependency of other projects. 38 | - The `shamir` CLI still requires `click`. A new extra `cli` was introduced to handle 39 | this dependency. Use the command `pip install shamir-mnemonic[cli]` to install the CLI 40 | dependencies along with the package. 41 | 42 | Removed 43 | ~~~~~~~ 44 | 45 | - Removed dependency on `attrs`. 46 | 47 | .. _0.3.0: https://github.com/trezor/python-shamir-mnemonic/compare/v0.2.2...v0.3.0 48 | 49 | 50 | `0.2.2`_ - 2021-12-07 51 | --------------------- 52 | 53 | Changed 54 | ~~~~~~~ 55 | 56 | - Relaxed Click constraint so that Click 8.x is allowed 57 | - Applied `black` and `flake8` code style 58 | 59 | .. _0.2.2: https://github.com/trezor/python-shamir-mnemonic/compare/v0.2.1...v0.2.2 60 | 61 | 62 | `0.2.1`_ - 2021-02-03 63 | --------------------- 64 | 65 | .. _0.2.1: https://github.com/trezor/python-shamir-mnemonic/compare/v0.1.0...v0.2.1 66 | 67 | Fixed 68 | ~~~~~ 69 | 70 | - Re-released on the correct commit 71 | 72 | 73 | `0.2.0`_ - 2021-02-03 74 | --------------------- 75 | 76 | .. _0.2.0: https://github.com/trezor/python-shamir-mnemonic/compare/v0.1.0...v0.2.0 77 | 78 | Added 79 | ~~~~~ 80 | 81 | - Introduce `split_ems` and `recover_ems` to separate password-based encryption from the Shamir Secret recovery 82 | - Introduce classes representing a share and group-common parameters 83 | - Introduce `RecoveryState` class that allows reusing the logic of the `shamir recover` command 84 | 85 | Changed 86 | ~~~~~~~ 87 | 88 | - Use `secrets` module instead of `os.urandom` 89 | - Refactor and restructure code into separate modules 90 | 91 | 92 | 0.1.0 - 2019-07-19 93 | ------------------ 94 | 95 | Added 96 | ~~~~~ 97 | 98 | - Initial implementation 99 | 100 | 101 | .. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/ 102 | .. _Semantic Versioning: https://semver.org/spec/v2.0.0.html 103 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-shamir-mnemonic 2 | ====================== 3 | 4 | .. image:: https://badge.fury.io/py/shamir-mnemonic.svg 5 | :target: https://badge.fury.io/py/shamir-mnemonic 6 | 7 | Reference implementation of SLIP-0039: Shamir's Secret-Sharing for Mnemonic 8 | Codes 9 | 10 | Abstract 11 | -------- 12 | 13 | This SLIP describes a standard and interoperable implementation of Shamir's 14 | secret sharing (SSS). SSS splits a secret into unique parts which can be 15 | distributed among participants, and requires a specified minimum number of 16 | parts to be supplied in order to reconstruct the original secret. Knowledge of 17 | fewer than the required number of parts does not leak information about the 18 | secret. 19 | 20 | Specification 21 | ------------- 22 | 23 | See https://github.com/satoshilabs/slips/blob/master/slip-0039.md for full 24 | specification. 25 | 26 | Security 27 | -------- 28 | 29 | This implementation is not using any hardening techniques. Secrets are passed in the 30 | open, and calculations are most likely trivially vulnerable to side-channel attacks. 31 | 32 | The purpose of this code is to verify correctness of other implementations. **It should 33 | not be used for handling sensitive secrets**. 34 | 35 | Installation 36 | ------------ 37 | 38 | With pip from PyPI: 39 | 40 | .. code-block:: console 41 | 42 | $ pip3 install shamir-mnemonic[cli] # for CLI tool 43 | 44 | From local checkout for development: 45 | 46 | Install the [Poetry](https://python-poetry.org/) tool, checkout 47 | `python-shamir-mnemonic` from git, and enter the poetry shell: 48 | 49 | .. code-block:: console 50 | 51 | $ pip3 install poetry 52 | $ git clone https://github.com/trezor/python-shamir-mnemonic 53 | $ cd python-shamir-mnemonic 54 | $ poetry install 55 | $ poetry shell 56 | 57 | CLI usage 58 | --------- 59 | 60 | CLI tool is included as a reference and UX testbed. 61 | 62 | **Warning:** this tool makes no attempt to protect sensitive data! Use at your own risk. 63 | If you need this to recover your wallet seeds, make sure to do it on an air-gapped 64 | computer, preferably running a live system such as Tails. 65 | 66 | When the :code:`shamir_mnemonic` package is installed, you can use the :code:`shamir` 67 | command: 68 | 69 | .. code-block:: console 70 | 71 | $ shamir create 3of5 # create a 3-of-5 set of shares 72 | $ shamir recover # interactively recombine shares to get the master secret 73 | 74 | You can supply your own master secret as a hexadecimal string: 75 | 76 | .. code-block:: console 77 | 78 | $ shamir create 3of5 --master-secret=cb21904441dfd01a392701ecdc25d61c 79 | 80 | You can specify a custom scheme. For example, to create three groups, with 2-of-3, 81 | 2-of-5, and 4-of-5, and require completion of all three groups, use: 82 | 83 | .. code-block:: console 84 | 85 | $ shamir create custom --group-threshold 3 --group 2 3 --group 2 5 --group 4 5 86 | 87 | Use :code:`shamir --help` or :code:`shamir create --help` to see all available options. 88 | 89 | If you want to run the CLI from a local checkout without installing, use the following 90 | command: 91 | 92 | .. code-block:: console 93 | 94 | $ python3 -m shamir_mnemonic.cli 95 | 96 | Test vectors 97 | ------------ 98 | 99 | The test vectors in vectors.json are given as a list of quadruples: 100 | * The first member is a description of the test vector. 101 | * The second member is a list of mnemonics. 102 | * The third member is the master secret which results from combining the mnemonics. 103 | * The fourth member is the BIP32 master extended private key derived from the master secret. 104 | 105 | The master secret is encoded as a string containing two hexadecimal digits for each byte. If 106 | the string is empty, then attempting to combine the given set of mnemonics should result 107 | in error. The passphrase "TREZOR" is used for all valid sets of mnemonics. 108 | -------------------------------------------------------------------------------- /shamir_mnemonic/recovery.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from dataclasses import dataclass, field, replace 3 | from typing import Any, Dict, Optional, Tuple 4 | 5 | from .constants import GROUP_PREFIX_LENGTH_WORDS 6 | from .shamir import ShareGroup, recover_ems 7 | from .share import Share, ShareCommonParameters 8 | from .utils import MnemonicError 9 | 10 | UNDETERMINED = -1 11 | 12 | 13 | class RecoveryState: 14 | """Object for keeping track of running Shamir recovery.""" 15 | 16 | def __init__(self) -> None: 17 | self.last_share: Optional[Share] = None 18 | self.groups: Dict[int, ShareGroup] = defaultdict(ShareGroup) 19 | self.parameters: Optional[ShareCommonParameters] = None 20 | 21 | def group_prefix(self, group_index: int) -> str: 22 | """Return three starting words of a given group.""" 23 | if not self.last_share: 24 | raise RuntimeError("Add at least one share first") 25 | 26 | fake_share = replace(self.last_share, group_index=group_index) 27 | return " ".join(fake_share.words()[:GROUP_PREFIX_LENGTH_WORDS]) 28 | 29 | def group_status(self, group_index: int) -> Tuple[int, int]: 30 | """Return completion status of given group. 31 | 32 | Result consists of the number of shares already entered, and the threshold 33 | for recovering the group. 34 | """ 35 | group = self.groups[group_index] 36 | if not group: 37 | return 0, UNDETERMINED 38 | 39 | return len(group), group.member_threshold() 40 | 41 | def group_is_complete(self, group_index: int) -> bool: 42 | """Check whether a given group is already complete.""" 43 | return self.groups[group_index].is_complete() 44 | 45 | def groups_complete(self) -> int: 46 | """Return the number of groups that are already complete.""" 47 | if self.parameters is None: 48 | return 0 49 | 50 | return sum( 51 | self.group_is_complete(i) for i in range(self.parameters.group_count) 52 | ) 53 | 54 | def is_complete(self) -> bool: 55 | """Check whether the recovery set is complete. 56 | 57 | That is, at least M groups must be complete, where M is the global threshold. 58 | """ 59 | if self.parameters is None: 60 | return False 61 | return self.groups_complete() >= self.parameters.group_threshold 62 | 63 | def matches(self, share: Share) -> bool: 64 | """Check whether the provided share matches the current set, i.e., has the same 65 | common parameters. 66 | """ 67 | if self.parameters is None: 68 | return True 69 | return share.common_parameters() == self.parameters 70 | 71 | def add_share(self, share: Share) -> bool: 72 | """Add a share to the recovery set.""" 73 | if not self.matches(share): 74 | raise MnemonicError( 75 | "This mnemonic is not part of the current set. Please try again." 76 | ) 77 | self.groups[share.group_index].add(share) 78 | self.last_share = share 79 | if self.parameters is None: 80 | self.parameters = share.common_parameters() 81 | return True 82 | 83 | def __contains__(self, obj: Any) -> bool: 84 | if not isinstance(obj, Share): 85 | return False 86 | 87 | if not self.matches(obj): 88 | return False 89 | 90 | if not self.groups: 91 | return False 92 | 93 | return obj in self.groups[obj.group_index] 94 | 95 | def recover(self, passphrase: bytes) -> bytes: 96 | """Recover the master secret, given a passphrase.""" 97 | # Select a subset of shares which meets the thresholds. 98 | reduced_groups: Dict[int, ShareGroup] = {} 99 | for group_index, group in self.groups.items(): 100 | if group.is_complete(): 101 | reduced_groups[group_index] = group.get_minimal_group() 102 | 103 | # some groups have been added so parameters must be known 104 | assert self.parameters is not None 105 | if len(reduced_groups) >= self.parameters.group_threshold: 106 | break 107 | 108 | encrypted_master_secret = recover_ems(reduced_groups) 109 | return encrypted_master_secret.decrypt(passphrase) 110 | -------------------------------------------------------------------------------- /test_shamir.py: -------------------------------------------------------------------------------- 1 | import json 2 | import secrets 3 | from itertools import combinations 4 | from random import shuffle 5 | 6 | import pytest 7 | from bip32utils import BIP32Key 8 | 9 | import shamir_mnemonic as shamir 10 | from shamir_mnemonic import MnemonicError 11 | 12 | MS = b"ABCDEFGHIJKLMNOP" 13 | 14 | 15 | def test_basic_sharing_random(): 16 | secret = secrets.token_bytes(16) 17 | mnemonics = shamir.generate_mnemonics(1, [(3, 5)], secret)[0] 18 | assert shamir.combine_mnemonics(mnemonics[:3]) == shamir.combine_mnemonics( 19 | mnemonics[2:] 20 | ) 21 | 22 | 23 | def test_basic_sharing_fixed(): 24 | mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS)[0] 25 | assert MS == shamir.combine_mnemonics(mnemonics[:3]) 26 | assert MS == shamir.combine_mnemonics(mnemonics[1:4]) 27 | with pytest.raises(MnemonicError): 28 | shamir.combine_mnemonics(mnemonics[1:3]) 29 | 30 | 31 | def test_passphrase(): 32 | mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, b"TREZOR")[0] 33 | assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") 34 | assert MS != shamir.combine_mnemonics(mnemonics[1:4]) 35 | 36 | 37 | def test_non_extendable(): 38 | mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, extendable=False)[0] 39 | assert MS == shamir.combine_mnemonics(mnemonics[1:4]) 40 | 41 | 42 | def test_iteration_exponent(): 43 | mnemonics = shamir.generate_mnemonics( 44 | 1, [(3, 5)], MS, b"TREZOR", iteration_exponent=1 45 | )[0] 46 | assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") 47 | assert MS != shamir.combine_mnemonics(mnemonics[1:4]) 48 | 49 | mnemonics = shamir.generate_mnemonics( 50 | 1, [(3, 5)], MS, b"TREZOR", iteration_exponent=2 51 | )[0] 52 | assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") 53 | assert MS != shamir.combine_mnemonics(mnemonics[1:4]) 54 | 55 | 56 | def test_group_sharing(): 57 | group_threshold = 2 58 | group_sizes = (5, 3, 5, 1) 59 | member_thresholds = (3, 2, 2, 1) 60 | mnemonics = shamir.generate_mnemonics( 61 | group_threshold, list(zip(member_thresholds, group_sizes)), MS 62 | ) 63 | 64 | # Test all valid combinations of mnemonics. 65 | for groups in combinations(zip(mnemonics, member_thresholds), group_threshold): 66 | for group1_subset in combinations(groups[0][0], groups[0][1]): 67 | for group2_subset in combinations(groups[1][0], groups[1][1]): 68 | mnemonic_subset = list(group1_subset + group2_subset) 69 | shuffle(mnemonic_subset) 70 | assert MS == shamir.combine_mnemonics(mnemonic_subset) 71 | 72 | # Minimal sets of mnemonics. 73 | assert MS == shamir.combine_mnemonics( 74 | [mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]] 75 | ) 76 | assert MS == shamir.combine_mnemonics( 77 | [mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]] 78 | ) 79 | 80 | # One complete group and one incomplete group out of two groups required. 81 | with pytest.raises(MnemonicError): 82 | shamir.combine_mnemonics(mnemonics[0][2:] + [mnemonics[1][0]]) 83 | 84 | # One group of two required. 85 | with pytest.raises(MnemonicError): 86 | shamir.combine_mnemonics(mnemonics[0][1:4]) 87 | 88 | 89 | def test_group_sharing_threshold_1(): 90 | group_threshold = 1 91 | group_sizes = (5, 3, 5, 1) 92 | member_thresholds = (3, 2, 2, 1) 93 | mnemonics = shamir.generate_mnemonics( 94 | group_threshold, list(zip(member_thresholds, group_sizes)), MS 95 | ) 96 | 97 | # Test all valid combinations of mnemonics. 98 | for group, member_threshold in zip(mnemonics, member_thresholds): 99 | for group_subset in combinations(group, member_threshold): 100 | mnemonic_subset = list(group_subset) 101 | shuffle(mnemonic_subset) 102 | assert MS == shamir.combine_mnemonics(mnemonic_subset) 103 | 104 | 105 | def test_all_groups_exist(): 106 | for group_threshold in (1, 2, 5): 107 | mnemonics = shamir.generate_mnemonics( 108 | group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], MS 109 | ) 110 | assert len(mnemonics) == 5 111 | assert len(sum(mnemonics, [])) == 19 112 | 113 | 114 | def test_invalid_sharing(): 115 | # Short master secret. 116 | with pytest.raises(ValueError): 117 | shamir.generate_mnemonics(1, [(2, 3)], MS[:14]) 118 | 119 | # Odd length master secret. 120 | with pytest.raises(ValueError): 121 | shamir.generate_mnemonics(1, [(2, 3)], MS + b"X") 122 | 123 | # Group threshold exceeds number of groups. 124 | with pytest.raises(ValueError): 125 | shamir.generate_mnemonics(3, [(3, 5), (2, 5)], MS) 126 | 127 | # Invalid group threshold. 128 | with pytest.raises(ValueError): 129 | shamir.generate_mnemonics(0, [(3, 5), (2, 5)], MS) 130 | 131 | # Member threshold exceeds number of members. 132 | with pytest.raises(ValueError): 133 | shamir.generate_mnemonics(2, [(3, 2), (2, 5)], MS) 134 | 135 | # Invalid member threshold. 136 | with pytest.raises(ValueError): 137 | shamir.generate_mnemonics(2, [(0, 2), (2, 5)], MS) 138 | 139 | # Group with multiple members and member threshold 1. 140 | with pytest.raises(ValueError): 141 | shamir.generate_mnemonics(2, [(3, 5), (1, 3), (2, 5)], MS) 142 | 143 | 144 | def test_vectors(): 145 | with open("vectors.json", "r") as f: 146 | vectors = json.load(f) 147 | for description, mnemonics, secret_hex, xprv in vectors: 148 | if secret_hex: 149 | secret = bytes.fromhex(secret_hex) 150 | assert secret == shamir.combine_mnemonics( 151 | mnemonics, b"TREZOR" 152 | ), 'Incorrect secret for test vector "{}".'.format(description) 153 | assert ( 154 | BIP32Key.fromEntropy(secret).ExtendedKey() == xprv 155 | ), 'Incorrect xprv for test vector "{}".'.format(description) 156 | else: 157 | with pytest.raises(MnemonicError): 158 | shamir.combine_mnemonics(mnemonics) 159 | pytest.fail( 160 | 'Failed to raise exception for test vector "{}".'.format( 161 | description 162 | ) 163 | ) 164 | 165 | 166 | def test_split_ems(): 167 | encrypted_master_secret = shamir.EncryptedMasterSecret.from_master_secret( 168 | MS, b"TREZOR", identifier=42, extendable=True, iteration_exponent=1 169 | ) 170 | grouped_shares = shamir.split_ems(1, [(3, 5)], encrypted_master_secret) 171 | mnemonics = [share.mnemonic() for share in grouped_shares[0]] 172 | 173 | recovered = shamir.combine_mnemonics(mnemonics[:3], b"TREZOR") 174 | assert recovered == MS 175 | 176 | 177 | def test_recover_ems(): 178 | mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, b"TREZOR")[0] 179 | 180 | groups = shamir.decode_mnemonics(mnemonics[:3]) 181 | encrypted_master_secret = shamir.recover_ems(groups) 182 | recovered = encrypted_master_secret.decrypt(b"TREZOR") 183 | assert recovered == MS 184 | -------------------------------------------------------------------------------- /shamir_mnemonic/share.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Iterable, List, NamedTuple 3 | 4 | from . import rs1024, wordlist 5 | from .constants import ( 6 | CUSTOMIZATION_STRING_EXTENDABLE, 7 | CUSTOMIZATION_STRING_ORIG, 8 | EXTENDABLE_FLAG_LENGTH_BITS, 9 | ID_EXP_LENGTH_WORDS, 10 | ITERATION_EXP_LENGTH_BITS, 11 | METADATA_LENGTH_WORDS, 12 | MIN_MNEMONIC_LENGTH_WORDS, 13 | RADIX, 14 | RADIX_BITS, 15 | ) 16 | from .utils import MnemonicError, bits_to_bytes, bits_to_words, int_to_indices 17 | 18 | WordIndex = int 19 | 20 | 21 | def _int_to_word_indices(value: int, length: int) -> List[WordIndex]: 22 | """Converts an integer value to a list of base 1024 indices in big endian order.""" 23 | return list(int_to_indices(value, length, radix_bits=RADIX_BITS)) 24 | 25 | 26 | def _int_from_word_indices(indices: Iterable[WordIndex]) -> int: 27 | """Converts a list of base 1024 indices in big endian order to an integer value.""" 28 | value = 0 29 | for index in indices: 30 | value = value * RADIX + index 31 | return value 32 | 33 | 34 | def _customization_string(extendable: bool) -> bytes: 35 | if extendable: 36 | return CUSTOMIZATION_STRING_EXTENDABLE 37 | else: 38 | return CUSTOMIZATION_STRING_ORIG 39 | 40 | 41 | class ShareCommonParameters(NamedTuple): 42 | """Parameters that are common to all shares of a master secret.""" 43 | 44 | identifier: int 45 | extendable: bool 46 | iteration_exponent: int 47 | group_threshold: int 48 | group_count: int 49 | 50 | 51 | class ShareGroupParameters(NamedTuple): 52 | """Parameters that are common to all shares of a master secret, which belong to the same group.""" 53 | 54 | identifier: int 55 | extendable: bool 56 | iteration_exponent: int 57 | group_index: int 58 | group_threshold: int 59 | group_count: int 60 | member_threshold: int 61 | 62 | 63 | @dataclass(frozen=True) 64 | class Share: 65 | """Represents a single mnemonic share and its metadata""" 66 | 67 | identifier: int 68 | extendable: bool 69 | iteration_exponent: int 70 | group_index: int 71 | group_threshold: int 72 | group_count: int 73 | index: int 74 | member_threshold: int 75 | value: bytes 76 | 77 | def common_parameters(self) -> ShareCommonParameters: 78 | """Return values that uniquely identify a matching set of shares.""" 79 | return ShareCommonParameters( 80 | self.identifier, 81 | self.extendable, 82 | self.iteration_exponent, 83 | self.group_threshold, 84 | self.group_count, 85 | ) 86 | 87 | def group_parameters(self) -> ShareGroupParameters: 88 | """Return values that uniquely identify shares belonging to the same group.""" 89 | return ShareGroupParameters( 90 | self.identifier, 91 | self.extendable, 92 | self.iteration_exponent, 93 | self.group_index, 94 | self.group_threshold, 95 | self.group_count, 96 | self.member_threshold, 97 | ) 98 | 99 | def _encode_id_exp(self) -> List[WordIndex]: 100 | id_exp_int = self.identifier << ( 101 | ITERATION_EXP_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS 102 | ) 103 | id_exp_int += self.extendable << ITERATION_EXP_LENGTH_BITS 104 | id_exp_int += self.iteration_exponent 105 | return _int_to_word_indices(id_exp_int, ID_EXP_LENGTH_WORDS) 106 | 107 | def _encode_share_params(self) -> List[WordIndex]: 108 | # each value is 4 bits, for 20 bits total 109 | val = self.group_index 110 | val <<= 4 111 | val += self.group_threshold - 1 112 | val <<= 4 113 | val += self.group_count - 1 114 | val <<= 4 115 | val += self.index 116 | val <<= 4 117 | val += self.member_threshold - 1 118 | # group parameters are 2 words 119 | return _int_to_word_indices(val, 2) 120 | 121 | def words(self) -> List[str]: 122 | """Convert share data to a share mnemonic.""" 123 | 124 | value_word_count = bits_to_words(len(self.value) * 8) 125 | value_int = int.from_bytes(self.value, "big") 126 | value_data = _int_to_word_indices(value_int, value_word_count) 127 | 128 | share_data = self._encode_id_exp() + self._encode_share_params() + value_data 129 | checksum = rs1024.create_checksum( 130 | share_data, _customization_string(self.extendable) 131 | ) 132 | 133 | return list(wordlist.words_from_indices(share_data + checksum)) 134 | 135 | def mnemonic(self) -> str: 136 | """Convert share data to a share mnemonic.""" 137 | return " ".join(self.words()) 138 | 139 | @classmethod 140 | def from_mnemonic(cls, mnemonic: str) -> "Share": 141 | """Convert a share mnemonic to share data.""" 142 | 143 | mnemonic_data = wordlist.mnemonic_to_indices(mnemonic) 144 | 145 | if len(mnemonic_data) < MIN_MNEMONIC_LENGTH_WORDS: 146 | raise MnemonicError( 147 | "Invalid mnemonic length. The length of each mnemonic " 148 | f"must be at least {MIN_MNEMONIC_LENGTH_WORDS} words." 149 | ) 150 | 151 | padding_len = (RADIX_BITS * (len(mnemonic_data) - METADATA_LENGTH_WORDS)) % 16 152 | if padding_len > 8: 153 | raise MnemonicError("Invalid mnemonic length.") 154 | 155 | id_exp_data = mnemonic_data[:ID_EXP_LENGTH_WORDS] 156 | id_exp_int = _int_from_word_indices(id_exp_data) 157 | 158 | identifier = id_exp_int >> ( 159 | EXTENDABLE_FLAG_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS 160 | ) 161 | extendable = bool((id_exp_int >> ITERATION_EXP_LENGTH_BITS) & 1) 162 | iteration_exponent = id_exp_int & ((1 << ITERATION_EXP_LENGTH_BITS) - 1) 163 | 164 | if not rs1024.verify_checksum(mnemonic_data, _customization_string(extendable)): 165 | raise MnemonicError( 166 | 'Invalid mnemonic checksum for "{} ...".'.format( 167 | " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) 168 | ) 169 | ) 170 | 171 | share_params_data = mnemonic_data[ID_EXP_LENGTH_WORDS : ID_EXP_LENGTH_WORDS + 2] 172 | share_params_int = _int_from_word_indices(share_params_data) 173 | share_params = int_to_indices(share_params_int, 5, 4) 174 | ( 175 | group_index, 176 | group_threshold, 177 | group_count, 178 | index, 179 | member_threshold, 180 | ) = share_params 181 | 182 | if group_count < group_threshold: 183 | raise MnemonicError( 184 | 'Invalid mnemonic "{} ...". Group threshold cannot be greater than group count.'.format( 185 | " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) 186 | ) 187 | ) 188 | 189 | value_data = mnemonic_data[ 190 | ID_EXP_LENGTH_WORDS + 2 : -rs1024.CHECKSUM_LENGTH_WORDS 191 | ] 192 | value_byte_count = bits_to_bytes(RADIX_BITS * len(value_data) - padding_len) 193 | value_int = _int_from_word_indices(value_data) 194 | try: 195 | value = value_int.to_bytes(value_byte_count, "big") 196 | except OverflowError: 197 | raise MnemonicError( 198 | 'Invalid mnemonic padding for "{} ...".'.format( 199 | " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) 200 | ) 201 | ) from None 202 | 203 | return cls( 204 | identifier, 205 | extendable, 206 | iteration_exponent, 207 | group_index, 208 | group_threshold + 1, 209 | group_count + 1, 210 | index, 211 | member_threshold + 1, 212 | value, 213 | ) 214 | -------------------------------------------------------------------------------- /shamir_mnemonic/cli.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import sys 3 | from typing import Sequence, Tuple 4 | 5 | try: 6 | import click 7 | from click import style 8 | 9 | except ImportError: 10 | print("Required dependencies are missing. Install them with:") 11 | print(" pip install shamir_mnemonic[cli]") 12 | sys.exit(1) 13 | 14 | from .recovery import RecoveryState 15 | from .shamir import generate_mnemonics 16 | from .share import Share 17 | from .utils import MnemonicError 18 | 19 | 20 | @click.group() 21 | def cli() -> None: 22 | pass 23 | 24 | 25 | @cli.command() 26 | @click.argument("scheme") 27 | @click.option( 28 | "-g", 29 | "--group", 30 | "groups", 31 | type=(int, int), 32 | metavar="T N", 33 | multiple=True, 34 | help="Add a T-of-N group to the custom scheme.", 35 | ) 36 | @click.option( 37 | "-t", 38 | "--group-threshold", 39 | type=int, 40 | help="Number of groups required for recovery in the custom scheme.", 41 | ) 42 | @click.option( 43 | "-x/-X", 44 | "--extendable/--no-extendable", 45 | is_flag=True, 46 | default=True, 47 | help="Extendable backup flag.", 48 | ) 49 | @click.option("-E", "--exponent", type=int, default=0, help="Iteration exponent.") 50 | @click.option( 51 | "-s", "--strength", type=int, default=128, help="Secret strength in bits." 52 | ) 53 | @click.option( 54 | "-S", "--master-secret", help="Hex-encoded custom master secret.", metavar="HEX" 55 | ) 56 | @click.option("-p", "--passphrase", help="Supply passphrase for recovery.") 57 | def create( 58 | scheme: str, 59 | groups: Sequence[Tuple[int, int]], 60 | group_threshold: int, 61 | extendable: bool, 62 | exponent: int, 63 | master_secret: str, 64 | passphrase: str, 65 | strength: int, 66 | ) -> None: 67 | """Create a Shamir mnemonic set 68 | 69 | SCHEME can be one of: 70 | 71 | \b 72 | single: Create a single recovery seed. 73 | 2of3: Create 3 shares. Require 2 of them to recover the seed. 74 | (You can use any number up to 16. Try 3of5, 4of4, 1of7...) 75 | master: Create 1 master share that can recover the seed by itself, 76 | plus a 3-of-5 group: 5 shares, with 3 required for recovery. 77 | Keep the master for yourself, give the 5 shares to trusted friends. 78 | custom: Specify configuration with -t and -g options. 79 | """ 80 | if passphrase and not master_secret: 81 | raise click.ClickException( 82 | "Only use passphrase in conjunction with an explicit master secret" 83 | ) 84 | 85 | if (groups or group_threshold is not None) and scheme != "custom": 86 | raise click.BadArgumentUsage("To use -g/-t, you must select 'custom' scheme.") 87 | 88 | if scheme == "single": 89 | group_threshold = 1 90 | groups = [(1, 1)] 91 | elif scheme == "master": 92 | group_threshold = 1 93 | groups = [(1, 1), (3, 5)] 94 | elif "of" in scheme: 95 | try: 96 | m, n = map(int, scheme.split("of", maxsplit=1)) 97 | group_threshold = 1 98 | groups = [(m, n)] 99 | except Exception as e: 100 | raise click.BadArgumentUsage(f"Invalid scheme: {scheme}") from e 101 | elif scheme == "custom": 102 | if group_threshold is None: 103 | raise click.BadArgumentUsage( 104 | "Use '-t' to specify the number of groups required for recovery." 105 | ) 106 | if not groups: 107 | raise click.BadArgumentUsage( 108 | "Use '-g T N' to add a T-of-N group to the collection." 109 | ) 110 | else: 111 | raise click.ClickException(f"Unknown scheme: {scheme}") 112 | 113 | if any(m == 1 and n > 1 for m, n in groups): 114 | click.echo("1-of-X groups are not allowed.") 115 | click.echo("Instead, set up a 1-of-1 group and give everyone the same share.") 116 | sys.exit(1) 117 | 118 | if master_secret is not None: 119 | try: 120 | secret_bytes = bytes.fromhex(master_secret) 121 | except Exception as e: 122 | raise click.BadOptionUsage( 123 | "master_secret", "Secret bytes must be hex encoded" 124 | ) from e 125 | else: 126 | secret_bytes = secrets.token_bytes(strength // 8) 127 | 128 | secret_hex = style(secret_bytes.hex(), bold=True) 129 | click.echo(f"Using master secret: {secret_hex}") 130 | 131 | if passphrase: 132 | try: 133 | passphrase_bytes = passphrase.encode("ascii") 134 | except UnicodeDecodeError: 135 | raise click.ClickException("Passphrase must be ASCII only") 136 | else: 137 | passphrase_bytes = b"" 138 | 139 | mnemonics = generate_mnemonics( 140 | group_threshold, groups, secret_bytes, passphrase_bytes, extendable, exponent 141 | ) 142 | 143 | for i, (group, (m, n)) in enumerate(zip(mnemonics, groups)): 144 | group_str = ( 145 | style("Group ", fg="green") 146 | + style(str(i + 1), bold=True) 147 | + style(f" of {len(mnemonics)}", fg="green") 148 | ) 149 | share_str = style(f"{m} of {n}", fg="blue", bold=True) + style( 150 | " shares required:", fg="blue" 151 | ) 152 | click.echo(f"{group_str} - {share_str}") 153 | for g in group: 154 | click.echo(g) 155 | 156 | 157 | FINISHED = style("\u2713", fg="green", bold=True) 158 | EMPTY = style("\u2717", fg="red", bold=True) 159 | INPROGRESS = style("\u25cf", fg="yellow", bold=True) 160 | 161 | 162 | def error(s: str) -> None: 163 | click.echo(style("ERROR: ", fg="red") + s) 164 | 165 | 166 | @cli.command() 167 | @click.option( 168 | "-p", "--passphrase-prompt", is_flag=True, help="Use passphrase after recovering" 169 | ) 170 | def recover(passphrase_prompt: bool) -> None: 171 | recovery_state = RecoveryState() 172 | 173 | def print_group_status(idx: int) -> None: 174 | group_size, group_threshold = recovery_state.group_status(idx) 175 | group_prefix = style(recovery_state.group_prefix(idx), bold=True) 176 | bi = style(str(group_size), bold=True) 177 | if not group_size: 178 | click.echo(f"{EMPTY} {bi} shares from group {group_prefix}") 179 | else: 180 | prefix = FINISHED if group_size >= group_threshold else INPROGRESS 181 | bt = style(str(group_threshold), bold=True) 182 | click.echo(f"{prefix} {bi} of {bt} shares needed from group {group_prefix}") 183 | 184 | def print_status() -> None: 185 | bn = style(str(recovery_state.groups_complete()), bold=True) 186 | assert recovery_state.parameters is not None 187 | bt = style(str(recovery_state.parameters.group_threshold), bold=True) 188 | click.echo() 189 | if recovery_state.parameters.group_count > 1: 190 | click.echo(f"Completed {bn} of {bt} groups needed:") 191 | for i in range(recovery_state.parameters.group_count): 192 | print_group_status(i) 193 | 194 | while not recovery_state.is_complete(): 195 | try: 196 | mnemonic_str = click.prompt("Enter a recovery share") 197 | share = Share.from_mnemonic(mnemonic_str) 198 | if not recovery_state.matches(share): 199 | error("This mnemonic is not part of the current set. Please try again.") 200 | continue 201 | if share in recovery_state: 202 | error("Share already entered.") 203 | continue 204 | 205 | recovery_state.add_share(share) 206 | print_status() 207 | 208 | except click.Abort: 209 | return 210 | except Exception as e: 211 | error(str(e)) 212 | 213 | passphrase_bytes = b"" 214 | if passphrase_prompt: 215 | while True: 216 | passphrase = click.prompt( 217 | "Enter passphrase", hide_input=True, confirmation_prompt=True 218 | ) 219 | try: 220 | passphrase_bytes = passphrase.encode("ascii") 221 | break 222 | except UnicodeDecodeError: 223 | click.echo("Passphrase must be ASCII. Please try again.") 224 | 225 | try: 226 | master_secret = recovery_state.recover(passphrase_bytes) 227 | except MnemonicError as e: 228 | error(str(e)) 229 | click.echo("Recovery failed") 230 | sys.exit(1) 231 | click.secho("SUCCESS!", fg="green", bold=True) 232 | click.echo(f"Your master secret is: {master_secret.hex()}") 233 | 234 | 235 | if __name__ == "__main__": 236 | cli() 237 | -------------------------------------------------------------------------------- /generate_vectors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import random 4 | from dataclasses import astuple 5 | 6 | from bip32utils import BIP32Key 7 | 8 | from shamir_mnemonic import constants, rs1024, shamir, wordlist 9 | from shamir_mnemonic.share import Share 10 | 11 | 12 | def random_bytes(n): 13 | return bytes(random.randrange(256) for _ in range(n)) 14 | 15 | 16 | def output(description, mnemonics, secret): 17 | output.i += 1 18 | xprv = BIP32Key.fromEntropy(secret).ExtendedKey() if secret else "" 19 | output.data.append((f"{output.i}. {description}", mnemonics, secret.hex(), xprv)) 20 | 21 | 22 | def encode_mnemonic(*args): 23 | return Share(*args).mnemonic() 24 | 25 | 26 | def decode_mnemonic(mnemonic): 27 | return list(astuple(Share.from_mnemonic(mnemonic))) 28 | 29 | 30 | def generate_mnemonics_random(group_threshold, groups): 31 | secret = random_bytes(16) 32 | return shamir.generate_mnemonics( 33 | group_threshold, groups, secret, extendable=False, iteration_exponent=0 34 | ) 35 | 36 | 37 | output.i = 0 38 | output.data = [] 39 | 40 | shamir.RANDOM_BYTES = random_bytes 41 | 42 | if __name__ == "__main__": 43 | random.seed(1337) 44 | 45 | for n in [16, 32]: 46 | description = "Valid mnemonic without sharing ({} bits)" 47 | secret = random_bytes(n) 48 | groups = shamir.generate_mnemonics( 49 | 1, [(1, 1)], secret, b"TREZOR", extendable=False, iteration_exponent=0 50 | ) 51 | output(description.format(8 * n), groups[0], secret) 52 | 53 | description = "Mnemonic with invalid checksum ({} bits)" 54 | indices = wordlist.mnemonic_to_indices(groups[0][0]) 55 | indices[-1] ^= 1 56 | mnemonic = wordlist.mnemonic_from_indices(indices) 57 | output(description.format(8 * n), [mnemonic], b"") 58 | 59 | description = "Mnemonic with invalid padding ({} bits)" 60 | overflowing_bits = (8 * n) % constants.RADIX_BITS 61 | if overflowing_bits: 62 | indices = wordlist.mnemonic_to_indices(groups[0][0]) 63 | indices[4] += 1 << overflowing_bits 64 | indices = indices[: -constants.CHECKSUM_LENGTH_WORDS] 65 | mnemonic = wordlist.mnemonic_from_indices( 66 | indices 67 | + rs1024.create_checksum(indices, constants.CUSTOMIZATION_STRING_ORIG) 68 | ) 69 | output(description.format(8 * n), [mnemonic], b"") 70 | 71 | description = "Basic sharing 2-of-3 ({} bits)" 72 | secret = random_bytes(n) 73 | groups = shamir.generate_mnemonics( 74 | 1, [(2, 3)], secret, b"TREZOR", extendable=False, iteration_exponent=2 75 | ) 76 | output(description.format(8 * n), random.sample(groups[0], 2), secret) 77 | output(description.format(8 * n), random.sample(groups[0], 1), b"") 78 | 79 | description = "Mnemonics with different identifiers ({} bits)" 80 | groups = generate_mnemonics_random(1, [(2, 2)]) 81 | data = decode_mnemonic(groups[0][0]) 82 | data[0] ^= 1 # modify the identifier 83 | mnemonics = [encode_mnemonic(*data), groups[0][1]] 84 | output(description.format(8 * n), mnemonics, b"") 85 | 86 | description = "Mnemonics with different iteration exponents ({} bits)" 87 | groups = generate_mnemonics_random(1, [(2, 2)]) 88 | data = decode_mnemonic(groups[0][0]) 89 | data[2] = 3 # change iteration exponent from 0 to 3 90 | mnemonics = [encode_mnemonic(*data), groups[0][1]] 91 | output(description.format(8 * n), mnemonics, b"") 92 | 93 | description = "Mnemonics with mismatching group thresholds ({} bits)" 94 | groups = generate_mnemonics_random(2, [(1, 1), (2, 2)]) 95 | data = decode_mnemonic(groups[0][0]) 96 | data[4] = 1 # change group threshold from 2 to 1 97 | mnemonics = groups[1] + [encode_mnemonic(*data)] 98 | output(description.format(8 * n), mnemonics, b"") 99 | 100 | description = "Mnemonics with mismatching group counts ({} bits)" 101 | groups = generate_mnemonics_random(1, [(2, 2)]) 102 | data = decode_mnemonic(groups[0][0]) 103 | data[5] = 3 # change group count from 1 to 3 104 | mnemonics = [encode_mnemonic(*data), groups[0][1]] 105 | output(description.format(8 * n), mnemonics, b"") 106 | 107 | description = ( 108 | "Mnemonics with greater group threshold than group counts ({} bits)" 109 | ) 110 | groups = generate_mnemonics_random(2, [(2, 2), (1, 1)]) 111 | mnemonics = [] 112 | for group in groups: 113 | for mnemonic in group: 114 | data = decode_mnemonic(mnemonic) 115 | data[5] = 1 # change group count from 2 to 1 116 | mnemonics.append(encode_mnemonic(*data)) 117 | output(description.format(8 * n), mnemonics, b"") 118 | 119 | description = "Mnemonics with duplicate member indices ({} bits)" 120 | groups = generate_mnemonics_random(1, [(2, 3)]) 121 | data = decode_mnemonic(groups[0][0]) 122 | data[6] = 2 # change member index from 0 to 2 123 | mnemonics = [encode_mnemonic(*data), groups[0][2]] 124 | output(description.format(8 * n), mnemonics, b"") 125 | 126 | description = "Mnemonics with mismatching member thresholds ({} bits)" 127 | groups = generate_mnemonics_random(1, [(2, 2)]) 128 | data = decode_mnemonic(groups[0][0]) 129 | data[7] = 1 # change member threshold from 2 to 1 130 | mnemonics = [encode_mnemonic(*data), groups[0][1]] 131 | output(description.format(8 * n), mnemonics, b"") 132 | 133 | description = "Mnemonics giving an invalid digest ({} bits)" 134 | groups = generate_mnemonics_random(1, [(2, 2)]) 135 | data = decode_mnemonic(groups[0][0]) 136 | data[8] = bytes((data[8][0] ^ 1,)) + data[8][1:] # modify the share value 137 | mnemonics = [encode_mnemonic(*data), groups[0][1]] 138 | output(description.format(8 * n), mnemonics, b"") 139 | 140 | # Group sharing. 141 | secret = random_bytes(n) 142 | groups = shamir.generate_mnemonics( 143 | 2, 144 | [(1, 1), (1, 1), (3, 5), (2, 6)], 145 | secret, 146 | b"TREZOR", 147 | extendable=False, 148 | iteration_exponent=0, 149 | ) 150 | 151 | description = "Insufficient number of groups ({} bits, case {})" 152 | output(description.format(8 * n, 1), [groups[1][0]], b"") 153 | output(description.format(8 * n, 2), random.sample(groups[3], 2), b"") 154 | 155 | description = "Threshold number of groups, but insufficient number of members in one group ({} bits)" 156 | output(description.format(8 * n), [groups[3][2], groups[1][0]], b"") 157 | 158 | description = ( 159 | "Threshold number of groups and members in each group ({} bits, case {})" 160 | ) 161 | mnemonics = random.sample(groups[2], 3) + random.sample(groups[3], 2) 162 | random.shuffle(mnemonics) 163 | output(description.format(8 * n, 1), mnemonics, secret) 164 | 165 | mnemonics = groups[1] + random.sample(groups[3], 2) 166 | random.shuffle(mnemonics) 167 | output(description.format(8 * n, 2), mnemonics, secret) 168 | 169 | output(description.format(8 * n, 3), [groups[1][0], groups[0][0]], secret) 170 | 171 | description = "Mnemonic with insufficient length" 172 | secret = random_bytes((shamir.MIN_STRENGTH_BITS // 8) - 2) 173 | identifier = random.randrange(1 << shamir.ID_LENGTH_BITS) 174 | mnemonic = encode_mnemonic(identifier, False, 0, 0, 1, 1, 0, 1, secret) 175 | output(description, [mnemonic], b"") 176 | 177 | description = "Mnemonic with invalid master secret length" 178 | secret = b"\xff" + random_bytes(shamir.MIN_STRENGTH_BITS // 8) 179 | identifier = random.randrange(1 << shamir.ID_LENGTH_BITS) 180 | mnemonic = encode_mnemonic(identifier, False, 0, 0, 1, 1, 0, 1, secret) 181 | output(description, [mnemonic], b"") 182 | 183 | description = "Valid mnemonics which can detect some errors in modular arithmetic" 184 | secret = b"\xado*\xd8\xb5\x9b\xbb\xaa\x016\x9b\x90\x06 \x8d\x9a" 185 | mnemonics = [ 186 | "herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven", 187 | "herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace", 188 | "herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult", 189 | ] 190 | output(description, mnemonics, secret) 191 | 192 | for n in [16, 32]: 193 | description = "Valid extendable mnemonic without sharing ({} bits)" 194 | secret = random_bytes(n) 195 | groups = shamir.generate_mnemonics( 196 | 1, [(1, 1)], secret, b"TREZOR", extendable=True, iteration_exponent=3 197 | ) 198 | output(description.format(8 * n), groups[0], secret) 199 | 200 | description = "Extendable basic sharing 2-of-3 ({} bits)" 201 | secret = random_bytes(n) 202 | groups = shamir.generate_mnemonics( 203 | 1, [(2, 3)], secret, b"TREZOR", extendable=True, iteration_exponent=0 204 | ) 205 | output(description.format(8 * n), random.sample(groups[0], 2), secret) 206 | 207 | with open("vectors.json", "w") as f: 208 | json.dump( 209 | output.data, 210 | f, 211 | sort_keys=True, 212 | indent=2, 213 | separators=(",", ": "), 214 | ensure_ascii=False, 215 | ) 216 | -------------------------------------------------------------------------------- /shamir_mnemonic/wordlist.txt: -------------------------------------------------------------------------------- 1 | academic 2 | acid 3 | acne 4 | acquire 5 | acrobat 6 | activity 7 | actress 8 | adapt 9 | adequate 10 | adjust 11 | admit 12 | adorn 13 | adult 14 | advance 15 | advocate 16 | afraid 17 | again 18 | agency 19 | agree 20 | aide 21 | aircraft 22 | airline 23 | airport 24 | ajar 25 | alarm 26 | album 27 | alcohol 28 | alien 29 | alive 30 | alpha 31 | already 32 | alto 33 | aluminum 34 | always 35 | amazing 36 | ambition 37 | amount 38 | amuse 39 | analysis 40 | anatomy 41 | ancestor 42 | ancient 43 | angel 44 | angry 45 | animal 46 | answer 47 | antenna 48 | anxiety 49 | apart 50 | aquatic 51 | arcade 52 | arena 53 | argue 54 | armed 55 | artist 56 | artwork 57 | aspect 58 | auction 59 | august 60 | aunt 61 | average 62 | aviation 63 | avoid 64 | award 65 | away 66 | axis 67 | axle 68 | beam 69 | beard 70 | beaver 71 | become 72 | bedroom 73 | behavior 74 | being 75 | believe 76 | belong 77 | benefit 78 | best 79 | beyond 80 | bike 81 | biology 82 | birthday 83 | bishop 84 | black 85 | blanket 86 | blessing 87 | blimp 88 | blind 89 | blue 90 | body 91 | bolt 92 | boring 93 | born 94 | both 95 | boundary 96 | bracelet 97 | branch 98 | brave 99 | breathe 100 | briefing 101 | broken 102 | brother 103 | browser 104 | bucket 105 | budget 106 | building 107 | bulb 108 | bulge 109 | bumpy 110 | bundle 111 | burden 112 | burning 113 | busy 114 | buyer 115 | cage 116 | calcium 117 | camera 118 | campus 119 | canyon 120 | capacity 121 | capital 122 | capture 123 | carbon 124 | cards 125 | careful 126 | cargo 127 | carpet 128 | carve 129 | category 130 | cause 131 | ceiling 132 | center 133 | ceramic 134 | champion 135 | change 136 | charity 137 | check 138 | chemical 139 | chest 140 | chew 141 | chubby 142 | cinema 143 | civil 144 | class 145 | clay 146 | cleanup 147 | client 148 | climate 149 | clinic 150 | clock 151 | clogs 152 | closet 153 | clothes 154 | club 155 | cluster 156 | coal 157 | coastal 158 | coding 159 | column 160 | company 161 | corner 162 | costume 163 | counter 164 | course 165 | cover 166 | cowboy 167 | cradle 168 | craft 169 | crazy 170 | credit 171 | cricket 172 | criminal 173 | crisis 174 | critical 175 | crowd 176 | crucial 177 | crunch 178 | crush 179 | crystal 180 | cubic 181 | cultural 182 | curious 183 | curly 184 | custody 185 | cylinder 186 | daisy 187 | damage 188 | dance 189 | darkness 190 | database 191 | daughter 192 | deadline 193 | deal 194 | debris 195 | debut 196 | decent 197 | decision 198 | declare 199 | decorate 200 | decrease 201 | deliver 202 | demand 203 | density 204 | deny 205 | depart 206 | depend 207 | depict 208 | deploy 209 | describe 210 | desert 211 | desire 212 | desktop 213 | destroy 214 | detailed 215 | detect 216 | device 217 | devote 218 | diagnose 219 | dictate 220 | diet 221 | dilemma 222 | diminish 223 | dining 224 | diploma 225 | disaster 226 | discuss 227 | disease 228 | dish 229 | dismiss 230 | display 231 | distance 232 | dive 233 | divorce 234 | document 235 | domain 236 | domestic 237 | dominant 238 | dough 239 | downtown 240 | dragon 241 | dramatic 242 | dream 243 | dress 244 | drift 245 | drink 246 | drove 247 | drug 248 | dryer 249 | duckling 250 | duke 251 | duration 252 | dwarf 253 | dynamic 254 | early 255 | earth 256 | easel 257 | easy 258 | echo 259 | eclipse 260 | ecology 261 | edge 262 | editor 263 | educate 264 | either 265 | elbow 266 | elder 267 | election 268 | elegant 269 | element 270 | elephant 271 | elevator 272 | elite 273 | else 274 | email 275 | emerald 276 | emission 277 | emperor 278 | emphasis 279 | employer 280 | empty 281 | ending 282 | endless 283 | endorse 284 | enemy 285 | energy 286 | enforce 287 | engage 288 | enjoy 289 | enlarge 290 | entrance 291 | envelope 292 | envy 293 | epidemic 294 | episode 295 | equation 296 | equip 297 | eraser 298 | erode 299 | escape 300 | estate 301 | estimate 302 | evaluate 303 | evening 304 | evidence 305 | evil 306 | evoke 307 | exact 308 | example 309 | exceed 310 | exchange 311 | exclude 312 | excuse 313 | execute 314 | exercise 315 | exhaust 316 | exotic 317 | expand 318 | expect 319 | explain 320 | express 321 | extend 322 | extra 323 | eyebrow 324 | facility 325 | fact 326 | failure 327 | faint 328 | fake 329 | false 330 | family 331 | famous 332 | fancy 333 | fangs 334 | fantasy 335 | fatal 336 | fatigue 337 | favorite 338 | fawn 339 | fiber 340 | fiction 341 | filter 342 | finance 343 | findings 344 | finger 345 | firefly 346 | firm 347 | fiscal 348 | fishing 349 | fitness 350 | flame 351 | flash 352 | flavor 353 | flea 354 | flexible 355 | flip 356 | float 357 | floral 358 | fluff 359 | focus 360 | forbid 361 | force 362 | forecast 363 | forget 364 | formal 365 | fortune 366 | forward 367 | founder 368 | fraction 369 | fragment 370 | frequent 371 | freshman 372 | friar 373 | fridge 374 | friendly 375 | frost 376 | froth 377 | frozen 378 | fumes 379 | funding 380 | furl 381 | fused 382 | galaxy 383 | game 384 | garbage 385 | garden 386 | garlic 387 | gasoline 388 | gather 389 | general 390 | genius 391 | genre 392 | genuine 393 | geology 394 | gesture 395 | glad 396 | glance 397 | glasses 398 | glen 399 | glimpse 400 | goat 401 | golden 402 | graduate 403 | grant 404 | grasp 405 | gravity 406 | gray 407 | greatest 408 | grief 409 | grill 410 | grin 411 | grocery 412 | gross 413 | group 414 | grownup 415 | grumpy 416 | guard 417 | guest 418 | guilt 419 | guitar 420 | gums 421 | hairy 422 | hamster 423 | hand 424 | hanger 425 | harvest 426 | have 427 | havoc 428 | hawk 429 | hazard 430 | headset 431 | health 432 | hearing 433 | heat 434 | helpful 435 | herald 436 | herd 437 | hesitate 438 | hobo 439 | holiday 440 | holy 441 | home 442 | hormone 443 | hospital 444 | hour 445 | huge 446 | human 447 | humidity 448 | hunting 449 | husband 450 | hush 451 | husky 452 | hybrid 453 | idea 454 | identify 455 | idle 456 | image 457 | impact 458 | imply 459 | improve 460 | impulse 461 | include 462 | income 463 | increase 464 | index 465 | indicate 466 | industry 467 | infant 468 | inform 469 | inherit 470 | injury 471 | inmate 472 | insect 473 | inside 474 | install 475 | intend 476 | intimate 477 | invasion 478 | involve 479 | iris 480 | island 481 | isolate 482 | item 483 | ivory 484 | jacket 485 | jerky 486 | jewelry 487 | join 488 | judicial 489 | juice 490 | jump 491 | junction 492 | junior 493 | junk 494 | jury 495 | justice 496 | kernel 497 | keyboard 498 | kidney 499 | kind 500 | kitchen 501 | knife 502 | knit 503 | laden 504 | ladle 505 | ladybug 506 | lair 507 | lamp 508 | language 509 | large 510 | laser 511 | laundry 512 | lawsuit 513 | leader 514 | leaf 515 | learn 516 | leaves 517 | lecture 518 | legal 519 | legend 520 | legs 521 | lend 522 | length 523 | level 524 | liberty 525 | library 526 | license 527 | lift 528 | likely 529 | lilac 530 | lily 531 | lips 532 | liquid 533 | listen 534 | literary 535 | living 536 | lizard 537 | loan 538 | lobe 539 | location 540 | losing 541 | loud 542 | loyalty 543 | luck 544 | lunar 545 | lunch 546 | lungs 547 | luxury 548 | lying 549 | lyrics 550 | machine 551 | magazine 552 | maiden 553 | mailman 554 | main 555 | makeup 556 | making 557 | mama 558 | manager 559 | mandate 560 | mansion 561 | manual 562 | marathon 563 | march 564 | market 565 | marvel 566 | mason 567 | material 568 | math 569 | maximum 570 | mayor 571 | meaning 572 | medal 573 | medical 574 | member 575 | memory 576 | mental 577 | merchant 578 | merit 579 | method 580 | metric 581 | midst 582 | mild 583 | military 584 | mineral 585 | minister 586 | miracle 587 | mixed 588 | mixture 589 | mobile 590 | modern 591 | modify 592 | moisture 593 | moment 594 | morning 595 | mortgage 596 | mother 597 | mountain 598 | mouse 599 | move 600 | much 601 | mule 602 | multiple 603 | muscle 604 | museum 605 | music 606 | mustang 607 | nail 608 | national 609 | necklace 610 | negative 611 | nervous 612 | network 613 | news 614 | nuclear 615 | numb 616 | numerous 617 | nylon 618 | oasis 619 | obesity 620 | object 621 | observe 622 | obtain 623 | ocean 624 | often 625 | olympic 626 | omit 627 | oral 628 | orange 629 | orbit 630 | order 631 | ordinary 632 | organize 633 | ounce 634 | oven 635 | overall 636 | owner 637 | paces 638 | pacific 639 | package 640 | paid 641 | painting 642 | pajamas 643 | pancake 644 | pants 645 | papa 646 | paper 647 | parcel 648 | parking 649 | party 650 | patent 651 | patrol 652 | payment 653 | payroll 654 | peaceful 655 | peanut 656 | peasant 657 | pecan 658 | penalty 659 | pencil 660 | percent 661 | perfect 662 | permit 663 | petition 664 | phantom 665 | pharmacy 666 | photo 667 | phrase 668 | physics 669 | pickup 670 | picture 671 | piece 672 | pile 673 | pink 674 | pipeline 675 | pistol 676 | pitch 677 | plains 678 | plan 679 | plastic 680 | platform 681 | playoff 682 | pleasure 683 | plot 684 | plunge 685 | practice 686 | prayer 687 | preach 688 | predator 689 | pregnant 690 | premium 691 | prepare 692 | presence 693 | prevent 694 | priest 695 | primary 696 | priority 697 | prisoner 698 | privacy 699 | prize 700 | problem 701 | process 702 | profile 703 | program 704 | promise 705 | prospect 706 | provide 707 | prune 708 | public 709 | pulse 710 | pumps 711 | punish 712 | puny 713 | pupal 714 | purchase 715 | purple 716 | python 717 | quantity 718 | quarter 719 | quick 720 | quiet 721 | race 722 | racism 723 | radar 724 | railroad 725 | rainbow 726 | raisin 727 | random 728 | ranked 729 | rapids 730 | raspy 731 | reaction 732 | realize 733 | rebound 734 | rebuild 735 | recall 736 | receiver 737 | recover 738 | regret 739 | regular 740 | reject 741 | relate 742 | remember 743 | remind 744 | remove 745 | render 746 | repair 747 | repeat 748 | replace 749 | require 750 | rescue 751 | research 752 | resident 753 | response 754 | result 755 | retailer 756 | retreat 757 | reunion 758 | revenue 759 | review 760 | reward 761 | rhyme 762 | rhythm 763 | rich 764 | rival 765 | river 766 | robin 767 | rocky 768 | romantic 769 | romp 770 | roster 771 | round 772 | royal 773 | ruin 774 | ruler 775 | rumor 776 | sack 777 | safari 778 | salary 779 | salon 780 | salt 781 | satisfy 782 | satoshi 783 | saver 784 | says 785 | scandal 786 | scared 787 | scatter 788 | scene 789 | scholar 790 | science 791 | scout 792 | scramble 793 | screw 794 | script 795 | scroll 796 | seafood 797 | season 798 | secret 799 | security 800 | segment 801 | senior 802 | shadow 803 | shaft 804 | shame 805 | shaped 806 | sharp 807 | shelter 808 | sheriff 809 | short 810 | should 811 | shrimp 812 | sidewalk 813 | silent 814 | silver 815 | similar 816 | simple 817 | single 818 | sister 819 | skin 820 | skunk 821 | slap 822 | slavery 823 | sled 824 | slice 825 | slim 826 | slow 827 | slush 828 | smart 829 | smear 830 | smell 831 | smirk 832 | smith 833 | smoking 834 | smug 835 | snake 836 | snapshot 837 | sniff 838 | society 839 | software 840 | soldier 841 | solution 842 | soul 843 | source 844 | space 845 | spark 846 | speak 847 | species 848 | spelling 849 | spend 850 | spew 851 | spider 852 | spill 853 | spine 854 | spirit 855 | spit 856 | spray 857 | sprinkle 858 | square 859 | squeeze 860 | stadium 861 | staff 862 | standard 863 | starting 864 | station 865 | stay 866 | steady 867 | step 868 | stick 869 | stilt 870 | story 871 | strategy 872 | strike 873 | style 874 | subject 875 | submit 876 | sugar 877 | suitable 878 | sunlight 879 | superior 880 | surface 881 | surprise 882 | survive 883 | sweater 884 | swimming 885 | swing 886 | switch 887 | symbolic 888 | sympathy 889 | syndrome 890 | system 891 | tackle 892 | tactics 893 | tadpole 894 | talent 895 | task 896 | taste 897 | taught 898 | taxi 899 | teacher 900 | teammate 901 | teaspoon 902 | temple 903 | tenant 904 | tendency 905 | tension 906 | terminal 907 | testify 908 | texture 909 | thank 910 | that 911 | theater 912 | theory 913 | therapy 914 | thorn 915 | threaten 916 | thumb 917 | thunder 918 | ticket 919 | tidy 920 | timber 921 | timely 922 | ting 923 | tofu 924 | together 925 | tolerate 926 | total 927 | toxic 928 | tracks 929 | traffic 930 | training 931 | transfer 932 | trash 933 | traveler 934 | treat 935 | trend 936 | trial 937 | tricycle 938 | trip 939 | triumph 940 | trouble 941 | true 942 | trust 943 | twice 944 | twin 945 | type 946 | typical 947 | ugly 948 | ultimate 949 | umbrella 950 | uncover 951 | undergo 952 | unfair 953 | unfold 954 | unhappy 955 | union 956 | universe 957 | unkind 958 | unknown 959 | unusual 960 | unwrap 961 | upgrade 962 | upstairs 963 | username 964 | usher 965 | usual 966 | valid 967 | valuable 968 | vampire 969 | vanish 970 | various 971 | vegan 972 | velvet 973 | venture 974 | verdict 975 | verify 976 | very 977 | veteran 978 | vexed 979 | victim 980 | video 981 | view 982 | vintage 983 | violence 984 | viral 985 | visitor 986 | visual 987 | vitamins 988 | vocal 989 | voice 990 | volume 991 | voter 992 | voting 993 | walnut 994 | warmth 995 | warn 996 | watch 997 | wavy 998 | wealthy 999 | weapon 1000 | webcam 1001 | welcome 1002 | welfare 1003 | western 1004 | width 1005 | wildlife 1006 | window 1007 | wine 1008 | wireless 1009 | wisdom 1010 | withdraw 1011 | wits 1012 | wolf 1013 | woman 1014 | work 1015 | worthy 1016 | wrap 1017 | wrist 1018 | writing 1019 | wrote 1020 | year 1021 | yelp 1022 | yield 1023 | yoga 1024 | zero 1025 | -------------------------------------------------------------------------------- /shamir_mnemonic/shamir.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2018 Andrew R. Kozlik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | # of the Software, and to permit persons to whom the Software is furnished to do 9 | # so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | 22 | import hmac 23 | import secrets 24 | from dataclasses import dataclass 25 | from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Sequence, Set, Tuple 26 | 27 | from . import cipher 28 | from .constants import ( 29 | DIGEST_INDEX, 30 | DIGEST_LENGTH_BYTES, 31 | GROUP_PREFIX_LENGTH_WORDS, 32 | ID_EXP_LENGTH_WORDS, 33 | ID_LENGTH_BITS, 34 | MAX_SHARE_COUNT, 35 | MIN_STRENGTH_BITS, 36 | SECRET_INDEX, 37 | ) 38 | from .share import Share, ShareCommonParameters, ShareGroupParameters 39 | from .utils import MnemonicError, bits_to_bytes 40 | 41 | 42 | class RawShare(NamedTuple): 43 | x: int 44 | data: bytes 45 | 46 | 47 | class ShareGroup: 48 | def __init__(self) -> None: 49 | self.shares: Set[Share] = set() 50 | 51 | def __iter__(self) -> Iterator[Share]: 52 | return iter(self.shares) 53 | 54 | def __len__(self) -> int: 55 | return len(self.shares) 56 | 57 | def __bool__(self) -> bool: 58 | return bool(self.shares) 59 | 60 | def __contains__(self, obj: Any) -> bool: 61 | return obj in self.shares 62 | 63 | def add(self, share: Share) -> None: 64 | if self.shares and self.group_parameters() != share.group_parameters(): 65 | fields = zip( 66 | ShareGroupParameters._fields, 67 | self.group_parameters(), 68 | share.group_parameters(), 69 | ) 70 | mismatch = next(name for name, x, y in fields if x != y) 71 | raise MnemonicError( 72 | f"Invalid set of mnemonics. The {mismatch} parameters don't match." 73 | ) 74 | 75 | self.shares.add(share) 76 | 77 | def to_raw_shares(self) -> List[RawShare]: 78 | return [RawShare(s.index, s.value) for s in self.shares] 79 | 80 | def get_minimal_group(self) -> "ShareGroup": 81 | group = ShareGroup() 82 | group.shares = set( 83 | share for _, share in zip(range(self.member_threshold()), self.shares) 84 | ) 85 | return group 86 | 87 | def common_parameters(self) -> ShareCommonParameters: 88 | return next(iter(self.shares)).common_parameters() 89 | 90 | def group_parameters(self) -> ShareGroupParameters: 91 | return next(iter(self.shares)).group_parameters() 92 | 93 | def member_threshold(self) -> int: 94 | return next(iter(self.shares)).member_threshold 95 | 96 | def is_complete(self) -> bool: 97 | if self.shares: 98 | return len(self.shares) >= self.member_threshold() 99 | else: 100 | return False 101 | 102 | 103 | @dataclass(frozen=True) 104 | class EncryptedMasterSecret: 105 | identifier: int 106 | extendable: bool 107 | iteration_exponent: int 108 | ciphertext: bytes 109 | 110 | @classmethod 111 | def from_master_secret( 112 | cls, 113 | master_secret: bytes, 114 | passphrase: bytes, 115 | identifier: int, 116 | extendable: bool, 117 | iteration_exponent: int, 118 | ) -> "EncryptedMasterSecret": 119 | ciphertext = cipher.encrypt( 120 | master_secret, passphrase, iteration_exponent, identifier, extendable 121 | ) 122 | return EncryptedMasterSecret( 123 | identifier, extendable, iteration_exponent, ciphertext 124 | ) 125 | 126 | def decrypt(self, passphrase: bytes) -> bytes: 127 | return cipher.decrypt( 128 | self.ciphertext, 129 | passphrase, 130 | self.iteration_exponent, 131 | self.identifier, 132 | self.extendable, 133 | ) 134 | 135 | 136 | RANDOM_BYTES = secrets.token_bytes 137 | """Source of random bytes. Can be overriden for deterministic testing.""" 138 | 139 | 140 | def _precompute_exp_log() -> Tuple[List[int], List[int]]: 141 | exp = [0 for i in range(255)] 142 | log = [0 for i in range(256)] 143 | 144 | poly = 1 145 | for i in range(255): 146 | exp[i] = poly 147 | log[poly] = i 148 | 149 | # Multiply poly by the polynomial x + 1. 150 | poly = (poly << 1) ^ poly 151 | 152 | # Reduce poly by x^8 + x^4 + x^3 + x + 1. 153 | if poly & 0x100: 154 | poly ^= 0x11B 155 | 156 | return exp, log 157 | 158 | 159 | EXP_TABLE, LOG_TABLE = _precompute_exp_log() 160 | 161 | 162 | def _interpolate(shares: Sequence[RawShare], x: int) -> bytes: 163 | """ 164 | Returns f(x) given the Shamir shares (x_1, f(x_1)), ... , (x_k, f(x_k)). 165 | :param shares: The Shamir shares. 166 | :type shares: A list of pairs (x_i, y_i), where x_i is an integer and y_i is an array of 167 | bytes representing the evaluations of the polynomials in x_i. 168 | :param int x: The x coordinate of the result. 169 | :return: Evaluations of the polynomials in x. 170 | :rtype: Array of bytes. 171 | """ 172 | 173 | x_coordinates = set(share.x for share in shares) 174 | 175 | if len(x_coordinates) != len(shares): 176 | raise MnemonicError("Invalid set of shares. Share indices must be unique.") 177 | 178 | share_value_lengths = set(len(share.data) for share in shares) 179 | if len(share_value_lengths) != 1: 180 | raise MnemonicError( 181 | "Invalid set of shares. All share values must have the same length." 182 | ) 183 | 184 | if x in x_coordinates: 185 | for share in shares: 186 | if share.x == x: 187 | return share.data 188 | 189 | # Logarithm of the product of (x_i - x) for i = 1, ... , k. 190 | log_prod = sum(LOG_TABLE[share.x ^ x] for share in shares) 191 | 192 | result = bytes(share_value_lengths.pop()) 193 | for share in shares: 194 | # The logarithm of the Lagrange basis polynomial evaluated at x. 195 | log_basis_eval = ( 196 | log_prod 197 | - LOG_TABLE[share.x ^ x] 198 | - sum(LOG_TABLE[share.x ^ other.x] for other in shares) 199 | ) % 255 200 | 201 | result = bytes( 202 | intermediate_sum 203 | ^ ( 204 | EXP_TABLE[(LOG_TABLE[share_val] + log_basis_eval) % 255] 205 | if share_val != 0 206 | else 0 207 | ) 208 | for share_val, intermediate_sum in zip(share.data, result) 209 | ) 210 | 211 | return result 212 | 213 | 214 | def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes: 215 | return hmac.new(random_data, shared_secret, "sha256").digest()[:DIGEST_LENGTH_BYTES] 216 | 217 | 218 | def _split_secret( 219 | threshold: int, share_count: int, shared_secret: bytes 220 | ) -> List[RawShare]: 221 | if threshold < 1: 222 | raise ValueError("The requested threshold must be a positive integer.") 223 | 224 | if threshold > share_count: 225 | raise ValueError( 226 | "The requested threshold must not exceed the number of shares." 227 | ) 228 | 229 | if share_count > MAX_SHARE_COUNT: 230 | raise ValueError( 231 | f"The requested number of shares must not exceed {MAX_SHARE_COUNT}." 232 | ) 233 | 234 | # If the threshold is 1, then the digest of the shared secret is not used. 235 | if threshold == 1: 236 | return [RawShare(i, shared_secret) for i in range(share_count)] 237 | 238 | random_share_count = threshold - 2 239 | 240 | shares = [ 241 | RawShare(i, RANDOM_BYTES(len(shared_secret))) for i in range(random_share_count) 242 | ] 243 | 244 | random_part = RANDOM_BYTES(len(shared_secret) - DIGEST_LENGTH_BYTES) 245 | digest = _create_digest(random_part, shared_secret) 246 | 247 | base_shares = shares + [ 248 | RawShare(DIGEST_INDEX, digest + random_part), 249 | RawShare(SECRET_INDEX, shared_secret), 250 | ] 251 | 252 | for i in range(random_share_count, share_count): 253 | shares.append(RawShare(i, _interpolate(base_shares, i))) 254 | 255 | return shares 256 | 257 | 258 | def _recover_secret(threshold: int, shares: Sequence[RawShare]) -> bytes: 259 | # If the threshold is 1, then the digest of the shared secret is not used. 260 | if threshold == 1: 261 | return next(iter(shares)).data 262 | 263 | shared_secret = _interpolate(shares, SECRET_INDEX) 264 | digest_share = _interpolate(shares, DIGEST_INDEX) 265 | digest = digest_share[:DIGEST_LENGTH_BYTES] 266 | random_part = digest_share[DIGEST_LENGTH_BYTES:] 267 | 268 | if digest != _create_digest(random_part, shared_secret): 269 | raise MnemonicError("Invalid digest of the shared secret.") 270 | 271 | return shared_secret 272 | 273 | 274 | def decode_mnemonics(mnemonics: Iterable[str]) -> Dict[int, ShareGroup]: 275 | common_params: Set[ShareCommonParameters] = set() 276 | groups: Dict[int, ShareGroup] = {} 277 | for mnemonic in mnemonics: 278 | share = Share.from_mnemonic(mnemonic) 279 | common_params.add(share.common_parameters()) 280 | group = groups.setdefault(share.group_index, ShareGroup()) 281 | group.add(share) 282 | 283 | if len(common_params) != 1: 284 | raise MnemonicError( 285 | "Invalid set of mnemonics. " 286 | f"All mnemonics must begin with the same {ID_EXP_LENGTH_WORDS} words, " 287 | "must have the same group threshold and the same group count." 288 | ) 289 | 290 | return groups 291 | 292 | 293 | def split_ems( 294 | group_threshold: int, 295 | groups: Sequence[Tuple[int, int]], 296 | encrypted_master_secret: EncryptedMasterSecret, 297 | ) -> List[List[Share]]: 298 | """ 299 | Split an Encrypted Master Secret into mnemonic shares. 300 | 301 | This function is a counterpart to `recover_ems`, and it is used as a subroutine in 302 | `generate_mnemonics`. The input is an *already encrypted* Master Secret (EMS), so it 303 | is possible to encrypt the Master Secret in advance and perform the splitting later. 304 | 305 | :param group_threshold: The number of groups required to reconstruct the master secret. 306 | :param groups: A list of (member_threshold, member_count) pairs for each group, where member_count 307 | is the number of shares to generate for the group and member_threshold is the number of members required to 308 | reconstruct the group secret. 309 | :param encrypted_master_secret: The encrypted master secret to split. 310 | :return: List of groups of mnemonics. 311 | """ 312 | if len(encrypted_master_secret.ciphertext) * 8 < MIN_STRENGTH_BITS: 313 | raise ValueError( 314 | "The length of the master secret must be " 315 | f"at least {bits_to_bytes(MIN_STRENGTH_BITS)} bytes." 316 | ) 317 | 318 | if group_threshold > len(groups): 319 | raise ValueError( 320 | "The requested group threshold must not exceed the number of groups." 321 | ) 322 | 323 | if any( 324 | member_threshold == 1 and member_count > 1 325 | for member_threshold, member_count in groups 326 | ): 327 | raise ValueError( 328 | "Creating multiple member shares with member threshold 1 is not allowed. " 329 | "Use 1-of-1 member sharing instead." 330 | ) 331 | 332 | group_shares = _split_secret( 333 | group_threshold, len(groups), encrypted_master_secret.ciphertext 334 | ) 335 | 336 | return [ 337 | [ 338 | Share( 339 | encrypted_master_secret.identifier, 340 | encrypted_master_secret.extendable, 341 | encrypted_master_secret.iteration_exponent, 342 | group_index, 343 | group_threshold, 344 | len(groups), 345 | member_index, 346 | member_threshold, 347 | value, 348 | ) 349 | for member_index, value in _split_secret( 350 | member_threshold, member_count, group_secret 351 | ) 352 | ] 353 | for (member_threshold, member_count), (group_index, group_secret) in zip( 354 | groups, group_shares 355 | ) 356 | ] 357 | 358 | 359 | def _random_identifier() -> int: 360 | """Returns a random identifier with the given bit length.""" 361 | identifier = int.from_bytes(RANDOM_BYTES(bits_to_bytes(ID_LENGTH_BITS)), "big") 362 | return identifier & ((1 << ID_LENGTH_BITS) - 1) 363 | 364 | 365 | def generate_mnemonics( 366 | group_threshold: int, 367 | groups: Sequence[Tuple[int, int]], 368 | master_secret: bytes, 369 | passphrase: bytes = b"", 370 | extendable: bool = True, 371 | iteration_exponent: int = 1, 372 | ) -> List[List[str]]: 373 | """ 374 | Split a master secret into mnemonic shares using Shamir's secret sharing scheme. 375 | 376 | The supplied Master Secret is encrypted by the passphrase (empty passphrase is used 377 | if none is provided) and split into a set of mnemonic shares. 378 | 379 | This is the user-friendly method to back up a pre-existing secret with the Shamir 380 | scheme, optionally protected by a passphrase. 381 | 382 | :param group_threshold: The number of groups required to reconstruct the master secret. 383 | :param groups: A list of (member_threshold, member_count) pairs for each group, where member_count 384 | is the number of shares to generate for the group and member_threshold is the number of members required to 385 | reconstruct the group secret. 386 | :param master_secret: The master secret to split. 387 | :param passphrase: The passphrase used to encrypt the master secret. 388 | :param int iteration_exponent: The encryption iteration exponent. 389 | :return: List of groups mnemonics. 390 | """ 391 | if not all(32 <= c <= 126 for c in passphrase): 392 | raise ValueError( 393 | "The passphrase must contain only printable ASCII characters (code points 32-126)." 394 | ) 395 | 396 | identifier = _random_identifier() 397 | encrypted_master_secret = EncryptedMasterSecret.from_master_secret( 398 | master_secret, passphrase, identifier, extendable, iteration_exponent 399 | ) 400 | grouped_shares = split_ems(group_threshold, groups, encrypted_master_secret) 401 | return [[share.mnemonic() for share in group] for group in grouped_shares] 402 | 403 | 404 | def recover_ems(groups: Dict[int, ShareGroup]) -> EncryptedMasterSecret: 405 | """ 406 | Combine shares, recover metadata and the Encrypted Master Secret. 407 | 408 | This function is a counterpart to `split_ems`, and it is used as a subroutine in 409 | `combine_mnemonics`. It returns the EMS itself and data required for its decryption, 410 | except for the passphrase. It is thus possible to defer decryption of the Master 411 | Secret to a later time. 412 | 413 | :param groups: Set of shares classified into groups. 414 | :return: Encrypted Master Secret 415 | """ 416 | 417 | if not groups: 418 | raise MnemonicError("The set of shares is empty.") 419 | 420 | params = next(iter(groups.values())).common_parameters() 421 | 422 | if len(groups) < params.group_threshold: 423 | raise MnemonicError( 424 | "Insufficient number of mnemonic groups. " 425 | f"The required number of groups is {params.group_threshold}." 426 | ) 427 | 428 | if len(groups) != params.group_threshold: 429 | raise MnemonicError( 430 | "Wrong number of mnemonic groups. " 431 | f"Expected {params.group_threshold} groups, " 432 | f"but {len(groups)} were provided." 433 | ) 434 | 435 | for group in groups.values(): 436 | if len(group) != group.member_threshold(): 437 | share_words = next(iter(group)).words() 438 | prefix = " ".join(share_words[:GROUP_PREFIX_LENGTH_WORDS]) 439 | raise MnemonicError( 440 | "Wrong number of mnemonics. " 441 | f'Expected {group.member_threshold()} mnemonics starting with "{prefix} ...", ' 442 | f"but {len(group)} were provided." 443 | ) 444 | 445 | group_shares = [ 446 | RawShare( 447 | group_index, 448 | _recover_secret(group.member_threshold(), group.to_raw_shares()), 449 | ) 450 | for group_index, group in groups.items() 451 | ] 452 | 453 | ciphertext = _recover_secret(params.group_threshold, group_shares) 454 | return EncryptedMasterSecret( 455 | params.identifier, params.extendable, params.iteration_exponent, ciphertext 456 | ) 457 | 458 | 459 | def combine_mnemonics(mnemonics: Iterable[str], passphrase: bytes = b"") -> bytes: 460 | """ 461 | Combine mnemonic shares to obtain the master secret which was previously split 462 | using Shamir's secret sharing scheme. 463 | 464 | This is the user-friendly method to recover a backed-up secret optionally protected 465 | by a passphrase. 466 | 467 | :param mnemonics: List of mnemonics. 468 | :param passphrase: The passphrase used to encrypt the master secret. 469 | :return: The master secret. 470 | """ 471 | 472 | if not mnemonics: 473 | raise MnemonicError("The list of mnemonics is empty.") 474 | 475 | groups = decode_mnemonics(mnemonics) 476 | encrypted_master_secret = recover_ems(groups) 477 | return encrypted_master_secret.decrypt(passphrase) 478 | -------------------------------------------------------------------------------- /vectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "1. Valid mnemonic without sharing (128 bits)", 4 | [ 5 | "duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard" 6 | ], 7 | "bb54aac4b89dc868ba37d9cc21b2cece", 8 | "xprv9s21ZrQH143K4QViKpwKCpS2zVbz8GrZgpEchMDg6KME9HZtjfL7iThE9w5muQA4YPHKN1u5VM1w8D4pvnjxa2BmpGMfXr7hnRrRHZ93awZ" 9 | ], 10 | [ 11 | "2. Mnemonic with invalid checksum (128 bits)", 12 | [ 13 | "duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney" 14 | ], 15 | "", 16 | "" 17 | ], 18 | [ 19 | "3. Mnemonic with invalid padding (128 bits)", 20 | [ 21 | "duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness" 22 | ], 23 | "", 24 | "" 25 | ], 26 | [ 27 | "4. Basic sharing 2-of-3 (128 bits)", 28 | [ 29 | "shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed", 30 | "shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking" 31 | ], 32 | "b43ceb7e57a0ea8766221624d01b0864", 33 | "xprv9s21ZrQH143K2nNuAbfWPHBtfiSCS14XQgb3otW4pX655q58EEZeC8zmjEUwucBu9dPnxdpbZLCn57yx45RBkwJHnwHFjZK4XPJ8SyeYjYg" 34 | ], 35 | [ 36 | "5. Basic sharing 2-of-3 (128 bits)", 37 | [ 38 | "shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed" 39 | ], 40 | "", 41 | "" 42 | ], 43 | [ 44 | "6. Mnemonics with different identifiers (128 bits)", 45 | [ 46 | "adequate smoking academic acid debut wine petition glen cluster slow rhyme slow simple epidemic rumor junk tracks treat olympic tolerate", 47 | "adequate stay academic agency agency formal party ting frequent learn upstairs remember smear leaf damage anatomy ladle market hush corner" 48 | ], 49 | "", 50 | "" 51 | ], 52 | [ 53 | "7. Mnemonics with different iteration exponents (128 bits)", 54 | [ 55 | "peasant leaves academic acid desert exact olympic math alive axle trial tackle drug deny decent smear dominant desert bucket remind", 56 | "peasant leader academic agency cultural blessing percent network envelope medal junk primary human pumps jacket fragment payroll ticket evoke voice" 57 | ], 58 | "", 59 | "" 60 | ], 61 | [ 62 | "8. Mnemonics with mismatching group thresholds (128 bits)", 63 | [ 64 | "liberty category beard echo animal fawn temple briefing math username various wolf aviation fancy visual holy thunder yelp helpful payment", 65 | "liberty category beard email beyond should fancy romp founder easel pink holy hairy romp loyalty material victim owner toxic custody", 66 | "liberty category academic easy being hazard crush diminish oral lizard reaction cluster force dilemma deploy force club veteran expect photo" 67 | ], 68 | "", 69 | "" 70 | ], 71 | [ 72 | "9. Mnemonics with mismatching group counts (128 bits)", 73 | [ 74 | "average senior academic leaf broken teacher expect surface hour capture obesity desire negative dynamic dominant pistol mineral mailman iris aide", 75 | "average senior academic agency curious pants blimp spew clothes slice script dress wrap firm shaft regular slavery negative theater roster" 76 | ], 77 | "", 78 | "" 79 | ], 80 | [ 81 | "10. Mnemonics with greater group threshold than group counts (128 bits)", 82 | [ 83 | "music husband acrobat acid artist finance center either graduate swimming object bike medical clothes station aspect spider maiden bulb welcome", 84 | "music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow", 85 | "music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce" 86 | ], 87 | "", 88 | "" 89 | ], 90 | [ 91 | "11. Mnemonics with duplicate member indices (128 bits)", 92 | [ 93 | "device stay academic always dive coal antenna adult black exceed stadium herald advance soldier busy dryer daughter evaluate minister laser", 94 | "device stay academic always dwarf afraid robin gravity crunch adjust soul branch walnut coastal dream costume scholar mortgage mountain pumps" 95 | ], 96 | "", 97 | "" 98 | ], 99 | [ 100 | "12. Mnemonics with mismatching member thresholds (128 bits)", 101 | [ 102 | "hour painting academic academic device formal evoke guitar random modern justice filter withdraw trouble identify mailman insect general cover oven", 103 | "hour painting academic agency artist again daisy capital beaver fiber much enjoy suitable symbolic identify photo editor romp float echo" 104 | ], 105 | "", 106 | "" 107 | ], 108 | [ 109 | "13. Mnemonics giving an invalid digest (128 bits)", 110 | [ 111 | "guilt walnut academic acid deliver remove equip listen vampire tactics nylon rhythm failure husband fatigue alive blind enemy teaspoon rebound", 112 | "guilt walnut academic agency brave hamster hobo declare herd taste alpha slim criminal mild arcade formal romp branch pink ambition" 113 | ], 114 | "", 115 | "" 116 | ], 117 | [ 118 | "14. Insufficient number of groups (128 bits, case 1)", 119 | [ 120 | "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice" 121 | ], 122 | "", 123 | "" 124 | ], 125 | [ 126 | "15. Insufficient number of groups (128 bits, case 2)", 127 | [ 128 | "eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join", 129 | "eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter" 130 | ], 131 | "", 132 | "" 133 | ], 134 | [ 135 | "16. Threshold number of groups, but insufficient number of members in one group (128 bits)", 136 | [ 137 | "eraser senior decision shadow artist work morning estate greatest pipeline plan ting petition forget hormone flexible general goat admit surface", 138 | "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice" 139 | ], 140 | "", 141 | "" 142 | ], 143 | [ 144 | "17. Threshold number of groups and members in each group (128 bits, case 1)", 145 | [ 146 | "eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter", 147 | "eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup", 148 | "eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces", 149 | "eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate", 150 | "eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing" 151 | ], 152 | "7c3397a292a5941682d7a4ae2d898d11", 153 | "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" 154 | ], 155 | [ 156 | "18. Threshold number of groups and members in each group (128 bits, case 2)", 157 | [ 158 | "eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing", 159 | "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice", 160 | "eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join" 161 | ], 162 | "7c3397a292a5941682d7a4ae2d898d11", 163 | "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" 164 | ], 165 | [ 166 | "19. Threshold number of groups and members in each group (128 bits, case 3)", 167 | [ 168 | "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice", 169 | "eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market" 170 | ], 171 | "7c3397a292a5941682d7a4ae2d898d11", 172 | "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" 173 | ], 174 | [ 175 | "20. Valid mnemonic without sharing (256 bits)", 176 | [ 177 | "theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck" 178 | ], 179 | "989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92", 180 | "xprv9s21ZrQH143K41mrxxMT2FpiheQ9MFNmWVK4tvX2s28KLZAhuXWskJCKVRQprq9TnjzzzEYePpt764csiCxTt22xwGPiRmUjYUUdjaut8RM" 181 | ], 182 | [ 183 | "21. Mnemonic with invalid checksum (256 bits)", 184 | [ 185 | "theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect lunar" 186 | ], 187 | "", 188 | "" 189 | ], 190 | [ 191 | "22. Mnemonic with invalid padding (256 bits)", 192 | [ 193 | "theory painting academic academic campus sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips facility obtain sister" 194 | ], 195 | "", 196 | "" 197 | ], 198 | [ 199 | "23. Basic sharing 2-of-3 (256 bits)", 200 | [ 201 | "humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap", 202 | "humidity disease academic agency actress jacket gross physics cylinder solution fake mortgage benefit public busy prepare sharp friar change work slow purchase ruler again tricycle involve viral wireless mixture anatomy desert cargo upgrade" 203 | ], 204 | "c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae", 205 | "xprv9s21ZrQH143K3a4GRMgK8WnawupkwkP6gyHxRsXnMsYPTPH21fWwNcAytijtfyftqNfiaY8LgQVdBQvHZ9FBvtwdjC7LCYxjYruJFuLzyMQ" 206 | ], 207 | [ 208 | "24. Basic sharing 2-of-3 (256 bits)", 209 | [ 210 | "humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap" 211 | ], 212 | "", 213 | "" 214 | ], 215 | [ 216 | "25. Mnemonics with different identifiers (256 bits)", 217 | [ 218 | "smear husband academic acid deadline scene venture distance dive overall parking bracelet elevator justice echo burning oven chest duke nylon", 219 | "smear isolate academic agency alpha mandate decorate burden recover guard exercise fatal force syndrome fumes thank guest drift dramatic mule" 220 | ], 221 | "", 222 | "" 223 | ], 224 | [ 225 | "26. Mnemonics with different iteration exponents (256 bits)", 226 | [ 227 | "finger trash academic acid average priority dish revenue academic hospital spirit western ocean fact calcium syndrome greatest plan losing dictate", 228 | "finger traffic academic agency building lilac deny paces subject threaten diploma eclipse window unknown health slim piece dragon focus smirk" 229 | ], 230 | "", 231 | "" 232 | ], 233 | [ 234 | "27. Mnemonics with mismatching group thresholds (256 bits)", 235 | [ 236 | "flavor pink beard echo depart forbid retreat become frost helpful juice unwrap reunion credit math burning spine black capital lair", 237 | "flavor pink beard email diet teaspoon freshman identify document rebound cricket prune headset loyalty smell emission skin often square rebound", 238 | "flavor pink academic easy credit cage raisin crazy closet lobe mobile become drink human tactics valuable hand capture sympathy finger" 239 | ], 240 | "", 241 | "" 242 | ], 243 | [ 244 | "28. Mnemonics with mismatching group counts (256 bits)", 245 | [ 246 | "column flea academic leaf debut extra surface slow timber husky lawsuit game behavior husky swimming already paper episode tricycle scroll", 247 | "column flea academic agency blessing garbage party software stadium verify silent umbrella therapy decorate chemical erode dramatic eclipse replace apart" 248 | ], 249 | "", 250 | "" 251 | ], 252 | [ 253 | "29. Mnemonics with greater group threshold than group counts (256 bits)", 254 | [ 255 | "smirk pink acrobat acid auction wireless impulse spine sprinkle fortune clogs elbow guest hush loyalty crush dictate tracks airport talent", 256 | "smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit", 257 | "smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful" 258 | ], 259 | "", 260 | "" 261 | ], 262 | [ 263 | "30. Mnemonics with duplicate member indices (256 bits)", 264 | [ 265 | "fishing recover academic always device craft trend snapshot gums skin downtown watch device sniff hour clock public maximum garlic born", 266 | "fishing recover academic always aircraft view software cradle fangs amazing package plastic evaluate intend penalty epidemic anatomy quarter cage apart" 267 | ], 268 | "", 269 | "" 270 | ], 271 | [ 272 | "31. Mnemonics with mismatching member thresholds (256 bits)", 273 | [ 274 | "evoke garden academic academic answer wolf scandal modern warmth station devote emerald market physics surface formal amazing aquatic gesture medical", 275 | "evoke garden academic agency deal revenue knit reunion decrease magazine flexible company goat repair alarm military facility clogs aide mandate" 276 | ], 277 | "", 278 | "" 279 | ], 280 | [ 281 | "32. Mnemonics giving an invalid digest (256 bits)", 282 | [ 283 | "river deal academic acid average forbid pistol peanut custody bike class aunt hairy merit valid flexible learn ajar very easel", 284 | "river deal academic agency camera amuse lungs numb isolate display smear piece traffic worthy year patrol crush fact fancy emission" 285 | ], 286 | "", 287 | "" 288 | ], 289 | [ 290 | "33. Insufficient number of groups (256 bits, case 1)", 291 | [ 292 | "wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium" 293 | ], 294 | "", 295 | "" 296 | ], 297 | [ 298 | "34. Insufficient number of groups (256 bits, case 2)", 299 | [ 300 | "wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen", 301 | "wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install" 302 | ], 303 | "", 304 | "" 305 | ], 306 | [ 307 | "35. Threshold number of groups, but insufficient number of members in one group (256 bits)", 308 | [ 309 | "wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club", 310 | "wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium" 311 | ], 312 | "", 313 | "" 314 | ], 315 | [ 316 | "36. Threshold number of groups and members in each group (256 bits, case 1)", 317 | [ 318 | "wildlife deal ceramic round aluminum pitch goat racism employer miracle percent math decision episode dramatic editor lily prospect program scene rebuild display sympathy have single mustang junction relate often chemical society wits estate", 319 | "wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen", 320 | "wildlife deal ceramic scatter argue equip vampire together ruin reject literary rival distance aquatic agency teammate rebound false argue miracle stay again blessing peaceful unknown cover beard acid island language debris industry idle", 321 | "wildlife deal ceramic snake agree voter main lecture axis kitchen physics arcade velvet spine idea scroll promise platform firm sharp patrol divorce ancestor fantasy forbid goat ajar believe swimming cowboy symbolic plastic spelling", 322 | "wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club" 323 | ], 324 | "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", 325 | "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" 326 | ], 327 | [ 328 | "37. Threshold number of groups and members in each group (256 bits, case 2)", 329 | [ 330 | "wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen", 331 | "wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium", 332 | "wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install" 333 | ], 334 | "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", 335 | "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" 336 | ], 337 | [ 338 | "38. Threshold number of groups and members in each group (256 bits, case 3)", 339 | [ 340 | "wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium", 341 | "wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs" 342 | ], 343 | "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", 344 | "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" 345 | ], 346 | [ 347 | "39. Mnemonic with insufficient length", 348 | [ 349 | "junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder" 350 | ], 351 | "", 352 | "" 353 | ], 354 | [ 355 | "40. Mnemonic with invalid master secret length", 356 | [ 357 | "fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter" 358 | ], 359 | "", 360 | "" 361 | ], 362 | [ 363 | "41. Valid mnemonics which can detect some errors in modular arithmetic", 364 | [ 365 | "herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven", 366 | "herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace", 367 | "herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult" 368 | ], 369 | "ad6f2ad8b59bbbaa01369b9006208d9a", 370 | "xprv9s21ZrQH143K2R4HJxcG1eUsudvHM753BZ9vaGkpYCoeEhCQx147C5qEcupPHxcXYfdYMwJmsKXrHDhtEwutxTTvFzdDCZVQwHneeQH8ioH" 371 | ], 372 | [ 373 | "42. Valid extendable mnemonic without sharing (128 bits)", 374 | [ 375 | "testify swimming academic academic column loyalty smear include exotic bedroom exotic wrist lobe cover grief golden smart junior estimate learn" 376 | ], 377 | "1679b4516e0ee5954351d288a838f45e", 378 | "xprv9s21ZrQH143K2w6eTpQnB73CU8Qrhg6gN3D66Jr16n5uorwoV7CwxQ5DofRPyok5DyRg4Q3BfHfCgJFk3boNRPPt1vEW1ENj2QckzVLQFXu" 379 | ], 380 | [ 381 | "43. Extendable basic sharing 2-of-3 (128 bits)", 382 | [ 383 | "enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish", 384 | "enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce" 385 | ], 386 | "48b1a4b80b8c209ad42c33672bdaa428", 387 | "xprv9s21ZrQH143K4FS1qQdXYAFVAHiSAnjj21YAKGh2CqUPJ2yQhMmYGT4e5a2tyGLiVsRgTEvajXkxhg92zJ8zmWZas9LguQWz7WZShfJg6RS" 388 | ], 389 | [ 390 | "44. Valid extendable mnemonic without sharing (256 bits)", 391 | [ 392 | "impulse calcium academic academic alcohol sugar lyrics pajamas column facility finance tension extend space birthday rainbow swimming purple syndrome facility trial warn duration snapshot shadow hormone rhyme public spine counter easy hawk album" 393 | ], 394 | "8340611602fe91af634a5f4608377b5235fa2d757c51d720c0c7656249a3035f", 395 | "xprv9s21ZrQH143K2yJ7S8bXMiGqp1fySH8RLeFQKQmqfmmLTRwWmAYkpUcWz6M42oGoFMJRENmvsGQmunWTdizsi8v8fku8gpbVvYSiCYJTF1Y" 396 | ], 397 | [ 398 | "45. Extendable basic sharing 2-of-3 (256 bits)", 399 | [ 400 | "western apart academic always artist resident briefing sugar woman oven coding club ajar merit pecan answer prisoner artist fraction amount desktop mild false necklace muscle photo wealthy alpha category unwrap spew losing making", 401 | "western apart academic acid answer ancient auction flip image penalty oasis beaver multiple thunder problem switch alive heat inherit superior teaspoon explain blanket pencil numb lend punish endless aunt garlic humidity kidney observe" 402 | ], 403 | "8dc652d6d6cd370d8c963141f6d79ba440300f25c467302c1d966bff8f62300d", 404 | "xprv9s21ZrQH143K2eFW2zmu3aayWWd6MJZBG7RebW35fiKcoCZ6jFi6U5gzffB9McDdiKTecUtRqJH9GzueCXiQK1LaQXdgthS8DgWfC8Uu3z7" 405 | ] 406 | ] --------------------------------------------------------------------------------