Skip to content

Commit

Permalink
Merge branch 'feature/extendable'; v13.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
pjkundert committed Dec 8, 2024
2 parents 012598e + a427590 commit fa02f5f
Show file tree
Hide file tree
Showing 14 changed files with 531 additions and 334 deletions.
309 changes: 156 additions & 153 deletions README.org

Large diffs are not rendered by default.

Binary file modified README.pdf
Binary file not shown.
315 changes: 162 additions & 153 deletions README.txt

Large diffs are not rendered by default.

Binary file modified images/SLIP39-Example.pdf
Binary file not shown.
24 changes: 16 additions & 8 deletions slip39/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from shamir_mnemonic import EncryptedMasterSecret, split_ems
from shamir_mnemonic.shamir import _random_identifier, RANDOM_BYTES
from shamir_mnemonic.constants import ID_LENGTH_BITS

import hdwallet
from hdwallet import cryptocurrencies
Expand Down Expand Up @@ -981,15 +982,16 @@ def group_parser( group_spec ):

def create(
name: str,
group_threshold: Optional[Union[int,float]] = None, # Default: 1/2 of groups, rounded up
group_threshold: Optional[Union[int,float]] = None, # Default: 1/2 of groups, rounded up
groups: Optional[Union[List[str],Dict[str,Tuple[int, int]]]] = None, # Default: 4 groups (see defaults.py)
master_secret: Optional[Union[str,bytes]] = None, # Default: generate 128-bit Seed Entropy
master_secret: Optional[Union[str,bytes]] = None, # Default: generate 128-bit Seed Entropy
passphrase: Optional[Union[bytes,str]] = None,
using_bip39: Optional[bool] = None, # Produce wallet Seed from master_secret Entropy using BIP-39 generation
iteration_exponent: int = 1,
cryptopaths: Optional[Sequence[Union[str,Tuple[str,str],Tuple[str,str,str]]]] = None, # default: ETH, BTC at default path, format
strength: Optional[int] = None, # Default: 128
extendable: Optional[Union[bool,int]] = None, # Default: True w/ random identifier
strength: Optional[int] = None, # Default: 128
extendable: Optional[bool] = None, # Default: True
identifier: Optional[int] = None, # Default: random identifier
) -> Tuple[str,int,Dict[str,Tuple[int,List[str]]], Sequence[Sequence[Account]], bool]:
"""Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
accounts. Returns the Details, in a form directly compatible with the layout.produce_pdf API.
Expand Down Expand Up @@ -1092,7 +1094,8 @@ def create(
master_secret = master_secret,
passphrase = passphrase,
iteration_exponent= iteration_exponent,
extendable = extendable
extendable = extendable,
identifier = identifier,
)

groups = {
Expand Down Expand Up @@ -1120,7 +1123,8 @@ def mnemonics(
passphrase: Optional[Union[bytes,str]] = None,
iteration_exponent: int = 1,
strength: int = BITS_DEFAULT,
extendable: Optional[Tuple[bool,int]] = None,
extendable: Optional[bool] = None, # Default: True
identifier: Optional[int] = None, # Default: random identifier
) -> List[List[str]]:
"""Generate SLIP39 mnemonics for the supplied master_secret for group_threshold of the given
groups. Will generate a random master_secret, if necessary.
Expand Down Expand Up @@ -1151,11 +1155,15 @@ def mnemonics(
passphrase = ""
if isinstance( passphrase, str ):
passphrase = passphrase.encode( 'UTF-8' )
extendable = False if extendable is False else True
identifier = _random_identifier() if identifier is None else int( identifier )
assert isinstance( identifier, int) and 0 <= identifier < (1 << ID_LENGTH_BITS), \
"Identifier must be an {ID_LENGTH_BITS}-bit unsigned integer: {identifier}"
encrypted_secret = EncryptedMasterSecret.from_master_secret(
master_secret = master_secret,
passphrase = passphrase,
identifier = _random_identifier() if extendable in (None, False, True) else extendable,
extendable = False if extendable is False else True,
identifier = identifier,
extendable = extendable,
iteration_exponent = iteration_exponent,
)

Expand Down
60 changes: 60 additions & 0 deletions slip39/gui/SLIP-39-EXTENDABLE.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#+title: Extendable
#+OPTIONS: toc:nil title:nil author:nil

#+BEGIN_ABSTRACT
SLIP-39 Mnemonics recover a unique (but "valid") Seed and derived wallets, no matter what
/alternative/ passphrase you use! Your /original/ Seed can only be recovered with the originally
specified "correct" passphrase.

Extendable SLIP-39 Mnemonics ensures that all SLIP-39 Mnemonic sets generated from the /original/
Seed and original "correct" passphrase will /always/ result in the same unique Seed for each
/alternative/ passphrase.

Non-Extendable SLIP-39 Mnemonics recover the /original/ Seed with the "correct" passphrase, but
*different* unique Seeds for all /alternative/ passphrases.
#+END_ABSTRACT

* Extendable

The default is now /Extendable/ -- does /not/ use the Identifier to salt the encryption passphrase.

** The Purpose for Multiple Passphrases

Recovering different Seeds for different passphrases is a valuable feature, because you may use
the same SLIP-39 Mnemonic cards, and supply different passphrases to recover different (but valid)
Seeds and sets of derived HD wallets!

- You could have a "distress" passphrase that recovers a decoy wallet containing a
small amount of sacrificial funds, while your real savings are under a different passphrase.
- One password for your personal accounts and another for business accounts.

** Non-Extendable Encoding

Historically, the SLIP-39 encoding used the randomly assigned Identifier to both 1) associate groups
of Mnemonics belonging to the same set, but /also/ 2) to salt the Seed encryption.

This meant that: if you created 2 sets of SLIP-39 Mnemonics for the same Seed -- each set would
lead to */same/* Seed with the "correct" original passphrase, but to */different/* Seeds with
each "distress" passphrase!

Unless all sets of SLIP-39 Mnemonics lead to the same Seeds for each passphrase, you are
restricted to ever issue /only one/ set of SLIP-39 Mnemonics for each Seed! You lose the ability
to recover other "distress" passphrase Seeds from the new sets of Mnemonics!

** Issuing Multiple SLIP-39 Mnemonic Sets

You may want to issue a simple set of SLIP-39 Mnemonics for your Seed to begin with, and then
(later) decide to issue a more elaborate set of SLIP-39 Mnenmonic cards.

Only with Extendable SLIP-39 Mnemonics, will the /alternative/ passphrase Seeds and derived
wallets be consistent.

* Recovery

The SLIP-39 App supports recovery from both Extendable and (historic) non-Extendable SLIP-39
Mnemonics.

** Using [[https://iancoleman.io/slip39]]

Until the website is updated, you cannot (as of Dec 2024) use it to recover your Seed from
Extendable SLIP-39 Mnemonics.
78 changes: 78 additions & 0 deletions slip39/gui/SLIP-39-EXTENDABLE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
SLIP-39 Mnemonics recover a unique (but "valid") Seed and derived
wallets, no matter what /alternative/ passphrase you use! Your
/original/ Seed can only be recovered with the originally specified
"correct" passphrase.

Extendable SLIP-39 Mnemonics ensures that all SLIP-39 Mnemonic sets
generated from the /original/ Seed and original "correct" passphrase
will /always/ result in the same unique Seed for each /alternative/
passphrase.

Non-Extendable SLIP-39 Mnemonics recover the /original/ Seed with the
"correct" passphrase, but *different* unique Seeds for all /alternative/
passphrases.


1 Extendable
════════════

The default is now /Extendable/ – does /not/ use the Identifier to
salt the encryption passphrase.


1.1 The Purpose for Multiple Passphrases
────────────────────────────────────────

Recovering different Seeds for different passphrases is a valuable
feature, because you may use the same SLIP-39 Mnemonic cards, and
supply different passphrases to recover different (but valid) Seeds
and sets of derived HD wallets!

• You could have a "distress" passphrase that recovers a decoy wallet
containing a small amount of sacrificial funds, while your real
savings are under a different passphrase.
• One password for your personal accounts and another for business
accounts.


1.2 Non-Extendable Encoding
───────────────────────────

Historically, the SLIP-39 encoding used the randomly assigned
Identifier to both 1) associate groups of Mnemonics belonging to the
same set, but /also/ 2) to salt the Seed encryption.

This meant that: if you created 2 sets of SLIP-39 Mnemonics for the
same Seed – each set would lead to */same/* Seed with the "correct"
original passphrase, but to */different/* Seeds with each "distress"
passphrase!

Unless all sets of SLIP-39 Mnemonics lead to the same Seeds for each
passphrase, you are restricted to ever issue /only one/ set of SLIP-39
Mnemonics for each Seed! You lose the ability to recover other
"distress" passphrase Seeds from the new sets of Mnemonics!


1.3 Issuing Multiple SLIP-39 Mnemonic Sets
──────────────────────────────────────────

You may want to issue a simple set of SLIP-39 Mnemonics for your Seed
to begin with, and then (later) decide to issue a more elaborate set
of SLIP-39 Mnenmonic cards.

Only with Extendable SLIP-39 Mnemonics, will the /alternative/
passphrase Seeds and derived wallets be consistent.


2 Recovery
══════════

The SLIP-39 App supports recovery from both Extendable and (historic)
non-Extendable SLIP-39 Mnemonics.


2.1 Using <https://iancoleman.io/slip39>
────────────────────────────────────────

Until the website is updated, you cannot (as of Dec 2024) use it to
recover your Seed from Extendable SLIP-39 Mnemonics.
2 changes: 1 addition & 1 deletion slip39/gui/SLIP-39-SE-SIGS.org
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#+OPTIONS: toc:nil title:nil author:nil

#+BEGIN_ABSTRACT
Bad Entropy is risk to Cryptocurrency HD Wallet Seed Secrets!
Bad Entropy is a risk to Cryptocurrency HD Wallet Seed Secrets!

Avoid Harmonic and Shannon Entropy Deficiencies:
- Use strong cryptographically secure randomness for your Seed Data
Expand Down
2 changes: 1 addition & 1 deletion slip39/gui/SLIP-39-SE-SIGS.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Bad Entropy is risk to Cryptocurrency HD Wallet Seed Secrets!
Bad Entropy is a risk to Cryptocurrency HD Wallet Seed Secrets!

Avoid Harmonic and Shannon Entropy Deficiencies:
• Use strong cryptographically secure randomness for your Seed Data
Expand Down
27 changes: 19 additions & 8 deletions slip39/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"academic acid beard romp believe impulse species holiday demand building" \
" earth warn lunar olympic clothes piece campus alpha short endless"

SD_SEED_FRAME = 'Seed Source: Create your Seed Entropy here'
SD_SEED_FRAME = 'Seed Source: Create/Recover Seed Entropy here'
SE_SEED_FRAME = 'Seed Extra Randomness'
SS_SEED_FRAME = 'Seed Secret & SLIP-39 Recovery Groups'

Expand Down Expand Up @@ -269,16 +269,17 @@ def groups_layout(
],
] + [
[
# SLIP-39 only available in Recovery; SLIP-39 Passphrase only in Pro; BIP-39 and Fixed Hex only in Pro
# SLIP-39 only available in Recovery; SLIP-39 Passphrase only in Pro
sg.Frame( SD_SEED_FRAME, [
[
sg.Text( "Random:" if not LO_BAK else "Source:", visible=LO_CRE, **T_hue( T_kwds, 0/20 )),
sg.Radio( "128-bit", "SD", key='-SD-128-RND-', default=LO_CRE,
sg.Radio( "128-bit", "SD", key='-SD-128-RND-', visible=LO_CRE, **T_hue( B_kwds, 0/20 )),
sg.Radio( "256-bit", "SD", key='-SD-256-RND-', default=LO_CRE and not LO_REC,
visible=LO_CRE, **T_hue( B_kwds, 0/20 )),
sg.Radio( "256-bit", "SD", key='-SD-256-RND-', visible=LO_CRE, **T_hue( B_kwds, 0/20 )),
sg.Radio( "512-bit", "SD", key='-SD-512-RND-', visible=LO_PRO, **T_hue( B_kwds, 0/20 )),
sg.Text( "Recover:", visible=LO_CRE, **T_hue( T_kwds, 2/20 )),
sg.Radio( "SLIP-39", "SD", key='-SD-SLIP-', visible=LO_REC, **T_hue( B_kwds, 2/20 )),
sg.Radio( "SLIP-39", "SD", key='-SD-SLIP-', default=LO_REC,
visible=LO_REC, **T_hue( B_kwds, 2/20 )),
sg.Radio( "BIP-39", "SD", key='-SD-BIP-', default=LO_BAK,
visible=LO_CRE, **T_hue( B_kwds, 2/20 )),
sg.Radio( "BIP-39 Seed", "SD", key='-SD-BIP-SEED-', visible=LO_PRO, **T_hue( B_kwds, 2/20 )),
Expand Down Expand Up @@ -367,6 +368,9 @@ def groups_layout(
sg.Text( f"of {len(groups)}", key='-RECOVERY-', **T_kwds ),
sg.Button( '+', **B_kwds ),
sg.Text( "Mnemonic Card Groups", **T_kwds ),
sg.Checkbox( "Extendable", key='-EXTENDABLE-', visible=LO_CRE,
default=True,
size=prefix, **T_hue( B_kwds, +1/20 )),
],
group_body,
] ),
Expand Down Expand Up @@ -595,12 +599,12 @@ def update_seed_data( event, window, values ):
# We're recovering the BIP-39 Seed Phrase *Entropy*, NOT the derived (decrypted) 512-bit
# Seed Data! So, we don't deal in Passphrases, here. The Passphrase (to encrypt the Seed,
# when "Using BIP-39") is only required to display the correct wallet addresses.
window['-SD-DATA-F-'].update( "BIP-39 Mnemonic to Back Up: " )
window['-SD-DATA-F-'].update( "BIP-39 Mnemonic to Recover or Back Up: " )
window['-SD-DATA-F-'].update( visible=True )
window['-SD-PASS-C-'].update( visible=False )
window['-SD-PASS-F-'].update( visible=False )
elif 'SLIP' in update_seed_data.src:
window['-SD-DATA-F-'].update( "SLIP-39 Mnemonics to Back Up: " )
window['-SD-DATA-F-'].update( "SLIP-39 Mnemonics to Recover or Back Up: " )
window['-SD-DATA-F-'].update( visible=True )
window['-SD-PASS-C-'].update( visible=True )
window['-SD-PASS-F-'].update(
Expand Down Expand Up @@ -1073,6 +1077,7 @@ def app(
events_ignored = ('-MNEMONICS-'+sg.WRITE_ONLY_KEY,)
master_secret = None # default to produce randomly
details = None # The SLIP-39 details produced from groups; make None to force SLIP-39 Mnemonic update
extendable = True
cryptopaths = None
timeout = 0 # First time thru; refresh immediately; functions req. refresh may adjust via values['__TIMEOUT__']
instructions = '' # The last instructions .txt payload found
Expand Down Expand Up @@ -1441,6 +1446,11 @@ def deficiency( *deficiencies ):
# We avoid recomputing this unless something about the seed or the recovered groups changes;
# each time we recompute -- even without any changes -- the SLIP-39 Mnemonics will change,
# due to the use of entropy in the SLIP-39 process.
extendable_rec = values['-EXTENDABLE-']
if extendable_rec != extendable:
extendable = extendable_rec
details = None

if not details or names[0] not in details:
log.info( f"SLIP39 details for {names}..." )
try:
Expand All @@ -1457,6 +1467,7 @@ def deficiency( *deficiencies ):
passphrase = passphrase,
using_bip39 = using_bip39,
cryptopaths = cryptopaths,
extendable = extendable,
)
except Exception as exc:
status = f"Error creating: {exc}"
Expand Down Expand Up @@ -1532,7 +1543,7 @@ def deficiency( *deficiencies ):
continue
name_len = max( len( name ) for name in details )
status = '\n'.join(
f"Saved {name}"
f"{'Print' if printer else 'Saved'}: {name}"
for name in details
)
# Finally, success has been assured; turn off emboldened status line
Expand Down
19 changes: 15 additions & 4 deletions slip39/layout/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ def produce_pdf(

# Find the best PDF and orientation, by max of returned cards_pp (cards per page). Assumes
# layout_pdf returns a tuple that can be compared; cards_pp,orientation,... will always sort.
# This card orientation defines the overall orientation of the entire PDF; we'll rotate other
# elements (cover, paper wallets) to match.
page_margin_mm = PAGE_MARGIN * MM_IN
cards_pp,orientation,page_xy,pdf,_ = max(
layout_pdf( card_dim, page_margin_mm, orientation=orientation, paper_format=paper_format )
Expand Down Expand Up @@ -335,6 +337,8 @@ def produce_pdf(
cover_sent += "\n".join( slip39_group )
tpl_cover['cover-sent'] = cover_sent

# Add the cover page(s), rotated to match the orientation deduced for the cards. Thus, the
# whole PDF will be printable with the deduced card page orientation.
pdf.add_page()
if orientation.lower().startswith( 'p' ):
tpl_cover.render( offsetx=pdf.epw, rotate=-90.0 )
Expand Down Expand Up @@ -427,6 +431,8 @@ def write_pdfs(
cover_page = True, # Produce a cover page (including BIP-39 Mnemonic, if using_bip39)
watermark = None, # Any watermark desired on each output
double_sided = None,
extendable = None, # Default: True
identifier = None, # Default: random identifier
):
"""Writes a PDF containing a unique SLIP-39 encoded Seed Entropy for each of the names specified.
Expand Down Expand Up @@ -478,6 +484,8 @@ def write_pdfs(
passphrase = passphrase.encode( 'UTF-8' ) if passphrase else b'',
using_bip39 = using_bip39, # Derive wallet Seed using BIP-39 Mnemonic + passphrase generation
cryptopaths = cryptopaths,
extendable = extendable,
identifier = identifier,
)
for name in names or [ "SLIP39" ]
}
Expand Down Expand Up @@ -520,7 +528,9 @@ def write_pdfs(

# Unless no card_format (False) or paper wallet password specified, produce a PDF containing
# the SLIP-39 mnemonic recovery cards; remember the deduced (<pdf_paper>,<pdf_orient>). If
# we're producing paper wallets, always force portrait orientation for the cards, to match.
# we're producing paper wallets, always force portrait orientation for the cards, to match
# the layout of the paper wallets, so that all page orientations will match; this may result
# in a sub-optimal card layout, but mixing orientations in a PDF confuses some printers.
if card_format is not False or wallet_pwd is not None:
(pdf_paper,pdf_orient),pdf,_ = produce_pdf(
*details,
Expand All @@ -547,7 +557,8 @@ def write_pdfs(

if wallet_pwd is not None:
# Deduce the paper wallet size and create a template. All layouts are in specified in
# inches; template dimensions are in mm.
# inches; template dimensions are in mm. Paper wallets are always formatted for
# portrait orientation; we've forced that, above, if paper wallets are desired.
try:
(wall_h,wall_w),wall_margin = WALLET_SIZES[wallet_format.lower() if wallet_format else WALLET]
except KeyError:
Expand All @@ -557,8 +568,8 @@ def write_pdfs(
wall_dim = wall.mm()

# Lay out wallets, always in Portrait orientation, defaulting to the Card paper_format
# if it is a standard size (a str, not an (x,y) tuple), otherwise to "Letter" paper. Printers may
# have problems with a PDF mixing Landscape and Portrait, but do it if desired...
# if it is a standard size (a str, not an (x,y) tuple), otherwise to "Letter" paper.
# Rotate the wallets to match the card's orientation (like the cover page).
if wallet_paper is None:
wallet_paper = paper_format if type(paper_format) is str else PAPER

Expand Down
Loading

0 comments on commit fa02f5f

Please sign in to comment.