Skip to content

Commit 07d56ac

Browse files
committed
Rudimentary integration of signal analysis with Seed Source
1 parent ff92bbc commit 07d56ac

File tree

5 files changed

+97
-33
lines changed

5 files changed

+97
-33
lines changed

slip39/gui/main.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
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, analyze_entropy
17-
from ..util import log_level, log_cfg, ordinal, chunker, hue_shift
16+
from ..recovery import recover, recover_bip39, produce_bip39, scan_entropy, display_entropy
17+
from ..util import log_level, log_cfg, ordinal, chunker, hue_shift, rate_dB
1818
from ..layout import write_pdfs, printers_available
1919
from ..defaults import (
2020
GROUPS, GROUP_THRESHOLD_RATIO, MNEM_PREFIX, CRYPTO_PATHS, BITS, BITS_DEFAULT,
@@ -34,6 +34,9 @@
3434
" earth warn lunar olympic clothes piece campus alpha short endless"
3535

3636

37+
SD_SEED_FRAME = 'Seed Source: Input or Create your Seed Entropy here'
38+
39+
3740
def theme_color( thing, theme=None ):
3841
"""Get the currency configured PySimpleGUI Theme color for thing == eg. "TEXT", "BACKGROUND.
3942
"""
@@ -211,7 +214,7 @@ def groups_layout(
211214
] + [
212215
[
213216
# SLIP-39 only available in Recovery; SLIP-39 Passphrase only in Pro; BIP-39 and Fixed Hex only in Pro
214-
sg.Frame( 'Seed Source: Input or Create your Seed Entropy here', [
217+
sg.Frame( SD_SEED_FRAME, [
215218
[
216219
sg.Text( "Random:" if not LO_BAK else "Source:", visible=LO_CRE, **T_hue( T_kwds, 0/20 )),
217220
sg.Radio( "128-bit", "SD", key='-SD-128-RND-', default=LO_CRE,
@@ -458,6 +461,7 @@ def update_seed_data( event, window, values ):
458461
restores our last-known radio button and data. Since we cannot know if/when our main window is
459462
going to disappear and be replaced, we constantly save the current state.
460463
464+
Reports the quality of the Seed Data in the frame label.
461465
"""
462466
SD_CONTROLS = [
463467
'-SD-128-RND-',
@@ -615,14 +619,18 @@ def update_seed_data( event, window, values ):
615619

616620
# Analyze the seed for Signal harmonic or Shannon entropy failures, if we're in a __TIMEOUT__
617621
# (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-'] = ''
622+
# request a __TIMEOUT__; when it invokes, perform the entropy analysis.
623+
values['-SD-SIGS-'] = ''
624+
620625
if status is None and event == '__TIMEOUT__':
621626
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 )
627+
signals, shannons = scan_entropy( seed_bytes, show_details=True )
628+
analysis = display_entropy( signals, shannons, what=f"{len(seed_bytes)*8}-bit Seed Source" )
623629
if analysis:
624630
values['-SD-SIG-'] = analysis
625631
status = analysis.split( '\n' )[0]
632+
window['-SD-SEED-F-'].update(
633+
f"{SD_SEED_FRAME}; {rate_dB( max( signals ).dB, what='Harmonics')}, {rate_dB( max( signals ).dB, what='Shannon')}, " )
626634
elif changed:
627635
log.info( f"Seed Data requests __TIMEOUT__ w/ current source: {update_seed_data.src!r}" )
628636
values['__TIMEOUT__'] = .5
@@ -1001,7 +1009,7 @@ def app(
10011009
# specific instructional .txt we can load. Only if the current instructions is empty will
10021010
# we go all the way back to load the generic SLIP-39.txt. If the event corresponds to an
10031011
# object with text/backround_color, use it in the instructional text.
1004-
txt_segs = ( event or '' ).strip('-').split( '-' )
1012+
txt_segs = ( event or '' ).strip( '-' ).split( '-' )
10051013
for txt_i in range( len( txt_segs ), 0 if instructions else -1, -1 ):
10061014
txt_name = '-'.join( [ 'SLIP', '39' ] + txt_segs[:txt_i] ) + '.txt'
10071015
txt_path = os.path.join( os.path.dirname( __file__ ), txt_name )

slip39/recovery/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525

2626
from ..util import ordinal
2727

28-
from .entropy import shannon_entropy, signal_entropy, analyze_entropy # noqa F401
28+
from .entropy import ( # noqa F401
29+
shannon_entropy, signal_entropy, analyze_entropy, scan_entropy, display_entropy
30+
)
2931

3032
log = logging.getLogger( __package__ )
3133

slip39/recovery/entropy.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,30 @@ def scan_entropy(
10191019
return signals, shannons
10201020

10211021

1022+
def display_entropy( signals, shannons, what=None ):
1023+
result = None
1024+
if signals or shannons:
1025+
report = ''
1026+
dBs = sorted( list( s.dB for s in signals ) + list( s.dB for s in shannons) )
1027+
dBr = dBs[:1] + dBs[max( 1, len(dBs)-1):]
1028+
report += f"Entropy analysis {('of ' + what) if what else ''}: {len(signals)+len(shannons)}x"
1029+
report += f" {'-'.join( f'{dB:.1f}' for dB in dBr )}dB non-random energy patterns in"
1030+
report += f" {commas( sorted( set( s.stride for s in signals )), final_and=True)}-bit symbols\n"
1031+
for s,summary in sorted(
1032+
[
1033+
(s, f"{s.dB:5.1f}dB Signal harmonic feature at offset {s.offset} in {s.symbols}x {s.stride}-bit symbols")
1034+
for s in signals
1035+
] + [
1036+
(s, f"{s.dB:5.1f}dB Shannon entropy reduced at offset {s.offset} in {s.symbols}x {s.stride}-bit symbols")
1037+
for s in shannons
1038+
], reverse=True ):
1039+
report += f"-{summary}{': ' if s.details else ''}\n"
1040+
if s.details:
1041+
report += f"{s.details}\n"
1042+
result = report
1043+
return result
1044+
1045+
10221046
def analyze_entropy(
10231047
entropy: bytes,
10241048
strides: Optional[Union[int,Tuple[int,int]]] = None, # If only a specific stride/s makes sense, eg. for ASCII symbols
@@ -1052,27 +1076,9 @@ def analyze_entropy(
10521076
0.25% failure on each individual test.
10531077
10541078
"""
1055-
signals, shannons = scan_entropy(
1056-
entropy, strides, overlap, ignore_dc=ignore_dc, show_details=show_details,
1057-
signal_threshold=signal_threshold, shannon_threshold=shannon_threshold )
1058-
result = None
1059-
if signals or shannons:
1060-
report = ''
1061-
dBs = sorted( list( s.dB for s in signals ) + list( s.dB for s in shannons) )
1062-
dBr = dBs[:1] + dBs[max( 1, len(dBs)-1):]
1063-
report += f"Entropy analysis {('of ' + what) if what else ''}: {len(signals)+len(shannons)}x"
1064-
report += f" {'-'.join( f'{dB:.1f}' for dB in dBr )}dB non-random energy patterns in"
1065-
report += f" {commas( sorted( set( s.stride for s in signals )), final_and=True)}-bit symbols\n"
1066-
for s,summary in sorted(
1067-
[
1068-
(s, f"{s.dB:5.1f}dB Signal harmonic feature at offset {s.offset} in {s.symbols}x {s.stride}-bit symbols")
1069-
for s in signals
1070-
] + [
1071-
(s, f"{s.dB:5.1f}dB Shannon entropy reduced at offset {s.offset} in {s.symbols}x {s.stride}-bit symbols")
1072-
for s in shannons
1073-
], reverse=True ):
1074-
report += f"-{summary}{': ' if s.details else ''}\n"
1075-
if s.details:
1076-
report += f"{s.details}\n"
1077-
result = report
1078-
return result
1079+
return display_entropy(
1080+
*scan_entropy(
1081+
entropy, strides, overlap, ignore_dc=ignore_dc, show_details=show_details,
1082+
signal_threshold=signal_threshold, shannon_threshold=shannon_threshold ),
1083+
what=what
1084+
)

slip39/recovery_test.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .recovery import recover, recover_bip39, shannon_entropy, signal_entropy, analyze_entropy
2222
from .recovery.entropy import fft, ifft, pfft, dft, dft_on_real, dft_to_rms_mags, entropy_bin_dfts, denoise_mags, signal_draw, signal_recover_real, scan_entropy
2323
from .dependency_test import substitute, nonrandom_bytes, SEED_XMAS, SEED_ONES
24-
from .util import avg, rms, ordinal, commas
24+
from .util import avg, rms, ordinal, commas, round_onto
2525

2626
log = logging.getLogger( __package__ )
2727

@@ -330,6 +330,21 @@ def test_util():
330330
assert commas( [1,3,5], final_and=True ) == '1, 3 and 5'
331331
assert commas( [1,2,5], final_and=True ) == '1, 2 and 5'
332332

333+
assert round_onto( -.1, [-5, -1, 0, 1, 5], keep_sign=False ) == 0
334+
assert round_onto( -.9, [-5, -1, 0, 1, 5], keep_sign=False ) == -1
335+
assert round_onto( -10, [0, -1, -5, 5, 1], keep_sign=False ) == -5
336+
assert round_onto( 100, [-5, -1, 0, 1, 5], keep_sign=False ) == +5
337+
assert round_onto( 100, [0, -1, -5, 5, 1], keep_sign=False ) == +5
338+
assert round_onto( 2.9, [-5, -1, 0, 1, 5], keep_sign=False ) == +1
339+
assert round_onto( 3.1, [-5, -1, 0, 1, 5], keep_sign=False ) == +5
340+
assert round_onto( .01, [-5, -1, 0, 1, 5], keep_sign=False ) == 0
341+
assert round_onto( -.1, [-5, -1, 0, 1, 5], keep_sign=False ) == 0
342+
assert round_onto( -.1, [-5, -1, 0, 1, 5], keep_sign=True ) == -1
343+
assert round_onto( -.1, [-5, -1, 1, 5], keep_sign=True ) == -1
344+
assert round_onto( -.1, [-5, 1, 5], keep_sign=True ) == -5
345+
assert round_onto( +.1, [-5, -1, 1, 5], keep_sign=True ) == +1
346+
assert round_onto( +.1, [-5, -1, 5], keep_sign=True ) == +5
347+
333348

334349
def test_dft_smoke():
335350
"""Test some basic assumptions on DFTs"""

slip39/util.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ def int_seq( seq ):
8383
return ', '.join( map( str, seq ))
8484

8585

86+
def round_onto( value, keys, keep_sign=True ):
87+
"""Find the numeric key closest to value, maintaining sign if possible."""
88+
keys = sorted( keys )
89+
near, near_i = min(
90+
(abs( k - value), i)
91+
for i, k in enumerate( keys )
92+
)
93+
print( f"round_onto: Rounding {value} onto {keys[near_i]}; {ordinal(near_i+1)} key in: {keys} ({keep_sign=})" )
94+
if keep_sign and (( value < 0 ) != ( keys[near_i] < 0 )):
95+
# The sign differs; if value -'ve but closest was +'ve, and there is a lower key available
96+
if value < 0:
97+
print( f"round_onto: sign of {value} != {keys[near_i]}: shift down in {keys[:near_i+1]}..." )
98+
if near_i > 0:
99+
near_i -= 1
100+
else:
101+
print( f"round_onto: sign of {value} != {keys[near_i]}: shift up in ...{keys[near_i:]}" )
102+
if near_i + 1 < len( keys ):
103+
near_i += 1
104+
return keys[near_i]
105+
106+
107+
def rate_dB( dB, what=None ):
108+
strength = {
109+
2: "very bad",
110+
1: "bad",
111+
0: "poor",
112+
-1: "ok",
113+
-2: "strong",
114+
}
115+
rating = strength[round_onto( dB, strength.keys(), keep_sign=True )]
116+
return f"{what or ''}{': ' if what else ''}{dB:.1f}dB ({rating})"
117+
118+
86119
def input_secure( prompt, secret=True, file=None ):
87120
"""When getting secure (optionally secret) input from standard input, we don't want to use getpass, which
88121
attempts to read from /dev/tty.

0 commit comments

Comments
 (0)