Skip to content

Commit 685e9cd

Browse files
committed
Initial integration of entropy analysis into slip39.gui
1 parent 98409d1 commit 685e9cd

File tree

5 files changed

+240
-95
lines changed

5 files changed

+240
-95
lines changed

slip39/gui/main.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import PySimpleGUI as sg
1414

1515
from ..api import Account, create, group_parser, random_secret, cryptopaths_parser, paper_wallet_available
16-
from ..recovery import recover, recover_bip39, produce_bip39
16+
from ..recovery import recover, recover_bip39, produce_bip39, analyze_entropy
1717
from ..util import log_level, log_cfg, ordinal, chunker, hue_shift
1818
from ..layout import write_pdfs, printers_available
1919
from ..defaults import (
@@ -455,21 +455,11 @@ def update_seed_data( event, window, values ):
455455
Stores the last known state of the -SD-... radio buttons, and saves/restores the user data being
456456
supplied on for BIP/SLIP/FIX. If event indicates one of our radio-buttons is re-selectedd, then
457457
also re-generate the random data. If a system event occurs (eg. after a Controls change),
458-
restores our last-known radio button and data.
458+
restores our last-known radio button and data. Since we cannot know if/when our main window is
459+
going to disappear and be replaced, we constantly save the current state.
459460
460461
"""
461-
changed = False
462-
if not event.startswith( '-' ) and update_seed_data.src and window[update_seed_data.src].visible:
463-
# Some system event; restore where we left off (if known, and if the remembered control is visible).
464-
data, pswd = update_seed_data.was.get( update_seed_data.src, ('','') )
465-
seed = None
466-
values[update_seed_data.src] = True
467-
window[update_seed_data.src].update( True )
468-
update_seed_data.src = None
469-
else:
470-
data, pswd = values['-SD-DATA-'], values['-SD-PASS-']
471-
seed = window['-SD-SEED-'].get()
472-
for src in [
462+
SD_CONTROLS = [
473463
'-SD-128-RND-',
474464
'-SD-256-RND-',
475465
'-SD-512-RND-',
@@ -479,15 +469,34 @@ def update_seed_data( event, window, values ):
479469
'-SD-BIP-', # Recover 128- to 256-bit Mnemonic Seed Entropy
480470
'-SD-BIP-SEED-', # Recover 512-bit Generated Seed w/ passphrase
481471
'-SD-SLIP-',
482-
]:
483-
# See what we got for -SD-DATA-, for this -SD-... radio button selection
484-
if values[src] and update_seed_data.src != src:
485-
# If selected radio-button for Seed Data source changed, save last source's working data
486-
# and restore what was, last time we were working on this source. If the triggering
487-
# event is not one of ours (eg. __TIMEOUT__), then don't overwrite memory.
488-
if update_seed_data.src and event.startswith( '-' ):
489-
#log.warning( f"Switching Seed Source w/ event {event} to {src}: saving {update_seed_data.src}: {data!r:.10}..., {pswd!r}" )
490-
update_seed_data.was[update_seed_data.src] = data, pswd
472+
]
473+
changed = False
474+
# Some system event (eg. __TIMEOUT__ immediately after a new main window is created, or due to
475+
# scheduled callback); restore where we left off if known, and if the remembered control is
476+
# visible. This allows continuation between major screen changes w/ full controls regeneration.
477+
# As long as the new screen contains the same visible controls, we'll continue editing our Seed
478+
# Data wherever we left off. If the new screen has different controls, we'll get whatever is
479+
# there, instead.
480+
if not event.startswith( '-' ) and update_seed_data.src and window[update_seed_data.src].visible:
481+
log.warning( f"Restoring Seed Source w/ event {event} from saved {update_seed_data.src}"
482+
f" data/password{' (none saved)' if update_seed_data.src not in update_seed_data.was else ''}" )
483+
data, pswd = update_seed_data.was.get( update_seed_data.src, ('','') )
484+
seed = None
485+
for src in SD_CONTROLS:
486+
values[src] = False
487+
values[update_seed_data.src] = True
488+
window[update_seed_data.src].update( True )
489+
update_seed_data.src = None # and force controls' visibility update
490+
else:
491+
data, pswd = values['-SD-DATA-'], values['-SD-PASS-']
492+
seed = window['-SD-SEED-'].get()
493+
# Now that we've recovered which Seed Data control is in play, See what we got for -SD-DATA-,
494+
# and update other controls' visibility for this -SD-... radio button selection.
495+
for src in SD_CONTROLS:
496+
if values[src] and ( update_seed_data.src != src ):
497+
# If selected radio-button for Seed Data source changed, restore what data/pswd was
498+
# there, last time we were working on this source.
499+
log.warning( f"Seed Data source changed from {update_seed_data.src!r} to {src!r} due to event: {event!r}" )
491500
changed = True
492501
update_seed_data.src = src
493502
data, pswd = update_seed_data.was.get( src, ('','') )
@@ -530,6 +539,7 @@ def update_seed_data( event, window, values ):
530539
window['-SD-PASS-F-'].update( visible=False )
531540
elif event == update_seed_data.src == src:
532541
# Same radio-button re-selected; just force an update (eg. re-generate random)
542+
log.info( f"Seed Data update forced due to event: {event!r}" )
533543
changed = True
534544
if not seed:
535545
seed = '-' * ( BITS_DEFAULT // 4 )
@@ -585,7 +595,7 @@ def update_seed_data( event, window, values ):
585595
seed = random_secret( bits // 8 )
586596

587597
# Compute any newly computed/recovered binary Seed Data bytes as hex. Must be 128-, 256- or
588-
# 512-bit hex data.
598+
# 512-bit hex data. Do a final comparison against current -SD-SEED- to detect changes.
589599
try:
590600
if type(seed) is not str:
591601
seed = codecs.encode( seed, 'hex_codec' ).decode( 'ascii' )
@@ -600,11 +610,30 @@ def update_seed_data( event, window, values ):
600610
if not bits:
601611
bits = BITS_DEFAULT
602612
seed = '-' * (bits // 4)
603-
613+
if window['-SD-SEED-'].get() != seed:
614+
changed = True
615+
616+
# Analyze the seed for Signal harmonic or Shannon entropy failures, if we're in a __TIMEOUT__
617+
# (between keystrokes or after a major controls change). Otherwise, if the seed's changed,
618+
# request a __TIMEOUT__; when it, perform the entropy analysis.
619+
values['-SD-SIG-'] = ''
620+
if status is None and event == '__TIMEOUT__':
621+
seed_bytes = codecs.decode( seed, 'hex_codec' )
622+
analysis = analyze_entropy( seed_bytes, what=f"{len(seed_bytes)*8}-bit Seed Source", show_details=False )
623+
if analysis:
624+
values['-SD-SIG-'] = analysis
625+
status = analysis.split( '\n' )[0]
626+
elif changed:
627+
log.info( f"Seed Data requests __TIMEOUT__ w/ current source: {update_seed_data.src!r}" )
628+
values['__TIMEOUT__'] = .5
629+
604630
# Since a window[...].update() doesn't show up to a .get() 'til the next cycle of the display,
605631
# we'll communicate updates to successive functions via values.
606632
values['-SD-SEED-'] = seed
607633
window['-SD-SEED-'].update( seed )
634+
635+
# Finally, remember what data/pswd we were working on, in case we get a major controls change.
636+
update_seed_data.was[update_seed_data.src] = values['-SD-DATA-'], values['-SD-PASS-']
608637
return status
609638

610639
update_seed_data.src = None # noqa: E305
@@ -902,9 +931,10 @@ def app(
902931
master_secret = None # default to produce randomly
903932
details = None # The SLIP-39 details produced from groups; make None to force SLIP-39 Mnemonic update
904933
cryptopaths = None
905-
timeout = 0 # First time thru; refresh immediately
934+
timeout = 0 # First time thru; refresh immediately; functions req. refresh may adjust via values['__TIMEOUT__']
906935
instructions = '' # The last instructions .txt payload found
907936
instructions_kwds = dict() # .. and its colors
937+
values = dict()
908938
while event not in events_termination:
909939
# A Controls layout selection, eg. '-LO-2-'; closes and re-generates window layout
910940
if event and event.startswith( '-LO-' ):
@@ -951,12 +981,15 @@ def app(
951981
no_titlebar = no_titlebar,
952982
scaling = scaling,
953983
)
954-
timeout = 0 # First time through w/ new window, refresh immediately
984+
values['__TIMEOUT__'] = 0 # First time through w/ new window, refresh immediately
955985

956-
# Block (except for first loop) and obtain current event and input values. Until we get a
957-
# new event, retain the current status
986+
# Block (except for first loop, or if someone requested a __TIMEOUT__) and obtain current
987+
# event and input values. Until we get a new event, retain the current status
988+
timeout = values.get( '__TIMEOUT__', None ) # Subsequently, default; block indefinitely (functions may adjust...)
989+
if timeout:
990+
logging.debug( f"A __TIMEOUT__ was requested: {timeout!r}" )
958991
event, values = window.read( timeout=timeout )
959-
timeout = None # Subsequently, default; block indefinitely
992+
assert '__TIMEOUT__' not in values
960993
logging.debug( f"{event}, {values}" )
961994
if not values or event in events_termination or event in events_ignored:
962995
continue

slip39/recovery/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ def recover(
8484
f"Recovered {len(secret)*8}-bit SLIP-39 Seed Entropy with {len(combo)}"
8585
f" ({'all' if len(combo) == len(mnemonics) else ', '.join( ordinal(i+1) for i in combo)})"
8686
f" of {len(mnemonics)} supplied mnemonics" + (
87-
"; Seed generated using BIP-39 Mnemonic representation w/ passphrase"
87+
f"; Seed decoded from SLIP-39 (w/ no passphrase) and generated using BIP-39 Mnemonic representation w/ {'a' if passphrase else 'no'} passphrase"
8888
if using_bip39 else
89-
"; Seed decoded from SLIP-39 Mnemonics w/ passphrase"
89+
f"; Seed decoded from SLIP-39 Mnemonics w/ {'a' if passphrase else 'no'} passphrase"
9090
)
9191
)
9292
if using_bip39:

0 commit comments

Comments
 (0)