Skip to content

Commit

Permalink
Upgrade to shamir_mnemonic.group_ems_mnemonics for recover
Browse files Browse the repository at this point in the history
  • Loading branch information
pjkundert committed Nov 15, 2024
1 parent 32b9c93 commit 6ecfc0e
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 97 deletions.
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[pytest]
testpaths = slip39
addopts = -vv --capture=no --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc
addopts = -v --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ hdwallet @ git+https://github.com/pjkundert/python-hdwallet.git@python-slip39#eg
tabulate @ git+https://github.com/pjkundert/python-tabulate.git@python-slip39#egg=tabulate
mnemonic >=0.21, <1
qrcode >=7.3
shamir-mnemonic >=0.3.0,<0.4
#shamir-mnemonic >=0.3.0,<0.4
shamir-mnemonic @ git+https://github.com/pjkundert/python-shamir-mnemonic.git@python-slip39#egg=shamir-mnemonic
4 changes: 2 additions & 2 deletions slip39/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,7 @@ def create(
for g_nam,(g_of,g_mns) in groups.items() )
requires = f"Recover w/ {group_threshold} of {len(groups)} groups {commas( group_reqs )}"
for g_n,(g_name,(g_of,g_mnems)) in enumerate( groups.items() ):
log.info( f"{g_name}({g_of}/{len(g_mnems)}): {requires}" )
log.info( f"{g_name}({g_of}/{len(g_mnems)}): {'' if g_n else requires}" )
for mn_n,mnem in enumerate( g_mnems ):
for line,_ in organize_mnemonic( mnem, label=f"{ordinal(mn_n+1)} " ):
log.info( f"{line}" )
Expand Down Expand Up @@ -1192,7 +1192,7 @@ def mnemonics_encrypted(
grouped_shares = split_ems( group_threshold, groups, encrypted_secret )
log.warning(
f"Generated {len(encrypted_secret.ciphertext)*8}-bit SLIP-39 Mnemonics w/ identifier {encrypted_secret.identifier} requiring {group_threshold}"
f" of {len(grouped_shares)} {'(extendable)' if encrypted_secret.extendable else ''} groups to recover" )
f" of {len(grouped_shares)}{' (extendable)' if encrypted_secret.extendable else ''} groups to recover" )

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

Expand Down
74 changes: 34 additions & 40 deletions slip39/recovery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@
#
from __future__ import annotations

import itertools
import logging

from typing import List, Optional, Union, Tuple, Sequence
from typing import Dict, List, Optional, Union, Tuple, Sequence

from shamir_mnemonic import decode_mnemonics, recover_ems, EncryptedMasterSecret
from shamir_mnemonic import group_ems_mnemonics, EncryptedMasterSecret, Share, MnemonicError
from shamir_mnemonic.shamir import RANDOM_BYTES

from mnemonic import Mnemonic # Requires passphrase as str
from ..util import ordinal, commas
from ..util import commas
from ..defaults import BITS_DEFAULT
from .entropy import ( # noqa F401
shannon_entropy, signal_entropy, analyze_entropy, scan_entropy, display_entropy
Expand All @@ -40,45 +39,40 @@


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


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

# python-shamir-mnemonic requires passphrase as bytes (not str)
if passphrase is None:
passphrase = ""
passphrase_slip39 = b"" if using_bip39 else (
passphrase if isinstance( passphrase, bytes ) else passphrase.encode( 'UTF-8' )
)

secret = encrypted_secret.decrypt( passphrase_slip39 )

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

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

0 comments on commit 6ecfc0e

Please sign in to comment.