Skip to content

Commit 6ecfc0e

Browse files
committed
Upgrade to shamir_mnemonic.group_ems_mnemonics for recover
1 parent 32b9c93 commit 6ecfc0e

File tree

5 files changed

+278
-97
lines changed

5 files changed

+278
-97
lines changed

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[pytest]
22
testpaths = slip39
3-
addopts = -vv --capture=no --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc
3+
addopts = -v --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ hdwallet @ git+https://github.com/pjkundert/python-hdwallet.git@python-slip39#eg
99
tabulate @ git+https://github.com/pjkundert/python-tabulate.git@python-slip39#egg=tabulate
1010
mnemonic >=0.21, <1
1111
qrcode >=7.3
12-
shamir-mnemonic >=0.3.0,<0.4
12+
#shamir-mnemonic >=0.3.0,<0.4
13+
shamir-mnemonic @ git+https://github.com/pjkundert/python-shamir-mnemonic.git@python-slip39#egg=shamir-mnemonic

slip39/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,7 @@ def create(
11051105
for g_nam,(g_of,g_mns) in groups.items() )
11061106
requires = f"Recover w/ {group_threshold} of {len(groups)} groups {commas( group_reqs )}"
11071107
for g_n,(g_name,(g_of,g_mnems)) in enumerate( groups.items() ):
1108-
log.info( f"{g_name}({g_of}/{len(g_mnems)}): {requires}" )
1108+
log.info( f"{g_name}({g_of}/{len(g_mnems)}): {'' if g_n else requires}" )
11091109
for mn_n,mnem in enumerate( g_mnems ):
11101110
for line,_ in organize_mnemonic( mnem, label=f"{ordinal(mn_n+1)} " ):
11111111
log.info( f"{line}" )
@@ -1192,7 +1192,7 @@ def mnemonics_encrypted(
11921192
grouped_shares = split_ems( group_threshold, groups, encrypted_secret )
11931193
log.warning(
11941194
f"Generated {len(encrypted_secret.ciphertext)*8}-bit SLIP-39 Mnemonics w/ identifier {encrypted_secret.identifier} requiring {group_threshold}"
1195-
f" of {len(grouped_shares)} {'(extendable)' if encrypted_secret.extendable else ''} groups to recover" )
1195+
f" of {len(grouped_shares)}{' (extendable)' if encrypted_secret.extendable else ''} groups to recover" )
11961196

11971197
return [[share.mnemonic() for share in group] for group in grouped_shares]
11981198

slip39/recovery/__init__.py

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@
1616
#
1717
from __future__ import annotations
1818

19-
import itertools
2019
import logging
2120

22-
from typing import List, Optional, Union, Tuple, Sequence
21+
from typing import Dict, List, Optional, Union, Tuple, Sequence
2322

24-
from shamir_mnemonic import decode_mnemonics, recover_ems, EncryptedMasterSecret
23+
from shamir_mnemonic import group_ems_mnemonics, EncryptedMasterSecret, Share, MnemonicError
2524
from shamir_mnemonic.shamir import RANDOM_BYTES
2625

2726
from mnemonic import Mnemonic # Requires passphrase as str
28-
from ..util import ordinal, commas
27+
from ..util import commas
2928
from ..defaults import BITS_DEFAULT
3029
from .entropy import ( # noqa F401
3130
shannon_entropy, signal_entropy, analyze_entropy, scan_entropy, display_entropy
@@ -40,45 +39,40 @@
4039

4140

4241
def recover_encrypted(
43-
mnemonics: List[str],
44-
) -> Tuple[EncryptedMasterSecret, Sequence[int]]:
45-
"""Recover an encrypted SLIP-39 master secret Seed Entropy from the supplied SLIP-39 mnemonics.
46-
Returns the EncryptedMasterSecret, and the sequence of exactly which mnemonics were used to
47-
resolve it.
42+
mnemonics: Sequence[Union[str,Share]],
43+
strict: bool = False,
44+
) -> Tuple[EncryptedMasterSecret, Dict[int,Share], Sequence[int]]:
45+
"""Recover encrypted SLIP-39 master secret Seed Entropy and Group details from the supplied
46+
SLIP-39 mnemonics. Returns a sequence of EncryptedMasterSecret, and group Share detail that
47+
were used to resolve each one.
4848
49-
We cannot know what subset of these supplied mnemonics is required and/or valid, so we need to
50-
iterate over all subset combinations on failure; this allows us to recover from 1 (or more)
51-
incorrectly recovered SLIP-39 Mnemonics, using any others available.
49+
We cannot know in advance what subset of these supplied mnemonics is required and/or valid, so
50+
we need to iterate over all subset combinations on failure; this allows us to recover from 1 (or
51+
more) incorrectly recovered SLIP-39 Mnemonics, using any others available.
5252
53-
We'll try to find one of the smallest subsets that satisfies the SLIP-39 recovery.
53+
We'll try to find one of the smallest subset combinations that satisfies the SLIP-39 recovery.
5454
5555
Use this method to recover but NOT decrypt the SLIP-39 master secret Seed. Later, you may use
5656
this as a master_secret to slip39.create another set of SLIP-39 Mnemonics for this same
5757
passphrase-encrypted secret.
5858
5959
"""
60-
try:
61-
return recover_ems( decode_mnemonics( mnemonics )), range( len( mnemonics ))
62-
except Exception as exc:
63-
# Try subsets of the supplied mnemonics, to silently reject any invalid/redundant mnemonics
64-
for length in range( len( mnemonics )):
65-
for combo in itertools.combinations( range( len( mnemonics )), length ):
66-
try:
67-
return recover_ems( decode_mnemonics( set( mnemonics[i] for i in combo ) )), combo
68-
except Exception:
69-
pass
70-
# No recovery; raise the Exception produced by original attempt w/ all mnemonics
71-
raise exc
60+
for ems, groups in group_ems_mnemonics( mnemonics, strict=strict ):
61+
log.info(
62+
f"Recovered {len(ems.ciphertext)*8}-bit Encrypted SLIP-39 Seed Entropy using {len(groups)} groups comprising {sum(map(len,groups.values()))} mnemonics"
63+
)
64+
yield ems, groups
7265

7366

7467
def recover(
75-
mnemonics: List[str],
68+
mnemonics: List[Union[str,Share]],
7669
passphrase: Optional[Union[str,bytes]] = None,
7770
using_bip39: Optional[bool] = None, # If a BIP-39 "backup" (default: Falsey)
7871
as_entropy: Optional[bool] = None, # .. and recover original Entropy (not 512-bit Seed)
7972
language: Optional[str] = None, # ... provide BIP-39 language if not default 'english'
73+
strict: bool = True, # Fail if invalid Mnemonics are supplied
8074
) -> bytes:
81-
"""Recover, decrypt and return the secret seed Entropy encoded in the SLIP-39 Mnemonics.
75+
"""Recover, decrypt and return the (first) secret seed Entropy encoded in the SLIP-39 Mnemonics.
8276
8377
If not 'using_bip39', the resultant secret Entropy is returned as the Seed, optionally with (not
8478
widely used) SLIP-39 decryption with the given passphrase (empty, if None). We handle either
@@ -98,28 +92,28 @@ def recover(
9892
SLIP-39 Mnemomnic Cards, you are free to destroy your original insecure and unreliable BIP-39
9993
Mnemonic backup(s).
10094
101-
"""
102-
if passphrase is None:
103-
passphrase = ""
95+
If strict, we will fail if invalid Mnemonics are supplied; otherwise, they'll be ignored.
10496
105-
encrypted_secret, combo = recover_encrypted( mnemonics )
97+
"""
98+
try:
99+
encrypted_secret, groups = next( recover_encrypted( mnemonics, strict=strict ))
100+
except StopIteration:
101+
raise MnemonicError( "Invalid set of mnemonics; No encoded secret found" )
106102

107103
# python-shamir-mnemonic requires passphrase as bytes (not str)
104+
if passphrase is None:
105+
passphrase = ""
108106
passphrase_slip39 = b"" if using_bip39 else (
109107
passphrase if isinstance( passphrase, bytes ) else passphrase.encode( 'UTF-8' )
110108
)
111109

112110
secret = encrypted_secret.decrypt( passphrase_slip39 )
113111

114-
log.info(
115-
f"Recovered {len(secret)*8}-bit SLIP-39 Seed Entropy with {len(combo)}"
116-
f" ({'all' if len(combo) == len(mnemonics) else ', '.join( ordinal(i+1) for i in combo)})"
117-
f" of {len(mnemonics)} supplied mnemonics" + (
118-
f"; Seed decoded from SLIP-39 (w/ no passphrase) and generated using BIP-39 Mnemonic representation w/ {'a' if passphrase else 'no'} passphrase"
119-
if using_bip39 else
120-
f"; Seed decoded from SLIP-39 Mnemonics w/ {'a' if passphrase else 'no'} passphrase"
121-
)
122-
)
112+
log.info( "Seed decoded from SLIP-39" + (
113+
f" (w/ no passphrase) and generated using BIP-39 Mnemonic representation w/ {'a' if passphrase else 'no'} passphrase"
114+
if using_bip39 else
115+
f" Mnemonics w/ {'a' if passphrase else 'no'} passphrase"
116+
))
123117

124118
if using_bip39:
125119
# python-mnemonic's Mnemonic requires passphrase as str (not bytes). This is all a NO-OP,

0 commit comments

Comments
 (0)