pax_global_header00006660000000000000000000000064146213546550014525gustar00rootroot0000000000000052 comment=08bbb74bdcb58c918981836cb3ccf401e57ecfb4 python-shamir-mnemonic-0.3.0/000077500000000000000000000000001462135465500161325ustar00rootroot00000000000000python-shamir-mnemonic-0.3.0/.github/000077500000000000000000000000001462135465500174725ustar00rootroot00000000000000python-shamir-mnemonic-0.3.0/.github/workflows/000077500000000000000000000000001462135465500215275ustar00rootroot00000000000000python-shamir-mnemonic-0.3.0/.github/workflows/lint.yml000066400000000000000000000007621462135465500232250ustar00rootroot00000000000000name: lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install poetry run: pipx install poetry - name: Setup python for ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: poetry install --only=dev - name: Run style check run: poetry run make style_check python-shamir-mnemonic-0.3.0/.github/workflows/test.yml000066400000000000000000000012231462135465500232270ustar00rootroot00000000000000name: test on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] python-version: ['3.7.13', '3.8.12', '3.9.12', '3.10.4'] steps: - uses: actions/checkout@v4 - name: Install poetry run: pipx install poetry - name: Setup python for ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: poetry install - name: Run tests for Python-${{ matrix.python-version }} with ${{ matrix.os }} run: poetry run pytest python-shamir-mnemonic-0.3.0/.gitignore000066400000000000000000000001441462135465500201210ustar00rootroot00000000000000/.vscode .mypy_cache .pytest_cache __pycache__ *.egg-info /build /dist *.swp *.py[co] poetry.lock python-shamir-mnemonic-0.3.0/CHANGELOG.rst000066400000000000000000000043051462135465500201550ustar00rootroot00000000000000Changelog ========= .. default-role:: code All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog`_, and this project adheres to `Semantic Versioning`_. `0.3.0`_ - 2024-05-15 --------------------- Incompatible ~~~~~~~~~~~~ - The `shamir` command no longer works out of the box. It is necessary to install the `cli` extra while installing the package. See README for instructions. Added ~~~~~ - Added BIP32 master extended private key to test vectors. - Added support for extendable backup flag. Changed ~~~~~~~ - The `shamir_mnemonic` package now has zero extra dependencies on Python 3.7 and up, making it more suitable as a dependency of other projects. - The `shamir` CLI still requires `click`. A new extra `cli` was introduced to handle this dependency. Use the command `pip install shamir-mnemonic[cli]` to install the CLI dependencies along with the package. Removed ~~~~~~~ - Removed dependency on `attrs`. .. _0.3.0: https://github.com/trezor/python-shamir-mnemonic/compare/v0.2.2...v0.3.0 `0.2.2`_ - 2021-12-07 --------------------- Changed ~~~~~~~ - Relaxed Click constraint so that Click 8.x is allowed - Applied `black` and `flake8` code style .. _0.2.2: https://github.com/trezor/python-shamir-mnemonic/compare/v0.2.1...v0.2.2 `0.2.1`_ - 2021-02-03 --------------------- .. _0.2.1: https://github.com/trezor/python-shamir-mnemonic/compare/v0.1.0...v0.2.1 Fixed ~~~~~ - Re-released on the correct commit `0.2.0`_ - 2021-02-03 --------------------- .. _0.2.0: https://github.com/trezor/python-shamir-mnemonic/compare/v0.1.0...v0.2.0 Added ~~~~~ - Introduce `split_ems` and `recover_ems` to separate password-based encryption from the Shamir Secret recovery - Introduce classes representing a share and group-common parameters - Introduce `RecoveryState` class that allows reusing the logic of the `shamir recover` command Changed ~~~~~~~ - Use `secrets` module instead of `os.urandom` - Refactor and restructure code into separate modules 0.1.0 - 2019-07-19 ------------------ Added ~~~~~ - Initial implementation .. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/ .. _Semantic Versioning: https://semver.org/spec/v2.0.0.html python-shamir-mnemonic-0.3.0/LICENSE000066400000000000000000000020331462135465500171350ustar00rootroot00000000000000Copyright 2019 SatoshiLabs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-shamir-mnemonic-0.3.0/MANIFEST.in000066400000000000000000000000461462135465500176700ustar00rootroot00000000000000include LICENSE include CHANGELOG.rst python-shamir-mnemonic-0.3.0/Makefile000066400000000000000000000016311462135465500175730ustar00rootroot00000000000000PYTHON=python3 POETRY=poetry build: $(POETRY) build install: $(POETRY) install clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts clean-build: ## remove build artifacts rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + clean-pyc: ## remove Python file artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: ## remove test and coverage artifacts rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ rm -fr .pytest_cache test: pytest style_check: isort --check-only shamir_mnemonic/ *.py black shamir_mnemonic/ *.py --check style: black shamir_mnemonic/ *.py isort shamir_mnemonic/ *.py .PHONY: clean clean-build clean-pyc clean-test test style_check style python-shamir-mnemonic-0.3.0/README.rst000066400000000000000000000065761462135465500176370ustar00rootroot00000000000000python-shamir-mnemonic ====================== .. image:: https://badge.fury.io/py/shamir-mnemonic.svg :target: https://badge.fury.io/py/shamir-mnemonic Reference implementation of SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes Abstract -------- This SLIP describes a standard and interoperable implementation of Shamir's secret sharing (SSS). SSS splits a secret into unique parts which can be distributed among participants, and requires a specified minimum number of parts to be supplied in order to reconstruct the original secret. Knowledge of fewer than the required number of parts does not leak information about the secret. Specification ------------- See https://github.com/satoshilabs/slips/blob/master/slip-0039.md for full specification. Security -------- This implementation is not using any hardening techniques. Secrets are passed in the open, and calculations are most likely trivially vulnerable to side-channel attacks. The purpose of this code is to verify correctness of other implementations. **It should not be used for handling sensitive secrets**. Installation ------------ With pip from PyPI: .. code-block:: console $ pip3 install shamir-mnemonic[cli] # for CLI tool From local checkout for development: Install the [Poetry](https://python-poetry.org/) tool, checkout `python-shamir-mnemonic` from git, and enter the poetry shell: .. code-block:: console $ pip3 install poetry $ git clone https://github.com/trezor/python-shamir-mnemonic $ cd python-shamir-mnemonic $ poetry install $ poetry shell CLI usage --------- CLI tool is included as a reference and UX testbed. **Warning:** this tool makes no attempt to protect sensitive data! Use at your own risk. If you need this to recover your wallet seeds, make sure to do it on an air-gapped computer, preferably running a live system such as Tails. When the :code:`shamir_mnemonic` package is installed, you can use the :code:`shamir` command: .. code-block:: console $ shamir create 3of5 # create a 3-of-5 set of shares $ shamir recover # interactively recombine shares to get the master secret You can supply your own master secret as a hexadecimal string: .. code-block:: console $ shamir create 3of5 --master-secret=cb21904441dfd01a392701ecdc25d61c You can specify a custom scheme. For example, to create three groups, with 2-of-3, 2-of-5, and 4-of-5, and require completion of all three groups, use: .. code-block:: console $ shamir create custom --group-threshold 3 --group 2 3 --group 2 5 --group 4 5 Use :code:`shamir --help` or :code:`shamir create --help` to see all available options. If you want to run the CLI from a local checkout without installing, use the following command: .. code-block:: console $ python3 -m shamir_mnemonic.cli Test vectors ------------ The test vectors in vectors.json are given as a list of quadruples: * The first member is a description of the test vector. * The second member is a list of mnemonics. * The third member is the master secret which results from combining the mnemonics. * The fourth member is the BIP32 master extended private key derived from the master secret. The master secret is encoded as a string containing two hexadecimal digits for each byte. If the string is empty, then attempting to combine the given set of mnemonics should result in error. The passphrase "TREZOR" is used for all valid sets of mnemonics. python-shamir-mnemonic-0.3.0/generate_vectors.py000077500000000000000000000213021462135465500220440ustar00rootroot00000000000000#!/usr/bin/env python3 import json import random from dataclasses import astuple from bip32utils import BIP32Key from shamir_mnemonic import constants, rs1024, shamir, wordlist from shamir_mnemonic.share import Share def random_bytes(n): return bytes(random.randrange(256) for _ in range(n)) def output(description, mnemonics, secret): output.i += 1 xprv = BIP32Key.fromEntropy(secret).ExtendedKey() if secret else "" output.data.append((f"{output.i}. {description}", mnemonics, secret.hex(), xprv)) def encode_mnemonic(*args): return Share(*args).mnemonic() def decode_mnemonic(mnemonic): return list(astuple(Share.from_mnemonic(mnemonic))) def generate_mnemonics_random(group_threshold, groups): secret = random_bytes(16) return shamir.generate_mnemonics( group_threshold, groups, secret, extendable=False, iteration_exponent=0 ) output.i = 0 output.data = [] shamir.RANDOM_BYTES = random_bytes if __name__ == "__main__": random.seed(1337) for n in [16, 32]: description = "Valid mnemonic without sharing ({} bits)" secret = random_bytes(n) groups = shamir.generate_mnemonics( 1, [(1, 1)], secret, b"TREZOR", extendable=False, iteration_exponent=0 ) output(description.format(8 * n), groups[0], secret) description = "Mnemonic with invalid checksum ({} bits)" indices = wordlist.mnemonic_to_indices(groups[0][0]) indices[-1] ^= 1 mnemonic = wordlist.mnemonic_from_indices(indices) output(description.format(8 * n), [mnemonic], b"") description = "Mnemonic with invalid padding ({} bits)" overflowing_bits = (8 * n) % constants.RADIX_BITS if overflowing_bits: indices = wordlist.mnemonic_to_indices(groups[0][0]) indices[4] += 1 << overflowing_bits indices = indices[: -constants.CHECKSUM_LENGTH_WORDS] mnemonic = wordlist.mnemonic_from_indices( indices + rs1024.create_checksum(indices, constants.CUSTOMIZATION_STRING_ORIG) ) output(description.format(8 * n), [mnemonic], b"") description = "Basic sharing 2-of-3 ({} bits)" secret = random_bytes(n) groups = shamir.generate_mnemonics( 1, [(2, 3)], secret, b"TREZOR", extendable=False, iteration_exponent=2 ) output(description.format(8 * n), random.sample(groups[0], 2), secret) output(description.format(8 * n), random.sample(groups[0], 1), b"") description = "Mnemonics with different identifiers ({} bits)" groups = generate_mnemonics_random(1, [(2, 2)]) data = decode_mnemonic(groups[0][0]) data[0] ^= 1 # modify the identifier mnemonics = [encode_mnemonic(*data), groups[0][1]] output(description.format(8 * n), mnemonics, b"") description = "Mnemonics with different iteration exponents ({} bits)" groups = generate_mnemonics_random(1, [(2, 2)]) data = decode_mnemonic(groups[0][0]) data[2] = 3 # change iteration exponent from 0 to 3 mnemonics = [encode_mnemonic(*data), groups[0][1]] output(description.format(8 * n), mnemonics, b"") description = "Mnemonics with mismatching group thresholds ({} bits)" groups = generate_mnemonics_random(2, [(1, 1), (2, 2)]) data = decode_mnemonic(groups[0][0]) data[4] = 1 # change group threshold from 2 to 1 mnemonics = groups[1] + [encode_mnemonic(*data)] output(description.format(8 * n), mnemonics, b"") description = "Mnemonics with mismatching group counts ({} bits)" groups = generate_mnemonics_random(1, [(2, 2)]) data = decode_mnemonic(groups[0][0]) data[5] = 3 # change group count from 1 to 3 mnemonics = [encode_mnemonic(*data), groups[0][1]] output(description.format(8 * n), mnemonics, b"") description = ( "Mnemonics with greater group threshold than group counts ({} bits)" ) groups = generate_mnemonics_random(2, [(2, 2), (1, 1)]) mnemonics = [] for group in groups: for mnemonic in group: data = decode_mnemonic(mnemonic) data[5] = 1 # change group count from 2 to 1 mnemonics.append(encode_mnemonic(*data)) output(description.format(8 * n), mnemonics, b"") description = "Mnemonics with duplicate member indices ({} bits)" groups = generate_mnemonics_random(1, [(2, 3)]) data = decode_mnemonic(groups[0][0]) data[6] = 2 # change member index from 0 to 2 mnemonics = [encode_mnemonic(*data), groups[0][2]] output(description.format(8 * n), mnemonics, b"") description = "Mnemonics with mismatching member thresholds ({} bits)" groups = generate_mnemonics_random(1, [(2, 2)]) data = decode_mnemonic(groups[0][0]) data[7] = 1 # change member threshold from 2 to 1 mnemonics = [encode_mnemonic(*data), groups[0][1]] output(description.format(8 * n), mnemonics, b"") description = "Mnemonics giving an invalid digest ({} bits)" groups = generate_mnemonics_random(1, [(2, 2)]) data = decode_mnemonic(groups[0][0]) data[8] = bytes((data[8][0] ^ 1,)) + data[8][1:] # modify the share value mnemonics = [encode_mnemonic(*data), groups[0][1]] output(description.format(8 * n), mnemonics, b"") # Group sharing. secret = random_bytes(n) groups = shamir.generate_mnemonics( 2, [(1, 1), (1, 1), (3, 5), (2, 6)], secret, b"TREZOR", extendable=False, iteration_exponent=0, ) description = "Insufficient number of groups ({} bits, case {})" output(description.format(8 * n, 1), [groups[1][0]], b"") output(description.format(8 * n, 2), random.sample(groups[3], 2), b"") description = "Threshold number of groups, but insufficient number of members in one group ({} bits)" output(description.format(8 * n), [groups[3][2], groups[1][0]], b"") description = ( "Threshold number of groups and members in each group ({} bits, case {})" ) mnemonics = random.sample(groups[2], 3) + random.sample(groups[3], 2) random.shuffle(mnemonics) output(description.format(8 * n, 1), mnemonics, secret) mnemonics = groups[1] + random.sample(groups[3], 2) random.shuffle(mnemonics) output(description.format(8 * n, 2), mnemonics, secret) output(description.format(8 * n, 3), [groups[1][0], groups[0][0]], secret) description = "Mnemonic with insufficient length" secret = random_bytes((shamir.MIN_STRENGTH_BITS // 8) - 2) identifier = random.randrange(1 << shamir.ID_LENGTH_BITS) mnemonic = encode_mnemonic(identifier, False, 0, 0, 1, 1, 0, 1, secret) output(description, [mnemonic], b"") description = "Mnemonic with invalid master secret length" secret = b"\xff" + random_bytes(shamir.MIN_STRENGTH_BITS // 8) identifier = random.randrange(1 << shamir.ID_LENGTH_BITS) mnemonic = encode_mnemonic(identifier, False, 0, 0, 1, 1, 0, 1, secret) output(description, [mnemonic], b"") description = "Valid mnemonics which can detect some errors in modular arithmetic" secret = b"\xado*\xd8\xb5\x9b\xbb\xaa\x016\x9b\x90\x06 \x8d\x9a" mnemonics = [ "herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven", "herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace", "herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult", ] output(description, mnemonics, secret) for n in [16, 32]: description = "Valid extendable mnemonic without sharing ({} bits)" secret = random_bytes(n) groups = shamir.generate_mnemonics( 1, [(1, 1)], secret, b"TREZOR", extendable=True, iteration_exponent=3 ) output(description.format(8 * n), groups[0], secret) description = "Extendable basic sharing 2-of-3 ({} bits)" secret = random_bytes(n) groups = shamir.generate_mnemonics( 1, [(2, 3)], secret, b"TREZOR", extendable=True, iteration_exponent=0 ) output(description.format(8 * n), random.sample(groups[0], 2), secret) with open("vectors.json", "w") as f: json.dump( output.data, f, sort_keys=True, indent=2, separators=(",", ": "), ensure_ascii=False, ) python-shamir-mnemonic-0.3.0/pyproject.toml000066400000000000000000000011761462135465500210530ustar00rootroot00000000000000[tool.poetry] name = "shamir-mnemonic" version = "0.3.0" description = "SLIP-39 Shamir Mnemonics" authors = ["Trezor "] license = "MIT" readme = [ "README.rst", "CHANGELOG.rst", ] [tool.poetry.dependencies] python = ">=3.6,<4.0" dataclasses = { version = "*", python = "<=3.6" } click = { version = ">=7,<9", optional = true } [tool.poetry.group.dev.dependencies] bip32utils = "^0.3.post4" pytest = "*" black = ">=20" isort = "^5" [tool.poetry.extras] cli = ["click"] [tool.poetry.scripts] shamir = "shamir_mnemonic.cli:cli" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" python-shamir-mnemonic-0.3.0/setup.cfg000066400000000000000000000003311462135465500177500ustar00rootroot00000000000000[flake8] max-line-length = 88 ignore = E501, E741, W503 extend-ignore = E203 [isort] combine_as_imports = True force_grid_wrap = 0 include_trailing_comma = True line_length = 88 multi_line_output = 3 profile = black python-shamir-mnemonic-0.3.0/shamir_mnemonic/000077500000000000000000000000001462135465500213025ustar00rootroot00000000000000python-shamir-mnemonic-0.3.0/shamir_mnemonic/__init__.py000066400000000000000000000007421462135465500234160ustar00rootroot00000000000000# flake8: noqa from .cipher import decrypt, encrypt from .shamir import ( EncryptedMasterSecret, combine_mnemonics, decode_mnemonics, generate_mnemonics, recover_ems, split_ems, ) from .share import Share from .utils import MnemonicError __all__ = [ "encrypt", "decrypt", "combine_mnemonics", "decode_mnemonics", "generate_mnemonics", "split_ems", "recover_ems", "EncryptedMasterSecret", "MnemonicError", "Share", ] python-shamir-mnemonic-0.3.0/shamir_mnemonic/cipher.py000066400000000000000000000040641462135465500231320ustar00rootroot00000000000000import hashlib from .constants import ( BASE_ITERATION_COUNT, CUSTOMIZATION_STRING_ORIG, ID_LENGTH_BITS, ROUND_COUNT, ) from .utils import bits_to_bytes def _xor(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes: """The round function used internally by the Feistel cipher.""" return hashlib.pbkdf2_hmac( "sha256", bytes([i]) + passphrase, salt + r, (BASE_ITERATION_COUNT << e) // ROUND_COUNT, dklen=len(r), ) def _get_salt(identifier: int, extendable: bool) -> bytes: if extendable: return bytes() identifier_len = bits_to_bytes(ID_LENGTH_BITS) return CUSTOMIZATION_STRING_ORIG + identifier.to_bytes(identifier_len, "big") def encrypt( master_secret: bytes, passphrase: bytes, iteration_exponent: int, identifier: int, extendable: bool, ) -> bytes: if len(master_secret) % 2 != 0: raise ValueError( "The length of the master secret in bytes must be an even number." ) l = master_secret[: len(master_secret) // 2] r = master_secret[len(master_secret) // 2 :] salt = _get_salt(identifier, extendable) for i in range(ROUND_COUNT): f = _round_function(i, passphrase, iteration_exponent, salt, r) l, r = r, _xor(l, f) return r + l def decrypt( encrypted_master_secret: bytes, passphrase: bytes, iteration_exponent: int, identifier: int, extendable: bool, ) -> bytes: if len(encrypted_master_secret) % 2 != 0: raise ValueError( "The length of the encrypted master secret in bytes must be an even number." ) l = encrypted_master_secret[: len(encrypted_master_secret) // 2] r = encrypted_master_secret[len(encrypted_master_secret) // 2 :] salt = _get_salt(identifier, extendable) for i in reversed(range(ROUND_COUNT)): f = _round_function(i, passphrase, iteration_exponent, salt, r) l, r = r, _xor(l, f) return r + l python-shamir-mnemonic-0.3.0/shamir_mnemonic/cli.py000066400000000000000000000166331462135465500224340ustar00rootroot00000000000000import secrets import sys from typing import Sequence, Tuple try: import click from click import style except ImportError: print("Required dependencies are missing. Install them with:") print(" pip install shamir_mnemonic[cli]") sys.exit(1) from .recovery import RecoveryState from .shamir import generate_mnemonics from .share import Share from .utils import MnemonicError @click.group() def cli() -> None: pass @cli.command() @click.argument("scheme") @click.option( "-g", "--group", "groups", type=(int, int), metavar="T N", multiple=True, help="Add a T-of-N group to the custom scheme.", ) @click.option( "-t", "--group-threshold", type=int, help="Number of groups required for recovery in the custom scheme.", ) @click.option( "-x/-X", "--extendable/--no-extendable", is_flag=True, default=True, help="Extendable backup flag.", ) @click.option("-E", "--exponent", type=int, default=0, help="Iteration exponent.") @click.option( "-s", "--strength", type=int, default=128, help="Secret strength in bits." ) @click.option( "-S", "--master-secret", help="Hex-encoded custom master secret.", metavar="HEX" ) @click.option("-p", "--passphrase", help="Supply passphrase for recovery.") def create( scheme: str, groups: Sequence[Tuple[int, int]], group_threshold: int, extendable: bool, exponent: int, master_secret: str, passphrase: str, strength: int, ) -> None: """Create a Shamir mnemonic set SCHEME can be one of: \b single: Create a single recovery seed. 2of3: Create 3 shares. Require 2 of them to recover the seed. (You can use any number up to 16. Try 3of5, 4of4, 1of7...) master: Create 1 master share that can recover the seed by itself, plus a 3-of-5 group: 5 shares, with 3 required for recovery. Keep the master for yourself, give the 5 shares to trusted friends. custom: Specify configuration with -t and -g options. """ if passphrase and not master_secret: raise click.ClickException( "Only use passphrase in conjunction with an explicit master secret" ) if (groups or group_threshold is not None) and scheme != "custom": raise click.BadArgumentUsage("To use -g/-t, you must select 'custom' scheme.") if scheme == "single": group_threshold = 1 groups = [(1, 1)] elif scheme == "master": group_threshold = 1 groups = [(1, 1), (3, 5)] elif "of" in scheme: try: m, n = map(int, scheme.split("of", maxsplit=1)) group_threshold = 1 groups = [(m, n)] except Exception as e: raise click.BadArgumentUsage(f"Invalid scheme: {scheme}") from e elif scheme == "custom": if group_threshold is None: raise click.BadArgumentUsage( "Use '-t' to specify the number of groups required for recovery." ) if not groups: raise click.BadArgumentUsage( "Use '-g T N' to add a T-of-N group to the collection." ) else: raise click.ClickException(f"Unknown scheme: {scheme}") if any(m == 1 and n > 1 for m, n in groups): click.echo("1-of-X groups are not allowed.") click.echo("Instead, set up a 1-of-1 group and give everyone the same share.") sys.exit(1) if master_secret is not None: try: secret_bytes = bytes.fromhex(master_secret) except Exception as e: raise click.BadOptionUsage( "master_secret", "Secret bytes must be hex encoded" ) from e else: secret_bytes = secrets.token_bytes(strength // 8) secret_hex = style(secret_bytes.hex(), bold=True) click.echo(f"Using master secret: {secret_hex}") if passphrase: try: passphrase_bytes = passphrase.encode("ascii") except UnicodeDecodeError: raise click.ClickException("Passphrase must be ASCII only") else: passphrase_bytes = b"" mnemonics = generate_mnemonics( group_threshold, groups, secret_bytes, passphrase_bytes, extendable, exponent ) for i, (group, (m, n)) in enumerate(zip(mnemonics, groups)): group_str = ( style("Group ", fg="green") + style(str(i + 1), bold=True) + style(f" of {len(mnemonics)}", fg="green") ) share_str = style(f"{m} of {n}", fg="blue", bold=True) + style( " shares required:", fg="blue" ) click.echo(f"{group_str} - {share_str}") for g in group: click.echo(g) FINISHED = style("\u2713", fg="green", bold=True) EMPTY = style("\u2717", fg="red", bold=True) INPROGRESS = style("\u25cf", fg="yellow", bold=True) def error(s: str) -> None: click.echo(style("ERROR: ", fg="red") + s) @cli.command() @click.option( "-p", "--passphrase-prompt", is_flag=True, help="Use passphrase after recovering" ) def recover(passphrase_prompt: bool) -> None: recovery_state = RecoveryState() def print_group_status(idx: int) -> None: group_size, group_threshold = recovery_state.group_status(idx) group_prefix = style(recovery_state.group_prefix(idx), bold=True) bi = style(str(group_size), bold=True) if not group_size: click.echo(f"{EMPTY} {bi} shares from group {group_prefix}") else: prefix = FINISHED if group_size >= group_threshold else INPROGRESS bt = style(str(group_threshold), bold=True) click.echo(f"{prefix} {bi} of {bt} shares needed from group {group_prefix}") def print_status() -> None: bn = style(str(recovery_state.groups_complete()), bold=True) assert recovery_state.parameters is not None bt = style(str(recovery_state.parameters.group_threshold), bold=True) click.echo() if recovery_state.parameters.group_count > 1: click.echo(f"Completed {bn} of {bt} groups needed:") for i in range(recovery_state.parameters.group_count): print_group_status(i) while not recovery_state.is_complete(): try: mnemonic_str = click.prompt("Enter a recovery share") share = Share.from_mnemonic(mnemonic_str) if not recovery_state.matches(share): error("This mnemonic is not part of the current set. Please try again.") continue if share in recovery_state: error("Share already entered.") continue recovery_state.add_share(share) print_status() except click.Abort: return except Exception as e: error(str(e)) passphrase_bytes = b"" if passphrase_prompt: while True: passphrase = click.prompt( "Enter passphrase", hide_input=True, confirmation_prompt=True ) try: passphrase_bytes = passphrase.encode("ascii") break except UnicodeDecodeError: click.echo("Passphrase must be ASCII. Please try again.") try: master_secret = recovery_state.recover(passphrase_bytes) except MnemonicError as e: error(str(e)) click.echo("Recovery failed") sys.exit(1) click.secho("SUCCESS!", fg="green", bold=True) click.echo(f"Your master secret is: {master_secret.hex()}") if __name__ == "__main__": cli() python-shamir-mnemonic-0.3.0/shamir_mnemonic/constants.py000066400000000000000000000037461462135465500237020ustar00rootroot00000000000000from .utils import bits_to_words RADIX_BITS = 10 """The length of the radix in bits.""" RADIX = 2 ** RADIX_BITS """The number of words in the wordlist.""" ID_LENGTH_BITS = 15 """The length of the random identifier in bits.""" EXTENDABLE_FLAG_LENGTH_BITS = 1 """The length of the extendable backup flag in bits.""" ITERATION_EXP_LENGTH_BITS = 4 """The length of the iteration exponent in bits.""" ID_EXP_LENGTH_WORDS = bits_to_words( ID_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS ) """The length of the random identifier, extendable backup flag and iteration exponent in words.""" MAX_SHARE_COUNT = 16 """The maximum number of shares that can be created.""" CHECKSUM_LENGTH_WORDS = 3 """The length of the RS1024 checksum in words.""" DIGEST_LENGTH_BYTES = 4 """The length of the digest of the shared secret in bytes.""" CUSTOMIZATION_STRING_ORIG = b"shamir" """The customization string used in the RS1024 checksum and in the PBKDF2 salt for shares _without_ the extendable backup flag.""" CUSTOMIZATION_STRING_EXTENDABLE = b"shamir_extendable" """The customization string used in the RS1024 checksum for shares _with_ the extendable backup flag.""" GROUP_PREFIX_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 1 """The length of the prefix of the mnemonic that is common to a share group.""" METADATA_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 2 + CHECKSUM_LENGTH_WORDS """The length of the mnemonic in words without the share value.""" MIN_STRENGTH_BITS = 128 """The minimum allowed entropy of the master secret.""" MIN_MNEMONIC_LENGTH_WORDS = METADATA_LENGTH_WORDS + bits_to_words(MIN_STRENGTH_BITS) """The minimum allowed length of the mnemonic in words.""" BASE_ITERATION_COUNT = 10000 """The minimum number of iterations to use in PBKDF2.""" ROUND_COUNT = 4 """The number of rounds to use in the Feistel cipher.""" SECRET_INDEX = 255 """The index of the share containing the shared secret.""" DIGEST_INDEX = 254 """The index of the share containing the digest of the shared secret.""" python-shamir-mnemonic-0.3.0/shamir_mnemonic/recovery.py000066400000000000000000000075011462135465500235150ustar00rootroot00000000000000from collections import defaultdict from dataclasses import dataclass, field, replace from typing import Any, Dict, Optional, Tuple from .constants import GROUP_PREFIX_LENGTH_WORDS from .shamir import ShareGroup, recover_ems from .share import Share, ShareCommonParameters from .utils import MnemonicError UNDETERMINED = -1 class RecoveryState: """Object for keeping track of running Shamir recovery.""" def __init__(self) -> None: self.last_share: Optional[Share] = None self.groups: Dict[int, ShareGroup] = defaultdict(ShareGroup) self.parameters: Optional[ShareCommonParameters] = None def group_prefix(self, group_index: int) -> str: """Return three starting words of a given group.""" if not self.last_share: raise RuntimeError("Add at least one share first") fake_share = replace(self.last_share, group_index=group_index) return " ".join(fake_share.words()[:GROUP_PREFIX_LENGTH_WORDS]) def group_status(self, group_index: int) -> Tuple[int, int]: """Return completion status of given group. Result consists of the number of shares already entered, and the threshold for recovering the group. """ group = self.groups[group_index] if not group: return 0, UNDETERMINED return len(group), group.member_threshold() def group_is_complete(self, group_index: int) -> bool: """Check whether a given group is already complete.""" return self.groups[group_index].is_complete() def groups_complete(self) -> int: """Return the number of groups that are already complete.""" if self.parameters is None: return 0 return sum( self.group_is_complete(i) for i in range(self.parameters.group_count) ) def is_complete(self) -> bool: """Check whether the recovery set is complete. That is, at least M groups must be complete, where M is the global threshold. """ if self.parameters is None: return False return self.groups_complete() >= self.parameters.group_threshold def matches(self, share: Share) -> bool: """Check whether the provided share matches the current set, i.e., has the same common parameters. """ if self.parameters is None: return True return share.common_parameters() == self.parameters def add_share(self, share: Share) -> bool: """Add a share to the recovery set.""" if not self.matches(share): raise MnemonicError( "This mnemonic is not part of the current set. Please try again." ) self.groups[share.group_index].add(share) self.last_share = share if self.parameters is None: self.parameters = share.common_parameters() return True def __contains__(self, obj: Any) -> bool: if not isinstance(obj, Share): return False if not self.matches(obj): return False if not self.groups: return False return obj in self.groups[obj.group_index] def recover(self, passphrase: bytes) -> bytes: """Recover the master secret, given a passphrase.""" # Select a subset of shares which meets the thresholds. reduced_groups: Dict[int, ShareGroup] = {} for group_index, group in self.groups.items(): if group.is_complete(): reduced_groups[group_index] = group.get_minimal_group() # some groups have been added so parameters must be known assert self.parameters is not None if len(reduced_groups) >= self.parameters.group_threshold: break encrypted_master_secret = recover_ems(reduced_groups) return encrypted_master_secret.decrypt(passphrase) python-shamir-mnemonic-0.3.0/shamir_mnemonic/rs1024.py000066400000000000000000000017051462135465500226120ustar00rootroot00000000000000from typing import Iterable, List from .constants import CHECKSUM_LENGTH_WORDS def _polymod(values: Iterable[int]) -> int: GEN = ( 0xE0E040, 0x1C1C080, 0x3838100, 0x7070200, 0xE0E0009, 0x1C0C2412, 0x38086C24, 0x3090FC48, 0x21B1F890, 0x3F3F120, ) chk = 1 for v in values: b = chk >> 20 chk = (chk & 0xFFFFF) << 10 ^ v for i in range(10): chk ^= GEN[i] if ((b >> i) & 1) else 0 return chk def create_checksum(data: Iterable[int], customization_string: bytes) -> List[int]: values = list(customization_string) + list(data) + [0] * CHECKSUM_LENGTH_WORDS polymod = _polymod(values) ^ 1 return [(polymod >> 10 * i) & 1023 for i in reversed(range(CHECKSUM_LENGTH_WORDS))] def verify_checksum(data: Iterable[int], customization_string: bytes) -> bool: return _polymod(list(customization_string) + list(data)) == 1 python-shamir-mnemonic-0.3.0/shamir_mnemonic/shamir.py000066400000000000000000000377671462135465500231630ustar00rootroot00000000000000# # Copyright (c) 2018 Andrew R. Kozlik # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies # of the Software, and to permit persons to whom the Software is furnished to do # so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import hmac import secrets from dataclasses import dataclass from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Sequence, Set, Tuple from . import cipher from .constants import ( DIGEST_INDEX, DIGEST_LENGTH_BYTES, GROUP_PREFIX_LENGTH_WORDS, ID_EXP_LENGTH_WORDS, ID_LENGTH_BITS, MAX_SHARE_COUNT, MIN_STRENGTH_BITS, SECRET_INDEX, ) from .share import Share, ShareCommonParameters, ShareGroupParameters from .utils import MnemonicError, bits_to_bytes class RawShare(NamedTuple): x: int data: bytes class ShareGroup: def __init__(self) -> None: self.shares: Set[Share] = set() def __iter__(self) -> Iterator[Share]: return iter(self.shares) def __len__(self) -> int: return len(self.shares) def __bool__(self) -> bool: return bool(self.shares) def __contains__(self, obj: Any) -> bool: return obj in self.shares def add(self, share: Share) -> None: if self.shares and self.group_parameters() != share.group_parameters(): fields = zip( ShareGroupParameters._fields, self.group_parameters(), share.group_parameters(), ) mismatch = next(name for name, x, y in fields if x != y) raise MnemonicError( f"Invalid set of mnemonics. The {mismatch} parameters don't match." ) self.shares.add(share) def to_raw_shares(self) -> List[RawShare]: return [RawShare(s.index, s.value) for s in self.shares] def get_minimal_group(self) -> "ShareGroup": group = ShareGroup() group.shares = set( share for _, share in zip(range(self.member_threshold()), self.shares) ) return group def common_parameters(self) -> ShareCommonParameters: return next(iter(self.shares)).common_parameters() def group_parameters(self) -> ShareGroupParameters: return next(iter(self.shares)).group_parameters() def member_threshold(self) -> int: return next(iter(self.shares)).member_threshold def is_complete(self) -> bool: if self.shares: return len(self.shares) >= self.member_threshold() else: return False @dataclass(frozen=True) class EncryptedMasterSecret: identifier: int extendable: bool iteration_exponent: int ciphertext: bytes @classmethod def from_master_secret( cls, master_secret: bytes, passphrase: bytes, identifier: int, extendable: bool, iteration_exponent: int, ) -> "EncryptedMasterSecret": ciphertext = cipher.encrypt( master_secret, passphrase, iteration_exponent, identifier, extendable ) return EncryptedMasterSecret( identifier, extendable, iteration_exponent, ciphertext ) def decrypt(self, passphrase: bytes) -> bytes: return cipher.decrypt( self.ciphertext, passphrase, self.iteration_exponent, self.identifier, self.extendable, ) RANDOM_BYTES = secrets.token_bytes """Source of random bytes. Can be overriden for deterministic testing.""" def _precompute_exp_log() -> Tuple[List[int], List[int]]: exp = [0 for i in range(255)] log = [0 for i in range(256)] poly = 1 for i in range(255): exp[i] = poly log[poly] = i # Multiply poly by the polynomial x + 1. poly = (poly << 1) ^ poly # Reduce poly by x^8 + x^4 + x^3 + x + 1. if poly & 0x100: poly ^= 0x11B return exp, log EXP_TABLE, LOG_TABLE = _precompute_exp_log() def _interpolate(shares: Sequence[RawShare], x: int) -> bytes: """ Returns f(x) given the Shamir shares (x_1, f(x_1)), ... , (x_k, f(x_k)). :param shares: The Shamir shares. :type shares: A list of pairs (x_i, y_i), where x_i is an integer and y_i is an array of bytes representing the evaluations of the polynomials in x_i. :param int x: The x coordinate of the result. :return: Evaluations of the polynomials in x. :rtype: Array of bytes. """ x_coordinates = set(share.x for share in shares) if len(x_coordinates) != len(shares): raise MnemonicError("Invalid set of shares. Share indices must be unique.") share_value_lengths = set(len(share.data) for share in shares) if len(share_value_lengths) != 1: raise MnemonicError( "Invalid set of shares. All share values must have the same length." ) if x in x_coordinates: for share in shares: if share.x == x: return share.data # Logarithm of the product of (x_i - x) for i = 1, ... , k. log_prod = sum(LOG_TABLE[share.x ^ x] for share in shares) result = bytes(share_value_lengths.pop()) for share in shares: # The logarithm of the Lagrange basis polynomial evaluated at x. log_basis_eval = ( log_prod - LOG_TABLE[share.x ^ x] - sum(LOG_TABLE[share.x ^ other.x] for other in shares) ) % 255 result = bytes( intermediate_sum ^ ( EXP_TABLE[(LOG_TABLE[share_val] + log_basis_eval) % 255] if share_val != 0 else 0 ) for share_val, intermediate_sum in zip(share.data, result) ) return result def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes: return hmac.new(random_data, shared_secret, "sha256").digest()[:DIGEST_LENGTH_BYTES] def _split_secret( threshold: int, share_count: int, shared_secret: bytes ) -> List[RawShare]: if threshold < 1: raise ValueError("The requested threshold must be a positive integer.") if threshold > share_count: raise ValueError( "The requested threshold must not exceed the number of shares." ) if share_count > MAX_SHARE_COUNT: raise ValueError( f"The requested number of shares must not exceed {MAX_SHARE_COUNT}." ) # If the threshold is 1, then the digest of the shared secret is not used. if threshold == 1: return [RawShare(i, shared_secret) for i in range(share_count)] random_share_count = threshold - 2 shares = [ RawShare(i, RANDOM_BYTES(len(shared_secret))) for i in range(random_share_count) ] random_part = RANDOM_BYTES(len(shared_secret) - DIGEST_LENGTH_BYTES) digest = _create_digest(random_part, shared_secret) base_shares = shares + [ RawShare(DIGEST_INDEX, digest + random_part), RawShare(SECRET_INDEX, shared_secret), ] for i in range(random_share_count, share_count): shares.append(RawShare(i, _interpolate(base_shares, i))) return shares def _recover_secret(threshold: int, shares: Sequence[RawShare]) -> bytes: # If the threshold is 1, then the digest of the shared secret is not used. if threshold == 1: return next(iter(shares)).data shared_secret = _interpolate(shares, SECRET_INDEX) digest_share = _interpolate(shares, DIGEST_INDEX) digest = digest_share[:DIGEST_LENGTH_BYTES] random_part = digest_share[DIGEST_LENGTH_BYTES:] if digest != _create_digest(random_part, shared_secret): raise MnemonicError("Invalid digest of the shared secret.") return shared_secret def decode_mnemonics(mnemonics: Iterable[str]) -> Dict[int, ShareGroup]: common_params: Set[ShareCommonParameters] = set() groups: Dict[int, ShareGroup] = {} for mnemonic in mnemonics: share = Share.from_mnemonic(mnemonic) common_params.add(share.common_parameters()) group = groups.setdefault(share.group_index, ShareGroup()) group.add(share) if len(common_params) != 1: raise MnemonicError( "Invalid set of mnemonics. " f"All mnemonics must begin with the same {ID_EXP_LENGTH_WORDS} words, " "must have the same group threshold and the same group count." ) return groups def split_ems( group_threshold: int, groups: Sequence[Tuple[int, int]], encrypted_master_secret: EncryptedMasterSecret, ) -> List[List[Share]]: """ Split an Encrypted Master Secret into mnemonic shares. This function is a counterpart to `recover_ems`, and it is used as a subroutine in `generate_mnemonics`. The input is an *already encrypted* Master Secret (EMS), so it is possible to encrypt the Master Secret in advance and perform the splitting later. :param group_threshold: The number of groups required to reconstruct the master secret. :param groups: A list of (member_threshold, member_count) pairs for each group, where member_count is the number of shares to generate for the group and member_threshold is the number of members required to reconstruct the group secret. :param encrypted_master_secret: The encrypted master secret to split. :return: List of groups of mnemonics. """ if len(encrypted_master_secret.ciphertext) * 8 < MIN_STRENGTH_BITS: raise ValueError( "The length of the master secret must be " f"at least {bits_to_bytes(MIN_STRENGTH_BITS)} bytes." ) if group_threshold > len(groups): raise ValueError( "The requested group threshold must not exceed the number of groups." ) if any( member_threshold == 1 and member_count > 1 for member_threshold, member_count in groups ): raise ValueError( "Creating multiple member shares with member threshold 1 is not allowed. " "Use 1-of-1 member sharing instead." ) group_shares = _split_secret( group_threshold, len(groups), encrypted_master_secret.ciphertext ) return [ [ Share( encrypted_master_secret.identifier, encrypted_master_secret.extendable, encrypted_master_secret.iteration_exponent, group_index, group_threshold, len(groups), member_index, member_threshold, value, ) for member_index, value in _split_secret( member_threshold, member_count, group_secret ) ] for (member_threshold, member_count), (group_index, group_secret) in zip( groups, group_shares ) ] def _random_identifier() -> int: """Returns a random identifier with the given bit length.""" identifier = int.from_bytes(RANDOM_BYTES(bits_to_bytes(ID_LENGTH_BITS)), "big") return identifier & ((1 << ID_LENGTH_BITS) - 1) def generate_mnemonics( group_threshold: int, groups: Sequence[Tuple[int, int]], master_secret: bytes, passphrase: bytes = b"", extendable: bool = True, iteration_exponent: int = 1, ) -> List[List[str]]: """ Split a master secret into mnemonic shares using Shamir's secret sharing scheme. The supplied Master Secret is encrypted by the passphrase (empty passphrase is used if none is provided) and split into a set of mnemonic shares. This is the user-friendly method to back up a pre-existing secret with the Shamir scheme, optionally protected by a passphrase. :param group_threshold: The number of groups required to reconstruct the master secret. :param groups: A list of (member_threshold, member_count) pairs for each group, where member_count is the number of shares to generate for the group and member_threshold is the number of members required to reconstruct the group secret. :param master_secret: The master secret to split. :param passphrase: The passphrase used to encrypt the master secret. :param int iteration_exponent: The encryption iteration exponent. :return: List of groups mnemonics. """ if not all(32 <= c <= 126 for c in passphrase): raise ValueError( "The passphrase must contain only printable ASCII characters (code points 32-126)." ) identifier = _random_identifier() encrypted_master_secret = EncryptedMasterSecret.from_master_secret( master_secret, passphrase, identifier, extendable, iteration_exponent ) grouped_shares = split_ems(group_threshold, groups, encrypted_master_secret) return [[share.mnemonic() for share in group] for group in grouped_shares] def recover_ems(groups: Dict[int, ShareGroup]) -> EncryptedMasterSecret: """ Combine shares, recover metadata and the Encrypted Master Secret. This function is a counterpart to `split_ems`, and it is used as a subroutine in `combine_mnemonics`. It returns the EMS itself and data required for its decryption, except for the passphrase. It is thus possible to defer decryption of the Master Secret to a later time. :param groups: Set of shares classified into groups. :return: Encrypted Master Secret """ if not groups: raise MnemonicError("The set of shares is empty.") params = next(iter(groups.values())).common_parameters() if len(groups) < params.group_threshold: raise MnemonicError( "Insufficient number of mnemonic groups. " f"The required number of groups is {params.group_threshold}." ) if len(groups) != params.group_threshold: raise MnemonicError( "Wrong number of mnemonic groups. " f"Expected {params.group_threshold} groups, " f"but {len(groups)} were provided." ) for group in groups.values(): if len(group) != group.member_threshold(): share_words = next(iter(group)).words() prefix = " ".join(share_words[:GROUP_PREFIX_LENGTH_WORDS]) raise MnemonicError( "Wrong number of mnemonics. " f'Expected {group.member_threshold()} mnemonics starting with "{prefix} ...", ' f"but {len(group)} were provided." ) group_shares = [ RawShare( group_index, _recover_secret(group.member_threshold(), group.to_raw_shares()), ) for group_index, group in groups.items() ] ciphertext = _recover_secret(params.group_threshold, group_shares) return EncryptedMasterSecret( params.identifier, params.extendable, params.iteration_exponent, ciphertext ) def combine_mnemonics(mnemonics: Iterable[str], passphrase: bytes = b"") -> bytes: """ Combine mnemonic shares to obtain the master secret which was previously split using Shamir's secret sharing scheme. This is the user-friendly method to recover a backed-up secret optionally protected by a passphrase. :param mnemonics: List of mnemonics. :param passphrase: The passphrase used to encrypt the master secret. :return: The master secret. """ if not mnemonics: raise MnemonicError("The list of mnemonics is empty.") groups = decode_mnemonics(mnemonics) encrypted_master_secret = recover_ems(groups) return encrypted_master_secret.decrypt(passphrase) python-shamir-mnemonic-0.3.0/shamir_mnemonic/share.py000066400000000000000000000155301462135465500227620ustar00rootroot00000000000000from dataclasses import dataclass from typing import Iterable, List, NamedTuple from . import rs1024, wordlist from .constants import ( CUSTOMIZATION_STRING_EXTENDABLE, CUSTOMIZATION_STRING_ORIG, EXTENDABLE_FLAG_LENGTH_BITS, ID_EXP_LENGTH_WORDS, ITERATION_EXP_LENGTH_BITS, METADATA_LENGTH_WORDS, MIN_MNEMONIC_LENGTH_WORDS, RADIX, RADIX_BITS, ) from .utils import MnemonicError, bits_to_bytes, bits_to_words, int_to_indices WordIndex = int def _int_to_word_indices(value: int, length: int) -> List[WordIndex]: """Converts an integer value to a list of base 1024 indices in big endian order.""" return list(int_to_indices(value, length, radix_bits=RADIX_BITS)) def _int_from_word_indices(indices: Iterable[WordIndex]) -> int: """Converts a list of base 1024 indices in big endian order to an integer value.""" value = 0 for index in indices: value = value * RADIX + index return value def _customization_string(extendable: bool) -> bytes: if extendable: return CUSTOMIZATION_STRING_EXTENDABLE else: return CUSTOMIZATION_STRING_ORIG class ShareCommonParameters(NamedTuple): """Parameters that are common to all shares of a master secret.""" identifier: int extendable: bool iteration_exponent: int group_threshold: int group_count: int class ShareGroupParameters(NamedTuple): """Parameters that are common to all shares of a master secret, which belong to the same group.""" identifier: int extendable: bool iteration_exponent: int group_index: int group_threshold: int group_count: int member_threshold: int @dataclass(frozen=True) class Share: """Represents a single mnemonic share and its metadata""" identifier: int extendable: bool iteration_exponent: int group_index: int group_threshold: int group_count: int index: int member_threshold: int value: bytes def common_parameters(self) -> ShareCommonParameters: """Return values that uniquely identify a matching set of shares.""" return ShareCommonParameters( self.identifier, self.extendable, self.iteration_exponent, self.group_threshold, self.group_count, ) def group_parameters(self) -> ShareGroupParameters: """Return values that uniquely identify shares belonging to the same group.""" return ShareGroupParameters( self.identifier, self.extendable, self.iteration_exponent, self.group_index, self.group_threshold, self.group_count, self.member_threshold, ) def _encode_id_exp(self) -> List[WordIndex]: id_exp_int = self.identifier << ( ITERATION_EXP_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS ) id_exp_int += self.extendable << ITERATION_EXP_LENGTH_BITS id_exp_int += self.iteration_exponent return _int_to_word_indices(id_exp_int, ID_EXP_LENGTH_WORDS) def _encode_share_params(self) -> List[WordIndex]: # each value is 4 bits, for 20 bits total val = self.group_index val <<= 4 val += self.group_threshold - 1 val <<= 4 val += self.group_count - 1 val <<= 4 val += self.index val <<= 4 val += self.member_threshold - 1 # group parameters are 2 words return _int_to_word_indices(val, 2) def words(self) -> List[str]: """Convert share data to a share mnemonic.""" value_word_count = bits_to_words(len(self.value) * 8) value_int = int.from_bytes(self.value, "big") value_data = _int_to_word_indices(value_int, value_word_count) share_data = self._encode_id_exp() + self._encode_share_params() + value_data checksum = rs1024.create_checksum( share_data, _customization_string(self.extendable) ) return list(wordlist.words_from_indices(share_data + checksum)) def mnemonic(self) -> str: """Convert share data to a share mnemonic.""" return " ".join(self.words()) @classmethod def from_mnemonic(cls, mnemonic: str) -> "Share": """Convert a share mnemonic to share data.""" mnemonic_data = wordlist.mnemonic_to_indices(mnemonic) if len(mnemonic_data) < MIN_MNEMONIC_LENGTH_WORDS: raise MnemonicError( "Invalid mnemonic length. The length of each mnemonic " f"must be at least {MIN_MNEMONIC_LENGTH_WORDS} words." ) padding_len = (RADIX_BITS * (len(mnemonic_data) - METADATA_LENGTH_WORDS)) % 16 if padding_len > 8: raise MnemonicError("Invalid mnemonic length.") id_exp_data = mnemonic_data[:ID_EXP_LENGTH_WORDS] id_exp_int = _int_from_word_indices(id_exp_data) identifier = id_exp_int >> ( EXTENDABLE_FLAG_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS ) extendable = bool((id_exp_int >> ITERATION_EXP_LENGTH_BITS) & 1) iteration_exponent = id_exp_int & ((1 << ITERATION_EXP_LENGTH_BITS) - 1) if not rs1024.verify_checksum(mnemonic_data, _customization_string(extendable)): raise MnemonicError( 'Invalid mnemonic checksum for "{} ...".'.format( " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) ) ) share_params_data = mnemonic_data[ID_EXP_LENGTH_WORDS : ID_EXP_LENGTH_WORDS + 2] share_params_int = _int_from_word_indices(share_params_data) share_params = int_to_indices(share_params_int, 5, 4) ( group_index, group_threshold, group_count, index, member_threshold, ) = share_params if group_count < group_threshold: raise MnemonicError( 'Invalid mnemonic "{} ...". Group threshold cannot be greater than group count.'.format( " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) ) ) value_data = mnemonic_data[ ID_EXP_LENGTH_WORDS + 2 : -rs1024.CHECKSUM_LENGTH_WORDS ] value_byte_count = bits_to_bytes(RADIX_BITS * len(value_data) - padding_len) value_int = _int_from_word_indices(value_data) try: value = value_int.to_bytes(value_byte_count, "big") except OverflowError: raise MnemonicError( 'Invalid mnemonic padding for "{} ...".'.format( " ".join(mnemonic.split()[: ID_EXP_LENGTH_WORDS + 2]) ) ) from None return cls( identifier, extendable, iteration_exponent, group_index, group_threshold + 1, group_count + 1, index, member_threshold + 1, value, ) python-shamir-mnemonic-0.3.0/shamir_mnemonic/utils.py000066400000000000000000000030421462135465500230130ustar00rootroot00000000000000from typing import Iterable class MnemonicError(Exception): pass def _round_bits(n: int, radix_bits: int) -> int: """Get the number of `radix_bits`-sized digits required to store a `n`-bit value.""" return (n + radix_bits - 1) // radix_bits def bits_to_bytes(n: int) -> int: """Round up bit count to whole bytes.""" return _round_bits(n, 8) def bits_to_words(n: int) -> int: """Round up bit count to a multiple of word size.""" # XXX # In order to properly functionally decompose the original 1-file implementation, # function bits_to_words can only exist if it knows the value of RADIX_BITS (which # informs us of the word size). However, constants.py make use of the function, # because some constants count word-size of things. # # I considered the "least evil" solution to define bits_to_words in utils where it # logically belongs, and import constants only inside the function. This will work # as long as calls to bits_to_words only happens *after* RADIX_BITS are declared. # # An alternative is to have a private implementation of bits_to_words in constants from . import constants assert hasattr(constants, "RADIX_BITS"), "Declare RADIX_BITS *before* calling this" return _round_bits(n, constants.RADIX_BITS) def int_to_indices(value: int, length: int, radix_bits: int) -> Iterable[int]: """Convert an integer value to indices in big endian order.""" mask = (1 << radix_bits) - 1 return ((value >> (i * radix_bits)) & mask for i in reversed(range(length))) python-shamir-mnemonic-0.3.0/shamir_mnemonic/wordlist.py000066400000000000000000000021311462135465500235200ustar00rootroot00000000000000import os.path from typing import Dict, Iterable, List, Sequence, Tuple from .constants import RADIX from .utils import MnemonicError def _load_wordlist() -> Tuple[List[str], Dict[str, int]]: with open(os.path.join(os.path.dirname(__file__), "wordlist.txt"), "r") as f: wordlist = [word.strip() for word in f] if len(wordlist) != RADIX: raise ImportError( f"The wordlist should contain {RADIX} words, but it contains {len(wordlist)} words." ) word_index_map = {word: i for i, word in enumerate(wordlist)} return wordlist, word_index_map WORDLIST, WORD_INDEX_MAP = _load_wordlist() def words_from_indices(indices: Iterable[int]) -> Iterable[str]: return (WORDLIST[i] for i in indices) def mnemonic_from_indices(indices: Iterable[int]) -> str: return " ".join(words_from_indices(indices)) def mnemonic_to_indices(mnemonic: str) -> Sequence[int]: try: return [WORD_INDEX_MAP[word.lower()] for word in mnemonic.split()] except KeyError as key_error: raise MnemonicError(f"Invalid mnemonic word {key_error}.") from None python-shamir-mnemonic-0.3.0/shamir_mnemonic/wordlist.txt000066400000000000000000000160771462135465500237250ustar00rootroot00000000000000academic acid acne acquire acrobat activity actress adapt adequate adjust admit adorn adult advance advocate afraid again agency agree aide aircraft airline airport ajar alarm album alcohol alien alive alpha already alto aluminum always amazing ambition amount amuse analysis anatomy ancestor ancient angel angry animal answer antenna anxiety apart aquatic arcade arena argue armed artist artwork aspect auction august aunt average aviation avoid award away axis axle beam beard beaver become bedroom behavior being believe belong benefit best beyond bike biology birthday bishop black blanket blessing blimp blind blue body bolt boring born both boundary bracelet branch brave breathe briefing broken brother browser bucket budget building bulb bulge bumpy bundle burden burning busy buyer cage calcium camera campus canyon capacity capital capture carbon cards careful cargo carpet carve category cause ceiling center ceramic champion change charity check chemical chest chew chubby cinema civil class clay cleanup client climate clinic clock clogs closet clothes club cluster coal coastal coding column company corner costume counter course cover cowboy cradle craft crazy credit cricket criminal crisis critical crowd crucial crunch crush crystal cubic cultural curious curly custody cylinder daisy damage dance darkness database daughter deadline deal debris debut decent decision declare decorate decrease deliver demand density deny depart depend depict deploy describe desert desire desktop destroy detailed detect device devote diagnose dictate diet dilemma diminish dining diploma disaster discuss disease dish dismiss display distance dive divorce document domain domestic dominant dough downtown dragon dramatic dream dress drift drink drove drug dryer duckling duke duration dwarf dynamic early earth easel easy echo eclipse ecology edge editor educate either elbow elder election elegant element elephant elevator elite else email emerald emission emperor emphasis employer empty ending endless endorse enemy energy enforce engage enjoy enlarge entrance envelope envy epidemic episode equation equip eraser erode escape estate estimate evaluate evening evidence evil evoke exact example exceed exchange exclude excuse execute exercise exhaust exotic expand expect explain express extend extra eyebrow facility fact failure faint fake false family famous fancy fangs fantasy fatal fatigue favorite fawn fiber fiction filter finance findings finger firefly firm fiscal fishing fitness flame flash flavor flea flexible flip float floral fluff focus forbid force forecast forget formal fortune forward founder fraction fragment frequent freshman friar fridge friendly frost froth frozen fumes funding furl fused galaxy game garbage garden garlic gasoline gather general genius genre genuine geology gesture glad glance glasses glen glimpse goat golden graduate grant grasp gravity gray greatest grief grill grin grocery gross group grownup grumpy guard guest guilt guitar gums hairy hamster hand hanger harvest have havoc hawk hazard headset health hearing heat helpful herald herd hesitate hobo holiday holy home hormone hospital hour huge human humidity hunting husband hush husky hybrid idea identify idle image impact imply improve impulse include income increase index indicate industry infant inform inherit injury inmate insect inside install intend intimate invasion involve iris island isolate item ivory jacket jerky jewelry join judicial juice jump junction junior junk jury justice kernel keyboard kidney kind kitchen knife knit laden ladle ladybug lair lamp language large laser laundry lawsuit leader leaf learn leaves lecture legal legend legs lend length level liberty library license lift likely lilac lily lips liquid listen literary living lizard loan lobe location losing loud loyalty luck lunar lunch lungs luxury lying lyrics machine magazine maiden mailman main makeup making mama manager mandate mansion manual marathon march market marvel mason material math maximum mayor meaning medal medical member memory mental merchant merit method metric midst mild military mineral minister miracle mixed mixture mobile modern modify moisture moment morning mortgage mother mountain mouse move much mule multiple muscle museum music mustang nail national necklace negative nervous network news nuclear numb numerous nylon oasis obesity object observe obtain ocean often olympic omit oral orange orbit order ordinary organize ounce oven overall owner paces pacific package paid painting pajamas pancake pants papa paper parcel parking party patent patrol payment payroll peaceful peanut peasant pecan penalty pencil percent perfect permit petition phantom pharmacy photo phrase physics pickup picture piece pile pink pipeline pistol pitch plains plan plastic platform playoff pleasure plot plunge practice prayer preach predator pregnant premium prepare presence prevent priest primary priority prisoner privacy prize problem process profile program promise prospect provide prune public pulse pumps punish puny pupal purchase purple python quantity quarter quick quiet race racism radar railroad rainbow raisin random ranked rapids raspy reaction realize rebound rebuild recall receiver recover regret regular reject relate remember remind remove render repair repeat replace require rescue research resident response result retailer retreat reunion revenue review reward rhyme rhythm rich rival river robin rocky romantic romp roster round royal ruin ruler rumor sack safari salary salon salt satisfy satoshi saver says scandal scared scatter scene scholar science scout scramble screw script scroll seafood season secret security segment senior shadow shaft shame shaped sharp shelter sheriff short should shrimp sidewalk silent silver similar simple single sister skin skunk slap slavery sled slice slim slow slush smart smear smell smirk smith smoking smug snake snapshot sniff society software soldier solution soul source space spark speak species spelling spend spew spider spill spine spirit spit spray sprinkle square squeeze stadium staff standard starting station stay steady step stick stilt story strategy strike style subject submit sugar suitable sunlight superior surface surprise survive sweater swimming swing switch symbolic sympathy syndrome system tackle tactics tadpole talent task taste taught taxi teacher teammate teaspoon temple tenant tendency tension terminal testify texture thank that theater theory therapy thorn threaten thumb thunder ticket tidy timber timely ting tofu together tolerate total toxic tracks traffic training transfer trash traveler treat trend trial tricycle trip triumph trouble true trust twice twin type typical ugly ultimate umbrella uncover undergo unfair unfold unhappy union universe unkind unknown unusual unwrap upgrade upstairs username usher usual valid valuable vampire vanish various vegan velvet venture verdict verify very veteran vexed victim video view vintage violence viral visitor visual vitamins vocal voice volume voter voting walnut warmth warn watch wavy wealthy weapon webcam welcome welfare western width wildlife window wine wireless wisdom withdraw wits wolf woman work worthy wrap wrist writing wrote year yelp yield yoga zero python-shamir-mnemonic-0.3.0/test_shamir.py000066400000000000000000000143421462135465500210320ustar00rootroot00000000000000import json import secrets from itertools import combinations from random import shuffle import pytest from bip32utils import BIP32Key import shamir_mnemonic as shamir from shamir_mnemonic import MnemonicError MS = b"ABCDEFGHIJKLMNOP" def test_basic_sharing_random(): secret = secrets.token_bytes(16) mnemonics = shamir.generate_mnemonics(1, [(3, 5)], secret)[0] assert shamir.combine_mnemonics(mnemonics[:3]) == shamir.combine_mnemonics( mnemonics[2:] ) def test_basic_sharing_fixed(): mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS)[0] assert MS == shamir.combine_mnemonics(mnemonics[:3]) assert MS == shamir.combine_mnemonics(mnemonics[1:4]) with pytest.raises(MnemonicError): shamir.combine_mnemonics(mnemonics[1:3]) def test_passphrase(): mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, b"TREZOR")[0] assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") assert MS != shamir.combine_mnemonics(mnemonics[1:4]) def test_non_extendable(): mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, extendable=False)[0] assert MS == shamir.combine_mnemonics(mnemonics[1:4]) def test_iteration_exponent(): mnemonics = shamir.generate_mnemonics( 1, [(3, 5)], MS, b"TREZOR", iteration_exponent=1 )[0] assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") assert MS != shamir.combine_mnemonics(mnemonics[1:4]) mnemonics = shamir.generate_mnemonics( 1, [(3, 5)], MS, b"TREZOR", iteration_exponent=2 )[0] assert MS == shamir.combine_mnemonics(mnemonics[1:4], b"TREZOR") assert MS != shamir.combine_mnemonics(mnemonics[1:4]) def test_group_sharing(): group_threshold = 2 group_sizes = (5, 3, 5, 1) member_thresholds = (3, 2, 2, 1) mnemonics = shamir.generate_mnemonics( group_threshold, list(zip(member_thresholds, group_sizes)), MS ) # Test all valid combinations of mnemonics. for groups in combinations(zip(mnemonics, member_thresholds), group_threshold): for group1_subset in combinations(groups[0][0], groups[0][1]): for group2_subset in combinations(groups[1][0], groups[1][1]): mnemonic_subset = list(group1_subset + group2_subset) shuffle(mnemonic_subset) assert MS == shamir.combine_mnemonics(mnemonic_subset) # Minimal sets of mnemonics. assert MS == shamir.combine_mnemonics( [mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]] ) assert MS == shamir.combine_mnemonics( [mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]] ) # One complete group and one incomplete group out of two groups required. with pytest.raises(MnemonicError): shamir.combine_mnemonics(mnemonics[0][2:] + [mnemonics[1][0]]) # One group of two required. with pytest.raises(MnemonicError): shamir.combine_mnemonics(mnemonics[0][1:4]) def test_group_sharing_threshold_1(): group_threshold = 1 group_sizes = (5, 3, 5, 1) member_thresholds = (3, 2, 2, 1) mnemonics = shamir.generate_mnemonics( group_threshold, list(zip(member_thresholds, group_sizes)), MS ) # Test all valid combinations of mnemonics. for group, member_threshold in zip(mnemonics, member_thresholds): for group_subset in combinations(group, member_threshold): mnemonic_subset = list(group_subset) shuffle(mnemonic_subset) assert MS == shamir.combine_mnemonics(mnemonic_subset) def test_all_groups_exist(): for group_threshold in (1, 2, 5): mnemonics = shamir.generate_mnemonics( group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], MS ) assert len(mnemonics) == 5 assert len(sum(mnemonics, [])) == 19 def test_invalid_sharing(): # Short master secret. with pytest.raises(ValueError): shamir.generate_mnemonics(1, [(2, 3)], MS[:14]) # Odd length master secret. with pytest.raises(ValueError): shamir.generate_mnemonics(1, [(2, 3)], MS + b"X") # Group threshold exceeds number of groups. with pytest.raises(ValueError): shamir.generate_mnemonics(3, [(3, 5), (2, 5)], MS) # Invalid group threshold. with pytest.raises(ValueError): shamir.generate_mnemonics(0, [(3, 5), (2, 5)], MS) # Member threshold exceeds number of members. with pytest.raises(ValueError): shamir.generate_mnemonics(2, [(3, 2), (2, 5)], MS) # Invalid member threshold. with pytest.raises(ValueError): shamir.generate_mnemonics(2, [(0, 2), (2, 5)], MS) # Group with multiple members and member threshold 1. with pytest.raises(ValueError): shamir.generate_mnemonics(2, [(3, 5), (1, 3), (2, 5)], MS) def test_vectors(): with open("vectors.json", "r") as f: vectors = json.load(f) for description, mnemonics, secret_hex, xprv in vectors: if secret_hex: secret = bytes.fromhex(secret_hex) assert secret == shamir.combine_mnemonics( mnemonics, b"TREZOR" ), 'Incorrect secret for test vector "{}".'.format(description) assert ( BIP32Key.fromEntropy(secret).ExtendedKey() == xprv ), 'Incorrect xprv for test vector "{}".'.format(description) else: with pytest.raises(MnemonicError): shamir.combine_mnemonics(mnemonics) pytest.fail( 'Failed to raise exception for test vector "{}".'.format( description ) ) def test_split_ems(): encrypted_master_secret = shamir.EncryptedMasterSecret.from_master_secret( MS, b"TREZOR", identifier=42, extendable=True, iteration_exponent=1 ) grouped_shares = shamir.split_ems(1, [(3, 5)], encrypted_master_secret) mnemonics = [share.mnemonic() for share in grouped_shares[0]] recovered = shamir.combine_mnemonics(mnemonics[:3], b"TREZOR") assert recovered == MS def test_recover_ems(): mnemonics = shamir.generate_mnemonics(1, [(3, 5)], MS, b"TREZOR")[0] groups = shamir.decode_mnemonics(mnemonics[:3]) encrypted_master_secret = shamir.recover_ems(groups) recovered = encrypted_master_secret.decrypt(b"TREZOR") assert recovered == MS python-shamir-mnemonic-0.3.0/vectors.json000066400000000000000000000536131462135465500205220ustar00rootroot00000000000000[ [ "1. Valid mnemonic without sharing (128 bits)", [ "duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard" ], "bb54aac4b89dc868ba37d9cc21b2cece", "xprv9s21ZrQH143K4QViKpwKCpS2zVbz8GrZgpEchMDg6KME9HZtjfL7iThE9w5muQA4YPHKN1u5VM1w8D4pvnjxa2BmpGMfXr7hnRrRHZ93awZ" ], [ "2. Mnemonic with invalid checksum (128 bits)", [ "duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney" ], "", "" ], [ "3. Mnemonic with invalid padding (128 bits)", [ "duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness" ], "", "" ], [ "4. Basic sharing 2-of-3 (128 bits)", [ "shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed", "shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking" ], "b43ceb7e57a0ea8766221624d01b0864", "xprv9s21ZrQH143K2nNuAbfWPHBtfiSCS14XQgb3otW4pX655q58EEZeC8zmjEUwucBu9dPnxdpbZLCn57yx45RBkwJHnwHFjZK4XPJ8SyeYjYg" ], [ "5. Basic sharing 2-of-3 (128 bits)", [ "shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed" ], "", "" ], [ "6. Mnemonics with different identifiers (128 bits)", [ "adequate smoking academic acid debut wine petition glen cluster slow rhyme slow simple epidemic rumor junk tracks treat olympic tolerate", "adequate stay academic agency agency formal party ting frequent learn upstairs remember smear leaf damage anatomy ladle market hush corner" ], "", "" ], [ "7. Mnemonics with different iteration exponents (128 bits)", [ "peasant leaves academic acid desert exact olympic math alive axle trial tackle drug deny decent smear dominant desert bucket remind", "peasant leader academic agency cultural blessing percent network envelope medal junk primary human pumps jacket fragment payroll ticket evoke voice" ], "", "" ], [ "8. Mnemonics with mismatching group thresholds (128 bits)", [ "liberty category beard echo animal fawn temple briefing math username various wolf aviation fancy visual holy thunder yelp helpful payment", "liberty category beard email beyond should fancy romp founder easel pink holy hairy romp loyalty material victim owner toxic custody", "liberty category academic easy being hazard crush diminish oral lizard reaction cluster force dilemma deploy force club veteran expect photo" ], "", "" ], [ "9. Mnemonics with mismatching group counts (128 bits)", [ "average senior academic leaf broken teacher expect surface hour capture obesity desire negative dynamic dominant pistol mineral mailman iris aide", "average senior academic agency curious pants blimp spew clothes slice script dress wrap firm shaft regular slavery negative theater roster" ], "", "" ], [ "10. Mnemonics with greater group threshold than group counts (128 bits)", [ "music husband acrobat acid artist finance center either graduate swimming object bike medical clothes station aspect spider maiden bulb welcome", "music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow", "music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce" ], "", "" ], [ "11. Mnemonics with duplicate member indices (128 bits)", [ "device stay academic always dive coal antenna adult black exceed stadium herald advance soldier busy dryer daughter evaluate minister laser", "device stay academic always dwarf afraid robin gravity crunch adjust soul branch walnut coastal dream costume scholar mortgage mountain pumps" ], "", "" ], [ "12. Mnemonics with mismatching member thresholds (128 bits)", [ "hour painting academic academic device formal evoke guitar random modern justice filter withdraw trouble identify mailman insect general cover oven", "hour painting academic agency artist again daisy capital beaver fiber much enjoy suitable symbolic identify photo editor romp float echo" ], "", "" ], [ "13. Mnemonics giving an invalid digest (128 bits)", [ "guilt walnut academic acid deliver remove equip listen vampire tactics nylon rhythm failure husband fatigue alive blind enemy teaspoon rebound", "guilt walnut academic agency brave hamster hobo declare herd taste alpha slim criminal mild arcade formal romp branch pink ambition" ], "", "" ], [ "14. Insufficient number of groups (128 bits, case 1)", [ "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice" ], "", "" ], [ "15. Insufficient number of groups (128 bits, case 2)", [ "eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join", "eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter" ], "", "" ], [ "16. Threshold number of groups, but insufficient number of members in one group (128 bits)", [ "eraser senior decision shadow artist work morning estate greatest pipeline plan ting petition forget hormone flexible general goat admit surface", "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice" ], "", "" ], [ "17. Threshold number of groups and members in each group (128 bits, case 1)", [ "eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter", "eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup", "eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces", "eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate", "eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing" ], "7c3397a292a5941682d7a4ae2d898d11", "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" ], [ "18. Threshold number of groups and members in each group (128 bits, case 2)", [ "eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing", "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice", "eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join" ], "7c3397a292a5941682d7a4ae2d898d11", "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" ], [ "19. Threshold number of groups and members in each group (128 bits, case 3)", [ "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice", "eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market" ], "7c3397a292a5941682d7a4ae2d898d11", "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV" ], [ "20. Valid mnemonic without sharing (256 bits)", [ "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" ], "989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92", "xprv9s21ZrQH143K41mrxxMT2FpiheQ9MFNmWVK4tvX2s28KLZAhuXWskJCKVRQprq9TnjzzzEYePpt764csiCxTt22xwGPiRmUjYUUdjaut8RM" ], [ "21. Mnemonic with invalid checksum (256 bits)", [ "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" ], "", "" ], [ "22. Mnemonic with invalid padding (256 bits)", [ "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" ], "", "" ], [ "23. Basic sharing 2-of-3 (256 bits)", [ "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", "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" ], "c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae", "xprv9s21ZrQH143K3a4GRMgK8WnawupkwkP6gyHxRsXnMsYPTPH21fWwNcAytijtfyftqNfiaY8LgQVdBQvHZ9FBvtwdjC7LCYxjYruJFuLzyMQ" ], [ "24. Basic sharing 2-of-3 (256 bits)", [ "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" ], "", "" ], [ "25. Mnemonics with different identifiers (256 bits)", [ "smear husband academic acid deadline scene venture distance dive overall parking bracelet elevator justice echo burning oven chest duke nylon", "smear isolate academic agency alpha mandate decorate burden recover guard exercise fatal force syndrome fumes thank guest drift dramatic mule" ], "", "" ], [ "26. Mnemonics with different iteration exponents (256 bits)", [ "finger trash academic acid average priority dish revenue academic hospital spirit western ocean fact calcium syndrome greatest plan losing dictate", "finger traffic academic agency building lilac deny paces subject threaten diploma eclipse window unknown health slim piece dragon focus smirk" ], "", "" ], [ "27. Mnemonics with mismatching group thresholds (256 bits)", [ "flavor pink beard echo depart forbid retreat become frost helpful juice unwrap reunion credit math burning spine black capital lair", "flavor pink beard email diet teaspoon freshman identify document rebound cricket prune headset loyalty smell emission skin often square rebound", "flavor pink academic easy credit cage raisin crazy closet lobe mobile become drink human tactics valuable hand capture sympathy finger" ], "", "" ], [ "28. Mnemonics with mismatching group counts (256 bits)", [ "column flea academic leaf debut extra surface slow timber husky lawsuit game behavior husky swimming already paper episode tricycle scroll", "column flea academic agency blessing garbage party software stadium verify silent umbrella therapy decorate chemical erode dramatic eclipse replace apart" ], "", "" ], [ "29. Mnemonics with greater group threshold than group counts (256 bits)", [ "smirk pink acrobat acid auction wireless impulse spine sprinkle fortune clogs elbow guest hush loyalty crush dictate tracks airport talent", "smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit", "smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful" ], "", "" ], [ "30. Mnemonics with duplicate member indices (256 bits)", [ "fishing recover academic always device craft trend snapshot gums skin downtown watch device sniff hour clock public maximum garlic born", "fishing recover academic always aircraft view software cradle fangs amazing package plastic evaluate intend penalty epidemic anatomy quarter cage apart" ], "", "" ], [ "31. Mnemonics with mismatching member thresholds (256 bits)", [ "evoke garden academic academic answer wolf scandal modern warmth station devote emerald market physics surface formal amazing aquatic gesture medical", "evoke garden academic agency deal revenue knit reunion decrease magazine flexible company goat repair alarm military facility clogs aide mandate" ], "", "" ], [ "32. Mnemonics giving an invalid digest (256 bits)", [ "river deal academic acid average forbid pistol peanut custody bike class aunt hairy merit valid flexible learn ajar very easel", "river deal academic agency camera amuse lungs numb isolate display smear piece traffic worthy year patrol crush fact fancy emission" ], "", "" ], [ "33. Insufficient number of groups (256 bits, case 1)", [ "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" ], "", "" ], [ "34. Insufficient number of groups (256 bits, case 2)", [ "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", "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" ], "", "" ], [ "35. Threshold number of groups, but insufficient number of members in one group (256 bits)", [ "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", "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" ], "", "" ], [ "36. Threshold number of groups and members in each group (256 bits, case 1)", [ "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", "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", "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", "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", "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" ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" ], [ "37. Threshold number of groups and members in each group (256 bits, case 2)", [ "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", "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", "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" ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" ], [ "38. Threshold number of groups and members in each group (256 bits, case 3)", [ "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", "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" ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c" ], [ "39. Mnemonic with insufficient length", [ "junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder" ], "", "" ], [ "40. Mnemonic with invalid master secret length", [ "fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter" ], "", "" ], [ "41. Valid mnemonics which can detect some errors in modular arithmetic", [ "herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven", "herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace", "herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult" ], "ad6f2ad8b59bbbaa01369b9006208d9a", "xprv9s21ZrQH143K2R4HJxcG1eUsudvHM753BZ9vaGkpYCoeEhCQx147C5qEcupPHxcXYfdYMwJmsKXrHDhtEwutxTTvFzdDCZVQwHneeQH8ioH" ], [ "42. Valid extendable mnemonic without sharing (128 bits)", [ "testify swimming academic academic column loyalty smear include exotic bedroom exotic wrist lobe cover grief golden smart junior estimate learn" ], "1679b4516e0ee5954351d288a838f45e", "xprv9s21ZrQH143K2w6eTpQnB73CU8Qrhg6gN3D66Jr16n5uorwoV7CwxQ5DofRPyok5DyRg4Q3BfHfCgJFk3boNRPPt1vEW1ENj2QckzVLQFXu" ], [ "43. Extendable basic sharing 2-of-3 (128 bits)", [ "enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish", "enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce" ], "48b1a4b80b8c209ad42c33672bdaa428", "xprv9s21ZrQH143K4FS1qQdXYAFVAHiSAnjj21YAKGh2CqUPJ2yQhMmYGT4e5a2tyGLiVsRgTEvajXkxhg92zJ8zmWZas9LguQWz7WZShfJg6RS" ], [ "44. Valid extendable mnemonic without sharing (256 bits)", [ "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" ], "8340611602fe91af634a5f4608377b5235fa2d757c51d720c0c7656249a3035f", "xprv9s21ZrQH143K2yJ7S8bXMiGqp1fySH8RLeFQKQmqfmmLTRwWmAYkpUcWz6M42oGoFMJRENmvsGQmunWTdizsi8v8fku8gpbVvYSiCYJTF1Y" ], [ "45. Extendable basic sharing 2-of-3 (256 bits)", [ "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", "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" ], "8dc652d6d6cd370d8c963141f6d79ba440300f25c467302c1d966bff8f62300d", "xprv9s21ZrQH143K2eFW2zmu3aayWWd6MJZBG7RebW35fiKcoCZ6jFi6U5gzffB9McDdiKTecUtRqJH9GzueCXiQK1LaQXdgthS8DgWfC8Uu3z7" ] ]