Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c41444e
Recover EncryptedMasterSecrets from arbitrary sequences of Mnemonics
pjkundert Nov 15, 2024
3324622
Blacken
pjkundert Nov 15, 2024
8e870f0
Flake8
pjkundert Nov 15, 2024
afe6eb0
Optionally be strict about invalid mnemonics, missing secret
pjkundert Nov 15, 2024
9f9fed9
Correct accumulation of unused mnemonics
pjkundert Nov 15, 2024
d25c3ad
Provide test repeatabilty by ordering recovery mnemonics
pjkundert Nov 20, 2024
211276f
Fix import order
pjkundert Nov 20, 2024
760c632
Provide Nix and venv support for build, tidy code
pjkundert Jul 21, 2025
6eba68e
Factor out locate_ems_rawshares and progress toward 'complete' recovery
pjkundert Jul 22, 2025
fcf45e2
Test recovery of multiple EncryptedMasterSecrets with corrupt Shares
pjkundert Jul 23, 2025
46b8a96
Recover complete distinct EncryptedMasterSecrets with corrupt shares
pjkundert Jul 24, 2025
f87a313
Check that subset of results returned without complete=True
pjkundert Jul 26, 2025
7aa869c
Update tests to reflect improved deepset comparison
pjkundert Jul 27, 2025
af76c82
Add black to style and flake8 to analyze targets
pjkundert Aug 13, 2025
ebd0a90
Remove unnecessary parameter, rename and simplify for clarity of purpose
pjkundert Aug 13, 2025
c2172f6
Working group membership expand
pjkundert Aug 14, 2025
7083628
Clean up and test group expand implementation, fix analyze target
pjkundert Aug 14, 2025
d2896cb
Clean up most type issues (remainder seem to be false positives)
pjkundert Aug 14, 2025
c954f84
Add the expand command to the cli and README, and tidy up type
pjkundert Aug 14, 2025
acf3f62
Select a sensible default for 0/None group expand
pjkundert Aug 14, 2025
3662561
Fix up style issues
pjkundert Aug 14, 2025
ce07c38
Explain and test the extendable flag
pjkundert Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
PYTHON=python3
POETRY=poetry
SHELL := /bin/bash

PYTHON ?= $(shell python3 --version >/dev/null 2>&1 && echo python3 || echo python )

# Ensure $(PYTHON), $(VENV) are re-evaluated at time of expansion, when target 'python' and 'poetry' are known to be available
PYTHON_V = $(shell $(PYTHON) -c "import sys; print('-'.join((('venv' if sys.prefix != sys.base_prefix else next(iter(filter(None,sys.base_prefix.split('/'))))),sys.platform,sys.implementation.cache_tag)))" 2>/dev/null )

VERSION = $(shell poetry version -s 2>/dev/null)
VENV = $(CURDIR)-$(VERSION)-$(PYTHON_V)

# Force export of variables that might be set from command line
export VENV_OPTS ?=
export POETRY ?= poetry
export PYTEST ?= pytest
export PYTEST_OPTS ?= # -vv --capture=no --mypy


build:
Expand All @@ -8,6 +21,9 @@ build:
install:
$(POETRY) install

install-dev:
$(POETRY) install --with dev

clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts

clean-build: ## remove build artifacts
Expand All @@ -30,15 +46,61 @@ clean-test: ## remove test and coverage artifacts
rm -fr .pytest_cache

test:
pytest
$(PYTEST) $(PYTEST_OPTS)

# Run all tests with names matching the target string
unit-%:
$(PYTEST) $(PYTEST_OPTS) -k $*

style_check:
isort --check-only shamir_mnemonic/ *.py
black shamir_mnemonic/ *.py --check

analyze: style_check
$(PYTHON) -m flake8 --color never -j 1 \
--ignore=W503,E501,E741 \
shamir_mnemonic test_shamir.py

style:
black shamir_mnemonic/ *.py
isort shamir_mnemonic/ *.py

#
# Nix and VirtualEnv build, install and activate
#
# Create, start and run commands in "interactive" shell with a python venv's activate init-file.
# Doesn't allow recursive creation of a venv with a venv-supplied python. Alters the bin/activate
# to include the user's .bashrc (eg. Git prompts, aliases, ...). Use to run Makefile targets in a
# proper context, for example to obtain a Nix environment containing the proper Python version,
# create a python venv with the current Python environment.
#
# make nix-venv-build
#
nix-%:
@if [ -r flake.nix ]; then \
nix develop $(NIX_OPTS) --command make $*; \
else \
nix-shell $(NIX_OPTS) --run "make $*"; \
fi

venv-%: $(VENV)
@echo; echo "*** Running in $< VirtualEnv: make $*"
@bash --init-file $</bin/activate -ic "make $*"

venv: $(VENV)
@echo; echo "*** Activating $< VirtualEnv for Interactive $(SHELL)"
@bash --init-file $</bin/activate -i

$(VENV):
@[[ "$(PYTHON_V)" =~ "^venv" ]] && ( echo -e "\n\n!!! $@ Cannot start a venv within a venv"; false ) || true
@echo; echo "*** Building $@ VirtualEnv..."
@rm -rf $@ && $(PYTHON) -m venv $(VENV_OPTS) $@ && sed -i -e '1s:^:. $$HOME/.bashrc\n:' $@/bin/activate \
&& source $@/bin/activate \
&& make install-dev

print-%:
@echo $* = $($*)
@echo $*\'s origin is $(origin $*)


.PHONY: clean clean-build clean-pyc clean-test test style_check style
.PHONY: clean clean-build clean-pyc clean-test test style_check style venv
49 changes: 49 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ open, and calculations are most likely trivially vulnerable to side-channel atta
The purpose of this code is to verify correctness of other implementations. **It should
not be used for handling sensitive secrets**.

Extendable encrypted master secrets
-----------------------------------

When you SLIP-39 encode a master secret with a password, you can always recover the original secret
with the same password. You of course get a **different** secret (ie. a different wallet) with a
different password; this is by design: you can (for example) have a master password for your true
wallet containing your funds, and a "decoy" password for a valid wallet that contains funds to
satisfy an attacker. Or, you may simply derive multiple wallets for different purposes with
different passwords.

The mnemonics are by default "extendable", meaning that you can re-encode the same master secret
again (with different SLIP-39 group specs), and get the same decrypted secret back with the original
password, and the **same** wallets with your other passwords.

If desired, you can produce **--no-extendable** encrypted master secrets (which used to be the
SLIP-39 standard), which always recover the original secret with the original password -- but
produce **different** secrets for all other passwords. This only causes surprises when you want to
re-encrypt the same master secret again: you can't re-obtain the **other** passwords' wallets using
the newly encoded mnemonics!

To reduce surprises, SLIP-39 now produces **--extendable** encrypted master secrets by default.

Installation
------------

Expand All @@ -54,6 +76,15 @@ Install the [Poetry](https://python-poetry.org/) tool, checkout
$ poetry install
$ poetry shell

Alternatively, install [Nix](https://nixos.org/download/), and (assuming you have GNU `make` available), run:

.. code-block:: console

$ make nix-venv-test

to install a Python environment, create a Python `venv`, enter it and run the `make test` target. To enter
the `venv` in an interactive shell, run `make nix-venv`.

CLI usage
---------

Expand Down Expand Up @@ -86,6 +117,24 @@ You can specify a custom scheme. For example, to create three groups, with 2-of-

Use :code:`shamir --help` or :code:`shamir create --help` to see all available options.

CLI usage: expand an existing mnemonic group
--------------------------------------------

If you wish to increase the number of mnemonics in an existing multi-mnemonic group, you can now do
this. All existing mnemonics remain valid.

To expand an existing group 3 to include 10 mnemonics, use:

.. code-block:: console

$ shamir expand --change 3 10

Enter mnemonics sufficient to recover the master secret, including all of the group(s) you desire to
:code:`--change`. However, you may elect to replace a missing group with a new single-Share group
(if you don't specify :code:`--strict`).

Use :code:`shamir --help` or :code:`shamir expand --help` to see all available options.

If you want to run the CLI from a local checkout without installing, use the following
command:

Expand Down
72 changes: 72 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{ pkgs ? import ./nixpkgs.nix {} }:

with pkgs;

let
in
{
py314 = stdenv.mkDerivation rec {
name = "python314-with-poetry";

buildInputs = [
cacert
git
gnumake
openssh
python314
poetry
];
};

py313 = stdenv.mkDerivation rec {
name = "python313-with-poetry";

buildInputs = [
cacert
git
gnumake
openssh
python313
poetry
];
};

py312 = stdenv.mkDerivation rec {
name = "python312-with-poetry";

buildInputs = [
cacert
git
gnumake
openssh
python312
poetry
];
};

py311 = stdenv.mkDerivation rec {
name = "python311-with-poetry";

buildInputs = [
cacert
git
gnumake
openssh
python311
poetry
];
};

py310 = stdenv.mkDerivation rec {
name = "python310-with-poetry";

buildInputs = [
cacert
git
gnumake
openssh
python310
poetry
];
};
}
4 changes: 4 additions & 0 deletions nixpkgs.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/25.05.tar.gz";
sha256 = "1915r28xc4znrh2vf4rrjnxldw2imysz819gzhk9qlrkqanmfsxd";
})
23 changes: 22 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "shamir-mnemonic"
version = "0.3.1"
version = "0.3.2"
description = "SLIP-39 Shamir Mnemonics"
authors = ["Trezor <info@trezor.io>"]
license = "MIT"
Expand All @@ -17,15 +17,36 @@ click = { version = ">=7,<9", optional = true }
[tool.poetry.group.dev.dependencies]
bip32utils = "^0.3.post4"
pytest = "*"
pytest-mypy = { version = "*", python = ">=3.9" }
black = ">=20"
flake8 = "*"
isort = "^5"
deepset = "^1"

[tool.poetry.extras]
cli = ["click"]

[tool.poetry.scripts]
shamir = "shamir_mnemonic.cli:cli"

[tool.black]
target-version = ['py36']

[tool.isort]
profile = "black"

[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
2 changes: 2 additions & 0 deletions shamir_mnemonic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
combine_mnemonics,
decode_mnemonics,
generate_mnemonics,
group_ems_mnemonics,
recover_ems,
split_ems,
)
Expand All @@ -18,6 +19,7 @@
"combine_mnemonics",
"decode_mnemonics",
"generate_mnemonics",
"group_ems_mnemonics",
"split_ems",
"recover_ems",
"EncryptedMasterSecret",
Expand Down
Loading