44import logging
55import math
66import os
7+ import re
78import subprocess
89
910from itertools import islice
1718from ..defaults import (
1819 GROUPS , GROUP_THRESHOLD_RATIO , MNEM_PREFIX , CRYPTO_PATHS , BITS ,
1920 CARD_SIZES , CARD , PAPER_FORMATS , PAPER , WALLET_SIZES , WALLET ,
21+ MNEM_CONT ,
2022)
2123
2224log = logging .getLogger ( __package__ )
2931 change_submits = True ,
3032 font = font ,
3133)
34+ I_kwds_small = dict (
35+ change_submits = True ,
36+ font = font_small ,
37+ )
3238T_kwds = dict (
3339 font = font ,
3440)
4248
4349prefix = (20 , 1 )
4450inputs = (40 , 1 )
45- inlong = (128 ,1 ) # 512-bit seeds require 128 hex nibbles
51+ inlong = (128 ,1 ) # 512-bit seeds require 128 hex nibbles
4652shorty = (10 , 1 )
53+ mnemos = (195 ,3 )
4754
4855
4956def groups_layout (
@@ -52,37 +59,54 @@ def groups_layout(
5259 groups ,
5360 passphrase = None ,
5461 cryptocurrency = None ,
55- wallet_pwd = None , # If False, disable capability
62+ wallet_pwd = None , # If False, disable capability
63+ simple = False , # Simplify layout by reducing complexity/choices
5664):
5765 """Return a layout for the specified number of SLIP-39 groups.
5866
5967 """
6068
6169 group_body = [
62- sg .Frame (
63- '#' , [[ sg .Column ( [
64- [ sg .Text ( f'{ g + 1 } ' , ** T_kwds ) ]
65- for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
66- ], key = '-GROUP-NUMBER-' ) ]], ** F_kwds
67- ),
68- sg .Frame (
69- 'Group Name; Recovery requires at least...' , [[ sg .Column ( [
70- [ sg .Input ( f'{ g_name } ' , key = f'-G-NAME-{ g } ' , ** I_kwds ) ]
71- for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
72- ], key = '-GROUP-NAMES-' ) ]], ** F_kwds
73- ),
74- sg .Frame (
75- '# Needed' , [[ sg .Column ( [
76- [ sg .Input ( f'{ g_need } ' , key = f'-G-NEED-{ g } ' , size = shorty , ** I_kwds ) ]
77- for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
78- ], key = '-GROUP-NEEDS-' ) ]], ** F_kwds
79- ),
80- sg .Frame (
81- 'of # in Group' , [[ sg .Column ( [
82- [ sg .Input ( f'{ g_size } ' , key = f'-G-SIZE-{ g } ' , size = shorty , ** I_kwds ) ]
83- for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
84- ], key = '-GROUP-SIZES-' ) ]], ** F_kwds
85- ),
70+ sg .Frame ( '#' , [
71+ [
72+ sg .Column ( [
73+ [
74+ sg .Text ( f'{ g + 1 } ' , ** T_kwds ),
75+ ]
76+ for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
77+ ], key = '-GROUP-NUMBER-' )
78+ ]
79+ ], ** F_kwds ),
80+ sg .Frame ( 'Group Name; Recovery requires at least...' , [
81+ [
82+ sg .Column ( [
83+ [
84+ sg .Input ( f'{ g_name } ' , key = f'-G-NAME-{ g } ' , ** I_kwds ),
85+ ]
86+ for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
87+ ], key = '-GROUP-NAMES-' )
88+ ]
89+ ], ** F_kwds ),
90+ sg .Frame ( '# Needed' , [
91+ [
92+ sg .Column ( [
93+ [
94+ sg .Input ( f'{ g_need } ' , key = f'-G-NEED-{ g } ' , size = shorty , ** I_kwds )
95+ ]
96+ for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
97+ ], key = '-GROUP-NEEDS-' )
98+ ]
99+ ], ** F_kwds ),
100+ sg .Frame ( 'of # in Group' , [
101+ [
102+ sg .Column ( [
103+ [
104+ sg .Input ( f'{ g_size } ' , key = f'-G-SIZE-{ g } ' , size = shorty , ** I_kwds )
105+ ]
106+ for g ,(g_name ,(g_need ,g_size )) in enumerate ( groups .items () )
107+ ], key = '-GROUP-SIZES-' )
108+ ]
109+ ], ** F_kwds ),
86110 ]
87111
88112 if wallet_pwd is False :
@@ -104,7 +128,7 @@ def groups_layout(
104128 [
105129 sg .Text ( "Seed Name(s): " , size = prefix , ** T_kwds ),
106130 sg .Input ( f"{ ', ' .join ( names )} " , key = '-NAMES-' , size = inputs , ** I_kwds ),
107- sg .Text ( "(comma-separated). Paper size: " , ** T_kwds ),
131+ sg .Text ( ", ... Paper size: " , ** T_kwds ),
108132 ] + [
109133 sg .Radio ( f"{ pf } " , "PF" , key = f"-PF-{ pf } " , default = (pf == PAPER ), ** B_kwds )
110134 for pf in PAPER_FORMATS
@@ -135,18 +159,18 @@ def groups_layout(
135159 ], key = '-SD-PASS-F-' , visible = False , ** F_kwds ),
136160 ],
137161 [
138- sg .Frame ( 'From' , [
162+ sg .Frame ( 'From Mnemonic(s): ' , [
139163 [
140- sg .Text ( "Mnemonic(s): " , key = '-SD-DATA-T-' , size = prefix , ** T_kwds ),
141- sg .Multiline ( "" , key = '-SD-DATA-' , size = inlong , ** I_kwds ),
164+ # sg.Text( "Mnemonic(s): ", key='-SD-DATA-T-', size=prefix, **T_kwds ),
165+ sg .Multiline ( "" , key = '-SD-DATA-' , size = mnemos , ** I_kwds_small ),
142166 ],
143167 ], key = '-SD-DATA-F-' , visible = False , ** F_kwds ),
144168 ],
145169 [
146170 sg .Text ( "Seed Data: " , size = prefix , ** T_kwds ),
147171 sg .Text ( "" , key = '-SD-SEED-' , size = inlong , ** T_kwds ),
148172 ],
149- ], key = '-SD-SEED-F-' , ** F_kwds ),
173+ ], key = '-SD-SEED-F-' , ** F_kwds ),
150174 ],
151175 ] + [
152176 [
@@ -178,7 +202,7 @@ def groups_layout(
178202 sg .Text ( "" , key = '-SEED-' , size = inlong , ** T_kwds ),
179203 ],
180204 [
181- sg .Column ( [
205+ sg .Column ( [
182206 [
183207 sg .Text ( "Requires recovery of: " , size = prefix , ** T_kwds ),
184208 sg .Input ( f"{ group_threshold } " , key = '-THRESHOLD-' , size = shorty , ** I_kwds ),
@@ -265,7 +289,8 @@ def groups_layout(
265289 [
266290 sg .Frame ( '8. SLIP-39 Recovery Mnemonics produced. These will be saved to the PDF on cards.' , [
267291 [
268- sg .Multiline ( "" , key = '-MNEMONICS-' + sg .WRITE_ONLY_KEY , size = (195 ,6 ), font = font_small )
292+ sg .Multiline ( "" , key = '-MNEMONICS-' + sg .WRITE_ONLY_KEY ,
293+ size = mnemos , ** I_kwds_small ) # noqa: E127
269294 ]
270295 ], key = '-MNEMONICS-F-' , ** F_kwds ),
271296 ],
@@ -282,6 +307,51 @@ def groups_layout(
282307 return layout
283308
284309
310+ def mnemonic_continuation ( lines ):
311+ """Filter out any prefixes consisting of word/space symbols followed by a single non-word/space
312+ symbol, before any number of Mnemonic word/space symbols:
313+
314+ Group 1 { word word ...
315+ Group 2 ╭ word word ...
316+ ╰ word word ...
317+ Group 3 ┌ word word ...
318+ ├ word word ...
319+ └ word word ...
320+ ^^^^^^^^ ^ ^^^^^^^^^^...
321+ | | |
322+ word/digit/space* | word/space*
323+ |
324+ single non-word/digit/space
325+
326+ Any joining lines w/ a recognized MNEM_CONT prefix symbol (a UTF-8 square/curly bracket top or
327+ center segment) are concatenated. Any other non-prefixed or unrecognized word/space symbols are
328+ considered complete mnemonic lines.
329+
330+ """
331+ mnemonic = []
332+ for li in lines :
333+ m = re .match ( r"([\w\d\s]*[^\w\d\s])?([\w\s]*)" , li )
334+ assert m , \
335+ f"Invalid BIP-39 or SLIP-39 Mnemonic [prefix:]phrase: { li } "
336+ pref ,mnem = m .groups ()
337+ if not pref and not mnem : # blank lines ignored
338+ continue
339+ mnemonic .append ( mnem .strip () )
340+ continuation = pref and ( pref [- 1 ] in MNEM_CONT )
341+ log .debug ( f"Prefix: { pref !r:10} , Phrase: { mnem !r} " + ( "..." if continuation else "" ))
342+ if continuation :
343+ continue
344+ if mnemonic :
345+ phrase = ' ' .join ( mnemonic )
346+ log .info ( f"Mnemonic phrase: { phrase !r} " )
347+ yield phrase
348+ mnemonic = []
349+ if mnemonic :
350+ phrase = ' ' .join ( mnemonic )
351+ log .info ( f"Mnemonic phrase: { phrase !r} " )
352+ yield phrase
353+
354+
285355def update_seed_data ( event , window , values ):
286356 """Respond to changes in the desired Seed Data source, and recover/generate and update the
287357 -SD-SEED- text, which is a Text field (not in values), and defines the size of the Seed in hex
@@ -320,17 +390,17 @@ def update_seed_data( event, window, values ):
320390 window ['-SD-PASS-' ].update ( pwd )
321391 # And change visibility of Seed Data source controls
322392 if 'FIX' in update_seed_data .src :
323- window ['-SD-DATA-T -' ].update ( "Hex data: " )
393+ window ['-SD-DATA-F -' ].update ( "Hex data: " )
324394 window ['-SD-DATA-F-' ].update ( visible = True )
325395 window ['-SD-PASS-C-' ].update ( visible = False )
326396 window ['-SD-PASS-F-' ].update ( visible = False )
327397 elif 'BIP' in update_seed_data .src :
328- window ['-SD-DATA-T -' ].update ( "BIP-39 Mnemonic: " )
398+ window ['-SD-DATA-F -' ].update ( "BIP-39 Mnemonic: " )
329399 window ['-SD-DATA-F-' ].update ( visible = True )
330400 window ['-SD-PASS-C-' ].update ( visible = False )
331401 window ['-SD-PASS-F-' ].update ( visible = True )
332402 elif 'SLIP' in update_seed_data .src :
333- window ['-SD-DATA-T -' ].update ( "SLIP-39 Mnemonics: " )
403+ window ['-SD-DATA-F -' ].update ( "SLIP-39 Mnemonics: " )
334404 window ['-SD-DATA-F-' ].update ( visible = True )
335405 window ['-SD-PASS-C-' ].update ( visible = True )
336406 window ['-SD-PASS-F-' ].update ( visible = values ['-SD-PASS-C-' ] )
@@ -359,7 +429,7 @@ def update_seed_data( event, window, values ):
359429 window ['-SD-PASS-F-' ].update ( visible = values ['-SD-PASS-C-' ] )
360430 try :
361431 seed_data = recover (
362- mnemonics = dat .strip ().split ( '\n ' ),
432+ mnemonics = list ( mnemonic_continuation ( dat .strip ().split ( '\n ' )) ),
363433 passphrase = pwd .strip ().encode ( 'UTF-8' )
364434 )
365435 bits = len ( seed_data ) * 4
@@ -497,7 +567,7 @@ def compute_master_secret( window, values, n=0 ):
497567
498568 This is a critical feature -- without this visual confirmation, it is NOT POSSIBLE to trust any
499569 cryptocurrency seed generation algorithm.
500-
570+
501571 This function must have knowledge of the extra Seed Entropy settings, so it inspects the
502572 -SE-{NON/HEX}- checkbox values.
503573 """
@@ -538,10 +608,15 @@ def update_seed_recovered( window, values, details, passphrase=None ):
538608 for g ,(g_of ,g_mnems ) in details .groups .items () if details else []:
539609 for i ,mnem in enumerate ( g_mnems , start = 1 ):
540610 mnemonics .append ( mnem )
541- # Display Mnemonics in rows of 20 words:
542- # / word something ...
543- # | another more ...
544- # \ last line of mnemonic ...
611+ # Display Mnemonics in rows of 20, 33 or 59 words:
612+ # 1: single line mnemonic ...
613+ # or
614+ # 2/ word something another ...
615+ # \ last line of mnemonic ...
616+ # or
617+ # 3/ word something another ...
618+ # | another more ...
619+ # \ last line of mnemonic ...
545620 mset = mnem .split ()
546621 for mri ,mout in enumerate ( chunker ( mset , 20 )):
547622 p = MNEM_PREFIX .get ( len (mset ), '' )[mri :mri + 1 ] or ':'
@@ -588,19 +663,19 @@ def app(
588663
589664 # Try to set a sane initial CWD (for saving generated files). If we start up in the standard
590665 # macOS App's "Container" directory for this App, ie.:
591- #
666+ #
592667 # /Users/<somebody>/Library/Containers/ca.kundert.perry.SLIP39/Data
593- #
668+ #
594669 # then we'll move upwards to the user's home directory. If we change the macOS App's Bundle ID,
595670 # this will change..
596671 cwd = os .getcwd ()
597672 if cwd .endswith ( '/Library/Containers/ca.kundert.perry.SLIP39/Data' ):
598673 cwd = cwd [:- 48 ]
599674 sg .user_settings_set_entry ( '-target folder-' , cwd )
600675
601- #
676+ #
602677 # If no name(s) supplied, try to get the User's full name.
603- #
678+ #
604679 if not names :
605680 try :
606681 scutil = subprocess .run (
@@ -612,10 +687,10 @@ def app(
612687 print ( repr ( scutil ))
613688 assert scutil .returncode == 0 and scutil .stdout , \
614689 "'scutil' command failed, or no output returned"
615- for l in scutil .stdout .split ( '\n ' ):
616- if 'kCGSessionLongUserNameKey' in l :
690+ for li in scutil .stdout .split ( '\n ' ):
691+ if 'kCGSessionLongUserNameKey' in li :
617692 # eg.: " kCGSessionLongUserNameKey : Perry Kundert"
618- full_name = l .split ( ':' )[1 ].strip ()
693+ full_name = li .split ( ':' )[1 ].strip ()
619694 log .info ( f"Current user's full name: { full_name !r} " )
620695 names = [ full_name ]
621696 break
@@ -640,14 +715,15 @@ def app(
640715 status_error = False
641716 event = False
642717 events_termination = (sg .WIN_CLOSED , 'Exit' ,)
718+ events_ignored = ('-MNEMONICS-' + sg .WRITE_ONLY_KEY ,)
643719 master_secret = None # default to produce randomly
644720 groups_recovered = None # The last SLIP-39 groups recovered from user input
645721 details = None # ..and the SLIP-39 details produced; make None to force SLIP-39 Mnemonic update
646722 cryptopaths = None
647723 while event not in events_termination :
648724 # Create window (for initial window.read()), or update status
649725 if window :
650- window ['-STATUS-' ].update ( f"{ status or 'OK' :.145} { '...' if len (status )> 145 else '' } " , font = font_bold if status_error else font )
726+ window ['-STATUS-' ].update ( f"{ status or 'OK' :.145} { '...' if len (status or 'OK' )> 145 else '' } " , font = font_bold if status_error else font )
651727 window ['-SAVE-' ].update ( disabled = status_error )
652728 window ['-RECOVERY-' ].update ( f"of { len (groups )} " )
653729 window ['-SD-SEED-F-' ].expand ( expand_x = True )
@@ -665,13 +741,16 @@ def app(
665741 window = sg .Window ( f"{ ', ' .join ( names or [ 'SLIP-39' ] )} Mnemonic Cards" , layout )
666742 timeout = 0 # First time through, refresh immediately
667743
668- status = None
669- status_error = True
744+ # Block (except for first loop) and obtain current event and input values. Until we get a
745+ # new event, retain the current status
670746 event , values = window .read ( timeout = timeout )
671- logging .info ( f"{ event } , { values } " )
672- if not values or event in events_termination :
747+ logging .debug ( f"{ event } , { values } " )
748+ if not values or event in events_termination or event in events_ignored :
673749 continue
674750
751+ status = None
752+ status_error = True
753+
675754 if event == '+' and len ( groups ) < 16 :
676755 # Add a SLIP39 Groups row (if not already at limit)
677756 g = len ( groups )
@@ -741,7 +820,7 @@ def app(
741820 if g_rec != groups_recovered : # eg. group name or details changed
742821 log .info ( "Updating SLIP-39 due to changing Groups" )
743822 groups_recovered = g_rec
744- detail = None
823+ details = None
745824
746825 # Confirm the selected Group Threshold requirement
747826 try :
0 commit comments