Skip to content

Commit 7031b69

Browse files
committed
Entropy Analysis of Seed Extra Randomness
1 parent 637f384 commit 7031b69

File tree

2 files changed

+119
-37
lines changed

2 files changed

+119
-37
lines changed

slip39/gui/main.py

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"academic acid beard romp believe impulse species holiday demand building" \
3434
" earth warn lunar olympic clothes piece campus alpha short endless"
3535

36-
37-
SD_SEED_FRAME = 'Seed Source: Input or Create your Seed Entropy here'
36+
SD_SEED_FRAME = 'Seed Source: Create your Seed Entropy here'
37+
SE_SEED_FRAME = 'Seed Extra Randomness'
3838

3939

4040
def theme_color( thing, theme=None ):
@@ -259,8 +259,9 @@ def groups_layout(
259259
], key='-SD-SEED-F-', **F_kwds_big ),
260260
],
261261
] + [
262+
262263
[
263-
sg.Frame( 'Seed Extra Randomness (Trust but Verify ;-)', [
264+
sg.Frame( SE_SEED_FRAME, [
264265
[
265266
sg.Radio( "None", "SE", key='-SE-NON-', visible=LO_REC,
266267
default=True, **B_kwds ),
@@ -641,7 +642,7 @@ def update_seed_data( event, window, values ):
641642
window['-SD-SEED-F-'].update( f"{SD_SEED_FRAME}; {sigs_rate}, {shan_rate}" )
642643
disp_dur,analysis = timing( display_entropy, instrument=True )( sigs, shan, what=f"{len(seed_bytes)*8}-bit Seed Source" )
643644
update_seed_data.entropy = (sigs, shan, analysis)
644-
log.debug( f"Entropy Analysis took {scan_dur:.3f}s + {disp_dur:.3f}s == {scan_dur+disp_dur:.3f}s: {analysis}" )
645+
log.debug( f"Seed Data Entropy Analysis took {scan_dur:.3f}s + {disp_dur:.3f}s == {scan_dur+disp_dur:.3f}s: {analysis}" )
645646
elif changed:
646647
log.info( f"Seed Data requests __TIMEOUT__ w/ current source: {update_seed_data.src!r}" )
647648
values['__TIMEOUT__'] = .5
@@ -703,63 +704,132 @@ def stretch_seed_entropy( entropy, n, bits, encoding=None ):
703704
return entropy[:octets]
704705

705706

706-
def update_seed_entropy( window, values ):
707+
def update_seed_entropy( event, window, values ):
707708
"""Respond to changes in the extra Seed Entropy, and recover/generate the -SE-SEED-. It is
708709
expected to be exactly the same size as the -SD-SEED- data. Stores the last known state of the
709-
-SE-... radio buttons, updating visibilties on change.
710+
-SE-... radio buttons, updating visibilities on change.
710711
712+
We analyze the entropy of the *input* data (as UTF-8), not the resultant entropy.
711713
"""
712-
dat = values['-SE-DATA-']
713-
for src in [
714+
SE_CONTROLS = [
714715
'-SE-NON-',
715716
'-SE-HEX-',
716717
'-SE-SHA-',
717-
]:
718+
]
719+
data = values['-SE-DATA-']
720+
try:
721+
data_bytes = data.encode( 'UTF-8' )
722+
except Exception as exc:
723+
status = f"Invalid data {data!r}: {exc}"
724+
725+
for src in SE_CONTROLS:
718726
if values[src] and update_seed_entropy.src != src:
719727
# If selected radio-button for Seed Entropy source changed, save last source's working
720728
# data and restore what was, last time we were working on this source.
721-
if update_seed_entropy.src:
722-
update_seed_entropy.was[update_seed_entropy.src] = dat
723-
dat = update_seed_entropy.was.get( src, '' )
729+
data = update_seed_entropy.was.get( src, '' )
724730
update_seed_entropy.src = src
725-
window['-SE-DATA-'].update( dat )
726-
values['-SE-DATA-'] = dat
731+
window['-SE-DATA-'].update( data )
732+
values['-SE-DATA-'] = data
727733
if 'HEX' in update_seed_entropy.src:
728734
window['-SE-DATA-T-'].update( "Hex digits: " )
729735
else:
730736
window['-SE-DATA-T-'].update( "Die rolls, etc.: " )
731737

738+
# Evaluate the nature of the extra entropy, and place interpretation to analyze in data_bytes.
739+
# Binary "hex" data should be neutral harmonically and in Shannon entropy. However, some data
740+
# such as dice rolls may exhibit restricted symbol values; try to deduce this case, restricting
741+
# Shannon Entropy 'N' to binary (coin-flip) or 4, 6 and 10-sided dice.
732742
status = None
733-
seed_data = values.get( '-SD-SEED-', window['-SD-SEED-'].get() )
734-
bits = len( seed_data ) * 4
735-
log.debug( f"Computing Extra Entropy, for {bits}-bit Seed Data: {seed_data!r}" )
743+
seed = values.get( '-SD-SEED-', window['-SD-SEED-'].get() )
744+
bits = len( seed ) * 4
745+
746+
strides = 8
747+
overlap = False
748+
ignore_dc = True
749+
N = None
750+
interpretation = 'Trust but Verify ;-'
751+
log.debug( f"Computing Extra Entropy, for {bits}-bit Seed Data: {seed!r}" )
736752
if 'NON' in update_seed_entropy.src:
737753
window['-SE-DATA-F-'].update( visible=False )
738754
extra_entropy = b''
739755
elif 'HEX' in update_seed_entropy.src:
740756
window['-SE-DATA-F-'].update( visible=True )
741757
try:
742-
# 0-fill and truncate any supplied hex data to the desired bit length
743-
extra_entropy = stretch_seed_entropy( dat, n=0, bits=bits, encoding='hex_codec' )
758+
# 0-fill and truncate any supplied hex data to the desired bit length, SHA-512 stretch
759+
extra_entropy = stretch_seed_entropy( data, n=0, bits=bits, encoding='hex_codec' )
744760
except Exception as exc:
745-
status = f"Invalid Hex {dat!r} for {bits}-bit extra seed entropy: {exc}"
761+
status = f"Invalid Hex {data!r} for {bits}-bit extra seed entropy: {exc}"
762+
else:
763+
data_bytes = codecs.decode( f"{data:<0{bits // 4}.{bits // 4}}", 'hex_codec' )
764+
interpretation = "Hexadecimal Data"
765+
strides = None
766+
overlap = True
767+
ignore_dc = False
746768
else:
747769
window['-SE-DATA-F-'].update( visible=True )
748770
try:
749771
# SHA-512 stretch and possibly truncate supplied Entropy (show for 1st Seed)
750-
extra_entropy = stretch_seed_entropy( dat, n=0, bits=bits, encoding='UTF-8' )
772+
extra_entropy = stretch_seed_entropy( data, n=0, bits=bits, encoding='UTF-8' )
751773
except Exception as exc:
752-
status = f"Invalid data {dat!r} for {bits}-bit extra seed entropy: {exc}"
753-
754-
# Compute the Seed Entropy as hex. Will be 128-, 256- or 512-bit hex data.
774+
status = f"Invalid data {data!r} for {bits}-bit extra seed entropy: {exc}"
775+
if data and all( '0' <= c <= '9' for c in data ):
776+
N = {
777+
'0': 2, '1': 2, # Coin flips
778+
'2': 6, '3': 6, '4': 6, '5': 6, '6': 6, # 6-sided (regular) dice
779+
'7': 10, '8': 10, '9': 10, # 10-sided (enter 0 for the 10 side)
780+
}[max( data )]
781+
interpretation = f"{N}-sided Dice" if N > 2 else "Coin-flips/Binary"
782+
783+
# Compute the Seed Entropy as hex. Will be 128-, 256- or 512-bit hex data. Ensure
784+
# extra_{bytes,entropy} are bytes and ASCII (hex, or -) respectively.
755785
if status or not extra_entropy:
756-
extra_entropy = '-' * (bits // 4)
786+
extra_entropy = '-' * ( bits // 4 )
787+
extra_bytes = b''
757788
elif type( extra_entropy ) is bytes:
758-
extra_entropy = codecs.encode( extra_entropy, 'hex_codec' ).decode( 'ascii' )
789+
extra_bytes = extra_entropy
790+
extra_entropy = codecs.encode( extra_bytes, 'hex_codec' ).decode( 'ascii' )
791+
else:
792+
extra_bytes = codecs.decode( extra_entropy, 'hex_codec' )
793+
794+
if window['-SE-SEED-'].get() != extra_entropy:
795+
update_seed_entropy.entropy = None
796+
797+
se_seed_frame = f"{SE_SEED_FRAME} ({interpretation})"
798+
if status is None and len( data_bytes ) >= 8 and not update_seed_entropy.entropy:
799+
if event == '__TIMEOUT__':
800+
scan_dur,(sigs,shan) = timing( scan_entropy, instrument=True )( # could be (None,None)
801+
data_bytes,
802+
strides = strides,
803+
overlap = overlap,
804+
ignore_dc = ignore_dc,
805+
N = N,
806+
signal_threshold = 300/100,
807+
shannon_threshold = 10/100,
808+
show_details = True
809+
)
810+
sigs_rate = f"{rate_dB( max( sigs ).dB if sigs else None, what='Harmonics')}"
811+
shan_rate = f"{rate_dB( max( shan ).dB if shan else None, what='Shannon Entropy')}"
812+
window['-SE-SEED-F-'].update( f"{se_seed_frame}: {sigs_rate}, {shan_rate}" )
813+
disp_dur,analysis = timing( display_entropy, instrument=True )( sigs, shan, what=f"{len(extra_bytes)*8}-bit Extra Seed Entropy" )
814+
update_seed_entropy.entropy = (sigs, shan, analysis)
815+
log.debug( f"Seed Extra Entropy Analysis took {scan_dur:.3f}s + {disp_dur:.3f}s == {scan_dur+disp_dur:.3f}s: {analysis}" )
816+
else:
817+
log.info( f"Seed Extra requests __TIMEOUT__ w/ current source: {update_seed_entropy.src!r}" )
818+
values['__TIMEOUT__'] = .5
819+
elif status:
820+
window['-SE-SEED-F-'].update( f"{se_seed_frame}: Invalid" )
821+
else:
822+
window['-SE-SEED-F-'].update( f"{se_seed_frame}" )
823+
759824
values['-SE-SEED-'] = extra_entropy
760825
window['-SE-SEED-'].update( extra_entropy )
761-
update_seed_entropy.src = None # noqa: E305
826+
827+
update_seed_entropy.was[update_seed_entropy.src] = data
828+
return status
829+
830+
update_seed_entropy.src = '-SE-NON-' # noqa: E305
762831
update_seed_entropy.was = {}
832+
update_seed_entropy.entropy = None
763833

764834

765835
def using_BIP39( values ):
@@ -1089,7 +1159,7 @@ def app(
10891159
# first SLIP-39 encoding.
10901160
master_secret_was = window['-SEED-'].get()
10911161
status_sd = update_seed_data( event, window, values )
1092-
status_se = update_seed_entropy( window, values )
1162+
status_se = update_seed_entropy( event, window, values )
10931163
status_ms = None
10941164
try:
10951165
master_secret = compute_master_secret( window, values, n=0 )

slip39/recovery/entropy.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ def signal_entropy(
399399
show_details: bool = True, # include signal details (incl. expensive ifft)
400400
middle: Optional[Callable[List[float],float]] = None, # default to normalized RMS energy, or eg. statistics.median on bin magnitudes
401401
harmonics_max: int = 2, # maximum harmonics to represent
402-
) -> Optional[Tuple[float, int]]:
402+
) -> Optional[Signal]:
403403
"""Checks for signals in the supplied entropy. If n-bit 'stride' or 'base'-n symbols are found
404404
to exhibit significant signal pattern over the average noise, then the pattern will be reported.
405405
@@ -600,6 +600,7 @@ def signal_entropy(
600600
#mags_avgs = [sum(col)/len(mags_all) for col in zip(*mags_all)]
601601
#print( f"avgs: {' '.join( f'{m:{stride*2}.1f}' for m in mags_avgs )}: {sum(mags_avgs):7.2f}" )
602602
return strongest
603+
603604
signal_entropy.signal_limits = { # noqa: E305
604605
False: {
605606
128: {
@@ -927,16 +928,22 @@ def shannon_entropy(
927928
threshold: float = None, # Allow up to a certain % bits-per-symbol entropy deficit
928929
show_details: bool = True, # include signal details (incl. expensive ifft)
929930
snr_min: float = 1/100, # Minimum snr .01 == -40dB (eg. if all symbols unique)
930-
) -> Optional[str]:
931+
N: Optional[int] = None, # If a max. number of unique symbols is known eg. dice
932+
) -> Optional[Signal]:
931933
"""Estimates the "Symbolised Shannon Entropy" for stride-bit chunks of the provided entropy; by
932934
default, since we don't know the bit offset of any pattern, we'll scan at each possible bit
933935
offset (overlap = True).
934936
935-
We'll return the estimated predictability of the entropy, in the range [0,1], where 0 is
937+
We'll compute the estimated predictability of the entropy, in the range [0,1], where 0 is
936938
unpredictable (high entropy), and 1 is totally predictable (no entropy), but as a dB range,
937939
where -'ve dB is below the threshold and acceptable, and +'ve is above the predictability
938940
threshold, and is unacceptable.
939941
942+
Typically, for large entropy and/or small 'stride' values, the number of unique values N found
943+
should tend toward 2^stride. However, for small entropy (eg. insufficient data) or small known
944+
N (eg. die rolls), we expect to see less unique values than would be implied by the stride.
945+
Therefore, we reduce the N in those cases.
946+
940947
The "strongest" (least entropy, most predictable, most indicative that the entropy is
941948
unacceptable) signal will be returned.
942949
@@ -976,9 +983,11 @@ def shannon_entropy(
976983
# required to encode the maximum number of unique symbols possible (due to the symbol or
977984
# entropy size), not in the range [0,2^stride].
978985
shannon = 0.0
979-
N = min( symbols, 2**stride )
980-
if N > 1:
981-
shannon = bitspersymbol / math.log( N, 2 )
986+
N_min = min( N or symbols, symbols, 2**stride )
987+
assert len( frequency ) <= N_min, \
988+
f"Observed {len(frequency)} unique symbols; more than expected by min({N=}, {symbols=} or {2**stride=})"
989+
if N_min > 1:
990+
shannon = bitspersymbol / math.log( N_min, 2 )
982991

983992
# So now, shannon is 1.0 for a "good" perfectly unpredictable entropy (all symbols
984993
# different, full bits-per-symbol required to encode), and 0 for "bad" perfectly predictable
@@ -1017,7 +1026,7 @@ def shannon_entropy(
10171026
snr_dB = into_dB( snr )
10181027
weaker = strongest and snr_dB < strongest.dB
10191028
( log.debug if snr_dB < 0 else log.info )(
1020-
f"Found {len(frequency):3} unique (of {2**stride:5} possible) in {symbols:3}"
1029+
f"Found {len(frequency):3} unique (of {N_min:3} possible) in {symbols:3}"
10211030
f"x {stride:2}-bit symbols at offset {offset:2} in {length:4}-bit entropy:"
10221031
f" Shannon Entropy {bitspersymbol:7.3f} b/s, P({shannon:7.3f}) unpredictable; {predictability=:7.3f}"
10231032
f" vs. threshold={thresh:7.3f} == {snr=:7.3f} {snr_dB:7.3f}dB: {entropy_hex}"
@@ -1071,6 +1080,7 @@ def shannon_entropy(
10711080
details = details,
10721081
)
10731082
return strongest
1083+
10741084
shannon_entropy.shannon_limits = { # noqa: E305
10751085
False: {
10761086
128: {
@@ -1181,6 +1191,7 @@ def scan_entropy(
11811191
overlap: bool = True,
11821192
ignore_dc: bool = False,
11831193
show_details: bool = True,
1194+
N: Optional[int] = None, # shannon_entropy may specify limited unique symbols
11841195
signal_threshold: Optional[float] = None,
11851196
shannon_threshold: Optional[float] = None,
11861197
) -> Tuple[List[Signal],List[Signal]]:
@@ -1214,7 +1225,7 @@ def scan_entropy(
12141225
(
12151226
s
12161227
for s in (
1217-
shannon_entropy( entropy, stride=stride, overlap=overlap,
1228+
shannon_entropy( entropy, stride=stride, overlap=overlap, N=N,
12181229
show_details=show_details, threshold=shannon_threshold )
12191230
for stride in range( *strides )
12201231
)
@@ -1254,6 +1265,7 @@ def analyze_entropy(
12541265
ignore_dc: bool = False,
12551266
what: Optional[str] = None,
12561267
show_details: bool = True,
1268+
N: Optional[int] = None, # shannon_entropy may specify limited unique symbols
12571269
signal_threshold: Optional[float] = None,
12581270
shannon_threshold: Optional[float] = None,
12591271
) -> Optional[str]:
@@ -1282,7 +1294,7 @@ def analyze_entropy(
12821294
"""
12831295
return display_entropy(
12841296
*scan_entropy(
1285-
entropy, strides, overlap, ignore_dc=ignore_dc, show_details=show_details,
1297+
entropy, strides, overlap, ignore_dc=ignore_dc, show_details=show_details, N=N,
12861298
signal_threshold=signal_threshold, shannon_threshold=shannon_threshold ),
12871299
what=what
12881300
)

0 commit comments

Comments
 (0)