1616#
1717from __future__ import annotations
1818
19- import itertools
2019import 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
2524from shamir_mnemonic .shamir import RANDOM_BYTES
2625
2726from mnemonic import Mnemonic # Requires passphrase as str
28- from ..util import ordinal , commas
27+ from ..util import commas
2928from ..defaults import BITS_DEFAULT
3029from .entropy import ( # noqa F401
3130 shannon_entropy , signal_entropy , analyze_entropy , scan_entropy , display_entropy
4039
4140
4241def 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
7467def 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