11import argparse
2+ import codecs
3+ import hashlib
24import logging
35import math
46import os
57
68import PySimpleGUI as sg
79
810from ..types import Account
9- from ..api import create , group_parser
11+ from ..api import create , group_parser , random_secret
12+ from ..recovery import recover , recover_bip39
1013from ..util import log_level , log_cfg
1114from ..layout import write_pdfs
1215from ..defaults import GROUPS , GROUP_THRESHOLD_RATIO , CRYPTO_PATHS
2427)
2528B_kwds = dict (
2629 font = font ,
30+ enable_events = True ,
2731)
2832
2933
3034def groups_layout ( names , group_threshold , groups , passphrase = None ):
3135 """Return a layout for the specified number of SLIP-39 groups.
3236
3337 """
38+
3439 group_body = [
3540 sg .Frame (
3641 '#' , [[ sg .Column ( [
@@ -57,29 +62,96 @@ def groups_layout( names, group_threshold, groups, passphrase=None ):
5762 ], key = '-GROUP-SIZES-' ) ]]
5863 ),
5964 ]
60- prefix = (30 ,1 )
61- layout = [
65+ prefix = (32 , 1 )
66+ inputs = (32 , 1 )
67+ bip39_sample = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
68+ layout = [
69+ [
70+ sg .Frame ( 'Seed Data Source' , [
71+ [
72+ sg .Radio ( "128-bit Random" , "SD" , key = '-SD-128-RND-' , default = True , ** B_kwds ),
73+ sg .Radio ( "256-bit Random" , "SD" , key = '-SD-256-RND-' , default = False , ** B_kwds ),
74+ sg .Radio ( "512-bit Random" , "SD" , key = '-SD-512-RND-' , default = False , ** B_kwds ),
75+ ],
76+ [
77+ sg .Radio ( "128-bit Fixed " , "SD" , key = '-SD-128-FIX-' , default = False , ** B_kwds ),
78+ sg .Radio ( "256-bit Fixed " , "SD" , key = '-SD-256-FIX-' , default = False , ** B_kwds ),
79+ sg .Radio ( "512-bit Fixed " , "SD" , key = '-SD-512-FIX-' , default = False , ** B_kwds ),
80+ ],
81+ [
82+ sg .Radio ( "BIP-39" , "SD" , key = '-SD-BIP-' , default = False , ** B_kwds ),
83+ sg .Radio ( "SLIP-39" , "SD" , key = '-SD-SLIP-' , default = False , ** B_kwds ),
84+ ],
85+ [
86+ sg .Frame ( 'From' , [
87+ [
88+ sg .Text ( "Mnemonic(s): " , key = '-SD-DATA-T-' , size = prefix , ** T_kwds ),
89+ sg .Multiline ( bip39_sample , key = '-SD-DATA-' , size = (128 ,1 ), ** I_kwds )
90+ ],
91+ ], key = '-SD-DATA-F-' , visible = False ),
92+ ],
93+ [
94+ sg .Frame ( 'Pass' , [
95+ [
96+ sg .Text ( "Passphrase (decrypt): " , size = prefix , ** T_kwds ),
97+ sg .Input ( "" , key = '-SD-PASS-' , ** I_kwds )
98+ ],
99+ ], key = '-SD-PASS-F-' , visible = False ),
100+ ],
101+ [
102+ sg .Text ( "Seed Data: " , size = prefix , ** T_kwds ),
103+ sg .Text ( f"" , key = '-SD-SEED-' , size = (128 ,1 ), ** T_kwds ),
104+ ],
105+ ] )
106+ ],
107+ ] + [
108+ [
109+ sg .Frame ( '⊻ Seed Extra Entropy (eg. Die rolls, ...)' , [
110+ [
111+ sg .Radio ( "Hex" , "SE" , key = '-SE-HEX-' , default = True , ** B_kwds ),
112+ sg .Radio ( "SHA-512 Stretch" , "SE" , key = '-SE-SHA-' , default = False , ** B_kwds ),
113+ ],
114+ [
115+ sg .Text ( "Entropy: " , key = '-SE-DATA-T-' , size = prefix , ** T_kwds ),
116+ sg .Input ( "" , key = '-SE-DATA-' , size = (128 ,1 ), ** I_kwds ),
117+ ],
118+ [
119+ sg .Text ( "Seed Entropy: " , size = prefix , ** T_kwds ),
120+ sg .Text ( f"" , key = '-SE-SEED-' , size = (128 ,1 ), ** T_kwds ),
121+ ],
122+ ] ),
123+ ]
124+ ] + [
125+ [
126+ sg .Frame ( 'Seed Master Secret' , [
127+ [
128+ sg .Text ( "Seed: " , size = prefix , ** T_kwds ),
129+ sg .Text ( f"" , key = '-SEED-' , size = (128 ,1 ), ** T_kwds ),
130+ ],
131+ ] ),
132+ ],
133+ ] + [
62134 [
63135 sg .Text ( "Save PDF to (ie. USB drive): " , size = prefix , ** T_kwds ),
64- sg .Input ( sg .user_settings_get_entry ( "-target folder-" , "" ), k = '-TARGET-' , ** I_kwds ),
65- sg .FolderBrowse ( ** B_kwds )
136+ sg .Input ( sg .user_settings_get_entry ( "-target folder-" , "" ), size = inputs , key = '-TARGET-' , ** I_kwds ),
137+ sg .FolderBrowse ( ** B_kwds ),
66138 ],
67139 ] + [
68140 [
69141 sg .Text ( "Seed File Name(s): " , size = prefix , ** T_kwds ),
70- sg .Input ( f"{ ', ' .join ( names )} " , key = '-NAMES-' , ** I_kwds ),
142+ sg .Input ( f"{ ', ' .join ( names )} " , size = inputs , key = '-NAMES-' , ** I_kwds ),
71143 sg .Text ( "(optional; comma-separated)" , ** T_kwds ),
72144 ]
73145 ] + [
74146 [
75- sg .Text ( "Requires recovery of at least: " , size = prefix , ** T_kwds ),
76- sg .Input ( f"{ group_threshold } " , key = '-THRESHOLD-' , ** I_kwds ),
147+ sg .Text ( "Requires recovery of at least: " , size = prefix , ** T_kwds ),
148+ sg .Input ( f"{ group_threshold } " , key = '-THRESHOLD-' , size = inputs , ** I_kwds ),
77149 sg .Text ( f"(of { len (groups )} SLIP-39 Recovery Groups)" , key = '-RECOVERY-' , ** T_kwds ),
78150 ],
79151 ] + [
80152 [
81- sg .Text ( "Passphrase to encrypt Seed : " , size = prefix , ** T_kwds ),
82- sg .Input ( f"{ passphrase or '' } " , key = '-PASSPHRASE-' , ** I_kwds ),
153+ sg .Text ( "Passphrase ( encrypt) : " , size = prefix , ** T_kwds ),
154+ sg .Input ( f"{ passphrase or '' } " , key = '-PASSPHRASE-' , size = inputs , ** I_kwds ),
83155 sg .Text ( "(optional; must be remembered separately!!)" , ** T_kwds ),
84156 ],
85157 ] + [
@@ -146,6 +218,9 @@ def app(
146218 status = None
147219 event = False
148220 events_termination = (sg .WIN_CLOSED , 'Exit' ,)
221+ master_secret = None # default to produce randomly
222+ seed_data = None
223+ seed_entr = None
149224 while event not in events_termination :
150225 # Create window (for initial window.read()), or update status
151226 if window :
@@ -161,6 +236,7 @@ def app(
161236 continue
162237
163238 if event == '+' :
239+ # Add a SLIP39 Groups row
164240 g = len (groups )
165241 name = f"Group { g + 1 } "
166242 needs = (2 ,3 )
@@ -170,6 +246,111 @@ def app(
170246 window .extend_layout ( window ['-GROUP-NEEDS-' ], [[ sg .Input ( f"{ needs [0 ]} " , key = f"-G-NEED-{ g } " , ** I_kwds ) ]] ) # noqa: 241
171247 window .extend_layout ( window ['-GROUP-SIZES-' ], [[ sg .Input ( f"{ needs [1 ]} " , key = f"-G-SIZE-{ g } " , ** I_kwds ) ]] ) # noqa: 241
172248
249+ # Respond to changes in the desired Seed Data source, and recover/generate and update the -SD-SEED-
250+ for seed_data_source in [
251+ '-SD-128-RND-' ,
252+ '-SD-256-RND-' ,
253+ '-SD-512-RND-' ,
254+ '-SD-128-FIX-' ,
255+ '-SD-256-FIX-' ,
256+ '-SD-512-FIX-' ,
257+ '-SD-BIP-' ,
258+ '-SD-SLIP-' ,
259+ ]:
260+ if values .get ( seed_data_source ) and seed_data != seed_data_source :
261+ seed_data = seed_data_source
262+ # Changed Seed Data source!
263+ if 'FIX' in seed_data :
264+ window ['-SD-DATA-T-' ].update ( f"Hex data: " )
265+ window ['-SD-DATA-F-' ].update ( visible = True )
266+ window ['-SD-PASS-F-' ].update ( visible = False )
267+ elif 'RND' in seed_data :
268+ window ['-SD-DATA-F-' ].update ( visible = False )
269+ window ['-SD-PASS-F-' ].update ( visible = False )
270+ elif 'BIP' in seed_data :
271+ window ['-SD-DATA-T-' ].update ( f"BIP-39 Mnemonic: " )
272+ window ['-SD-DATA-F-' ].update ( visible = True )
273+ window ['-SD-PASS-F-' ].update ( visible = True )
274+ elif 'SLIP' in seed_data :
275+ window ['-SD-DATA-T-' ].update ( f"SLIP-39 Mnemonics: " )
276+ window ['-SD-DATA-F-' ].update ( visible = True )
277+ window ['-SD-PASS-F-' ].update ( visible = True )
278+ if 'FIX' in seed_data :
279+ bits = int ( seed_data .split ( '-' )[2 ] )
280+ try :
281+ # 0-fill and truncate any supplied hex data to the desired bit length
282+ data = f"{ values ['-SD-DATA-' ]:<0{bits // 4 }.{bits // 4 }} "
283+ master_secret = codecs .decode ( data , 'hex_codec' )
284+ except Exception as exc :
285+ status = f"Invalid Hex for { bits } -bit fixed seed: { exc } "
286+ continue
287+ elif 'BIP' in seed_data :
288+ try :
289+ master_secret = recover_bip39 (
290+ mnemonic = values ['-SD-DATA-' ].strip (),
291+ passphrase = values ['-SD-PASS-' ].strip ().encode ( 'UTF-8' )
292+ )
293+ except Exception as exc :
294+ status = f"Invalid BIP-39 recovery mnemonic: { exc } "
295+ continue
296+ elif 'SLIP' in seed_data :
297+ try :
298+ master_secret = recover (
299+ mnemonics = values ['-SD-DATA-' ].strip ().split ( '\n ' ),
300+ passphrase = values ['-SD-PASS-' ].strip ().encode ( 'UTF-8' )
301+ )
302+ except Exception as exc :
303+ status = f"Invalid SLIP-39 recovery mnemonics: { exc } "
304+ continue
305+ else : # Random. Regenerated each time through.
306+ bits = int ( seed_data .split ( '-' )[2 ] )
307+ master_secret = random_secret ( bits // 8 )
308+
309+ # Compute the Seed Data as hex. Will be 128-, 256- or 512-bit hex data.
310+ window ['-SD-SEED-' ].update ( codecs .encode ( master_secret , 'hex_codec' ).decode ( 'ascii' ))
311+
312+ # Respond to changes in the Seed Entropy, and recover/generate the -SE-SEED-. It is
313+ # expected to be exactly the same size as the -SD-SEED- data.
314+ for seed_entr_source in [
315+ '-SE-HEX-' ,
316+ '-SE-SHA-' ,
317+ ]:
318+ if values .get ( seed_entr_source ) and seed_entr != seed_entr_source :
319+ seed_entr = seed_entr_source
320+ # Changed Seed Entropy source!
321+ if 'HEX' in seed_entr :
322+ window ['-SE-DATA-T-' ].update ( f"Entropy (hex): " )
323+ else :
324+ window ['-SE-DATA-T-' ].update ( f"Entropy (die rolls, etc.): " )
325+
326+ bits = len ( window ['-SD-SEED-' ].get () ) * 4
327+ if 'HEX' in seed_entr :
328+ try :
329+ # 0-fill and truncate any supplied hex data to the desired bit length
330+ data = f"{ values ['-SE-DATA-' ]:<0{bits // 4 }.{bits // 4 }} "
331+ master_entropy = codecs .decode ( data , 'hex_codec' )
332+ except Exception as exc :
333+ status = f"Invalid Hex for { bits } -bit extra seed entropy: { exc } "
334+ continue
335+ else :
336+ try :
337+ # SHA-512 stretch and possibly truncate
338+ stretch = hashlib .sha512 ()
339+ stretch .update ( values ['-SE-DATA-' ].encode ( 'UTF-8' ))
340+ master_entropy = stretch .digest ()[:bits // 8 ]
341+ except Exception as exc :
342+ status = f"Invalid data for { bits } -bit extra seed entropy: { exc } "
343+ continue
344+
345+ # Compute the Seed Entropy as hex. Will be 128-, 256- or 512-bit hex data.
346+ window ['-SE-SEED-' ].update ( codecs .encode ( master_entropy , 'hex_codec' ).decode ( 'ascii' ))
347+
348+ # Compute the Master Secret Seed, from the supplied Seed Data and any extra Seed Entropy
349+ data = codecs .decode ( window ['-SD-SEED-' ].get (), 'hex_codec' )
350+ entr = codecs .decode ( window ['-SE-SEED-' ].get (), 'hex_codec' )
351+ seed = bytes ( d ^ e for d ,e in zip ( data , entr ) )
352+ window ['-SEED-' ].update ( codecs .encode ( seed , 'hex_codec' ).decode ( 'ascii' ))
353+
173354 # A target directory must be selected; use it. This is where any output will be written.
174355 # It should usually be a removable volume, but we do not check for this.
175356 try :
@@ -264,7 +445,18 @@ def app(
264445def main ( argv = None ):
265446 ap = argparse .ArgumentParser (
266447 description = "Create and output SLIP39 encoded Ethereum wallet(s) to a PDF file." ,
267- epilog = "" )
448+ formatter_class = argparse .RawDescriptionHelpFormatter ,
449+ epilog = """\
450+
451+ A GUI App for creating SLIP-39 Mnemonic encoded cryptocurrency wallet seeds, either from secure
452+ randomness, or from pre-existing seed entropy, or recovered from prior SLIP-39 or BIP-39 encoded
453+ seed Mnemonics.
454+
455+ This can be useful for converting existing BIP-39 Mnemonic encoded seeds to more secure and
456+ recoverable SLIP-39 Mnemonic encoding.
457+
458+ """
459+ )
268460 ap .add_argument ( '-v' , '--verbose' , action = "count" ,
269461 default = 0 ,
270462 help = "Display logging information." )
0 commit comments