-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpassphrase
executable file
·1565 lines (1351 loc) · 69 KB
/
passphrase
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
A passphrase and passcode generator using MVC architecture, which is
structured in three main classes of Model, View, and Controller;
modified from posts by Brian Oakley at
https://stackoverflow.com/questions/32864610/
"""
# 'Copyright (C) 2021- 2024 C.S. Echt, under GNU General Public License'
# Standard library imports
import glob
import random
from math import log
from pathlib import Path
from signal import signal, SIGINT
from string import digits, punctuation, ascii_letters, ascii_uppercase
# Third party imports (tk may not be included with some Python installations).
try:
import tkinter as tk
from tkinter import messagebox, ttk, font
from tkinter.scrolledtext import ScrolledText
except (ImportError, ModuleNotFoundError) as error:
print('Passphrase requires tkinter, which is included with some Python 3.7+'
'\ndistributions, such as from Active State.'
'\nInstall 3.7+ or re-install Python and include Tk/Tcl.'
'\nDownloads available from python.org'
'\nOn Linux-Ubuntu you may need: sudo apt install python3-tk'
f'\nSee also: https://tkdocs.com/tutorial/install.html \n{error}')
# Local application imports
import pass_utils
from pass_utils import (constants as const,
path_check as path,
utils, vcheck)
from pass_utils.utils import MY_OS
from pass_utils.constants import (STUBRESULT,
SYMBOLS,
COLORS,
STRING_DATA,
LIST_DATA)
class PassModeler:
"""
Set up lists of words and characters, randomize subsets to construct
pass-strings, then calculate their length and entropy for display.
"""
def __init__(self, share):
self.share = share
self.share.word_files = {
'System dictionary': path.SYSDICT_PATH,
'EFF long wordlist': path.WORDDIR / 'eff_large_wordlist.txt',
'US Constitution': path.WORDDIR / 'usconst_wordlist.txt',
'Don Quijote': path.WORDDIR / 'don_quijote_wordlist.txt',
'Frankenstein': path.WORDDIR / 'frankenstein_wordlist.txt',
'Les Miserables': path.WORDDIR / 'les_miserables.txt',
'此開卷第 Story of the Stone': path.WORDDIR / 'red_chamber_wordlist.txt',
'The Death of Arthur': path.WORDDIR / 'le_morte_darthur_wordlist.txt',
}
def check_files(self) -> None:
"""
Confirm whether required files are present, exit if not.
Update wordlist options based on availability.
"""
all_lists = list(self.share.word_files.keys())
# Uncertain how to handle system dictionary on Windows.
if MY_OS == 'win':
all_lists.remove('System dictionary')
self.share.choose_wordlist['values'] = all_lists
fnf_msg = ('\nHmmm. Cannot locate system dictionary\n'
'words nor any custom wordlist files\n'
'(*_wordlist.txt). Wordlist files should be\n'
'in a folder called "wordlists" included\n'
'with the repository downloaded from:\n'
f'{pass_utils.URL}\nWill exit program now...')
wordfile_list = glob.glob(str(path.WORDDIR / '*_wordlist.txt'))
# This covers platforms with and w/o system dictionary.
if not Path.is_file(path.SYSDICT_PATH) and len(wordfile_list) == 0:
print(fnf_msg)
messagebox.showinfo(title='Files not found', detail=fnf_msg)
utils.quit_gui(mainloop=app, gui=True)
if not Path.is_file(path.SYSDICT_PATH) and len(wordfile_list) > 0 and MY_OS != 'win':
notice = ('Hmmm. The system dictionary cannot be found.\n'
'Using only custom wordlists ...')
messagebox.showinfo(title='File not found', detail=notice)
# Need to remove 'System dictionary' as an option.
all_lists.remove('System dictionary')
self.share.choose_wordlist['values'] = all_lists
if Path.is_file(path.SYSDICT_PATH) and len(wordfile_list) == 0:
notice = ('Oops! Optional wordlists are missing.\n'
'Wordlist files should be in a folder\n'
'called "wordlists" included with\n'
'the repository downloaded from:\n'
f'{pass_utils.URL}\n'
'Using system dictionary words...\n')
self.share.choose_wordlist.config(state='disabled')
messagebox.showinfo(title='File not found', detail=notice)
self.share.choose_wordlist['values'] = ('System dictionary',)
# Need to have default .get() in combobox be the 1st available wordlist.
self.share.choose_wordlist.current(0)
def get_words(self) -> None:
"""
Populate lists with words to randomize in make_pass(); needs to
run at start and each time a new wordlist is selected by user.
"""
# Need to reset excluded characters and prior pass-strings when a new
# wordlist is selected.
self.share.tkdata['pp_raw_h'].set(0)
self.share.tkdata['pp_extra_h'].set(0)
self.share.tkdata['pp_short_h'].set(0)
self.share.tkdata['pp_raw_len'].set(0)
self.share.tkdata['pp_extra_len'].set(0)
self.share.tkdata['pp_short_len'].set(0)
self.share.exclude_entry.delete(0, 'end')
self.share.tkdata['excluded'].set('')
STRING_DATA['all_unused'] = ''
# Need to retain stub result only for startup, otherwise delete
# the results each time get_words() or share.getwords() is called.
if self.share.tkdata['phrase_raw'].get() not in STUBRESULT:
self.share.tkdata['phrase_raw'].set('')
self.share.tkdata['phrase_plus'].set('')
self.share.tkdata['phrase_short'].set('')
# The *_wordlist.txt files have only unique words, but...
# use set() and split() here to generalize for any text file.
# Need read_text(encoding) so Windows can read all wordlist fonts.
choice = self.share.choose_wordlist.get()
wordfile = self.share.word_files[choice]
# all_words is also needed in make_pass to handle transitions with
# no_caps_chkbtn <-> caps.
self.share.all_words = set(
Path(wordfile).read_text(encoding='utf-8').split())
# Need to remove words having the possessive form ('s) b/c they
# duplicate many nouns in an English system dictionary.
# isalpha() also removes hyphenated words; EFF large wordlist has 4.
# NOTE that all wordfiles were constructed with parse_wordlist script from
# https://github.com/csecht/make_wordlist, and so contain only words
# of 3 or more characters.
# Also, need to ignore case for Chinese wordlist.
if (self.share.tkdata['no_caps_chkbtn'].get() and
self.share.choose_wordlist.get() != '此開卷第 Story of the Stone'):
working_list = LIST_DATA['word_list'] = [
_w for _w in self.share.all_words if _w.isalpha() and _w[0].islower()]
else:
working_list = LIST_DATA['word_list'] = [
_w for _w in self.share.all_words if _w.isalpha()]
LIST_DATA['short_list'] = [_w for _w in working_list if len(_w) < 7]
# This is used for live updates in the main window for number of words
# in the selected wordlist.
self.share.tkdata['available'].set(len(working_list))
def exclude_words_and_characters(self) -> None:
"""
Set up lists of characters to exclude from pass-strings.
Is called, along with make_pass(), from self.share.makepass().
Calls reset() to restore default values if user deletes entry.
Returns: None
"""
# Do not accept entries with space between characters, and.
# need to reset to default values if user deletes the prior entry.
excluded = self.share.exclude_entry.get().strip()
if ' ' in excluded or not excluded:
self.reset()
return
# Need to filter words and strings containing characters to be excluded.
# When switching Checkbox from no_caps_chkbtn to caps, need to restore the
# full 'word_list' so exclusions can be reapplied. This redefines
# 'word_list' and 'short_list' from get_words().
# Don't apply caps filters to Chinese wordlists.
if self.share.tkdata[
'no_caps_chkbtn'].get() and self.share.choose_wordlist.get() != '此開卷第 Story of the Stone':
LIST_DATA['word_list'] = [_w for _w in LIST_DATA['word_list'] if _w[0].islower()]
else:
LIST_DATA['word_list'] = [_w for _w in self.share.all_words if _w.isalpha()]
LIST_DATA['short_list'] = [_w for _w in LIST_DATA['word_list'] if len(_w) < 7]
if excluded not in STRING_DATA['all_unused']:
STRING_DATA['all_unused'] += ' ' + excluded
self.share.tkdata['excluded'].set(STRING_DATA['all_unused'])
# Remove exclude characters from data lists that are used to generate
# pass-strings; resets to '' when PassModeler.reset() is called.
for excl in STRING_DATA['all_unused'].split():
LIST_DATA['word_list'] = [
_w for _w in LIST_DATA['word_list'] if excl not in _w]
LIST_DATA['short_list'] = [
_w for _w in LIST_DATA['short_list'] if excl not in _w]
STRING_DATA['symbols'] = [
_s for _s in STRING_DATA['symbols'] if excl not in _s]
STRING_DATA['digi'] = [
_d for _d in STRING_DATA['digi'] if excl not in _d]
STRING_DATA['caps'] = [
_uc for _uc in STRING_DATA['caps'] if excl not in _uc]
STRING_DATA['all_char'] = [
_ch for _ch in STRING_DATA['all_char'] if excl not in _ch]
STRING_DATA['some_char'] = [
_ch for _ch in STRING_DATA['some_char'] if excl not in _ch]
# Need to display # of currently available words in two places.
# 'available' is used as param in PassController.explain() and
# the self.share.available_show Label in main window.
self.share.tkdata['available'].set(len(LIST_DATA['word_list']))
def make_pass(self) -> None:
"""
Generate and set random pass-strings.
Called through Controller from keybinding, menu, or button.
That Controller call also calls exclude_words_and_characters().
Calls set_entropy(), config_results(), and conditional quit_gui().
Returns: None
"""
# Need to correct invalid user entries for number of words & characters.
numwords = self.share.numwords_entry.get().strip()
if not numwords.isdigit():
self.share.numwords_entry.delete(0, 'end')
self.share.numwords_entry.insert(0, '0')
numwords = int(self.share.numwords_entry.get() or '0')
numchars = self.share.numchars_entry.get().strip()
if not numchars.isdigit():
self.share.numchars_entry.delete(0, 'end')
self.share.numchars_entry.insert(0, '0')
numchars = int(self.share.numchars_entry.get() or '0')
# Build pass-strings.
passphrase = ""
shortphrase = ""
passcode1 = ""
passcode2 = ""
addsymbol = ""
addnum = ""
addcaps = ""
# If all char are excluded, then cannot generate pass-strings
# because raises "IndexError: Cannot choose from an empty sequence".
try:
passphrase = "".join(const.VERY_RANDOM.choice(LIST_DATA['word_list']) for
_ in range(numwords))
shortphrase = "".join(const.VERY_RANDOM.choice(LIST_DATA['short_list']) for
_ in range(numwords))
passcode1 = "".join(const.VERY_RANDOM.choice(STRING_DATA['all_char']) for
_ in range(numchars))
passcode2 = "".join(const.VERY_RANDOM.choice(STRING_DATA['some_char']) for
_ in range(numchars))
# Randomly select 1 of each symbol to append.
addsymbol = const.VERY_RANDOM.choice(STRING_DATA['symbols'])
addnum = const.VERY_RANDOM.choice(STRING_DATA['digi'])
addcaps = const.VERY_RANDOM.choice(STRING_DATA['caps'])
except IndexError:
messagebox.showerror(
title='Exceeded exclusion limit',
message='Uh oh!',
detail='No characters of one of the required types are left to exclude.'
' Try again with fewer exclusions. Time to exit...')
utils.quit_gui(mainloop=app, gui=True)
# Build passphrase alternatives.
phraseplus = passphrase + addsymbol + addnum + addcaps
phraseshort = shortphrase + addsymbol + addnum + addcaps
# Set all pass-strings for display in results frames.
self.share.tkdata['phrase_raw'].set(passphrase)
self.share.tkdata['pp_raw_len'].set(len(passphrase))
self.share.tkdata['phrase_plus'].set(phraseplus)
self.share.tkdata['pp_extra_len'].set(len(phraseplus))
self.share.tkdata['phrase_short'].set(phraseshort)
self.share.tkdata['pp_short_len'].set(len(phraseshort))
self.share.tkdata['pc_any'].set(passcode1)
self.share.tkdata['pc_any_len'].set(len(passcode1))
self.share.tkdata['pc_some'].set(passcode2)
self.share.tkdata['pc_some_len'].set(len(passcode2))
# Finally, set H values for each pass-string and configure results.
self.set_entropy(numwords, numchars)
self.config_results()
def set_entropy(self, numwords: int, numchars: int) -> None:
"""Calculate and set values for information entropy, H.
:param numwords: User-defined number of passphrase words.
:param numchars: User-defined number of passcode characters.
"""
# https://en.wikipedia.org/wiki/Password_strength
# For +3 characters, we use only 1 character each from each set of
# symbols, numbers, caps, so only need P of selecting one element
# from a set to obtain H, then sum all P.
# https://en.wikipedia.org/wiki/Entropy_(information_theory)
# Note that length of these string may reflect excluded characters.
h_symbol = -log(1 / len(STRING_DATA['symbols']), 2)
h_digit = -log(1 / len(STRING_DATA['digi']), 2)
h_cap = -log(1 / len(STRING_DATA['caps']), 2)
h_add3 = int(h_symbol + h_cap + h_digit) # H ~= 11
# Calculate information entropy, H = L * log N / log 2, where N is the
# number of possible characters or words and L is the number of
# characters or words in the pass-string. Log can be any base,
# but needs to be the same base in numerator and denominator.
# Note that N is corrected for any excluded words.
# Need to display H as integer, not float.
self.share.tkdata['pp_raw_h'].set(
int(numwords * log(len(LIST_DATA['word_list'])) / log(2)))
self.share.tkdata['pp_extra_h'].set(
self.share.tkdata['pp_raw_h'].get() + h_add3)
h_some = int(numwords * log(len(LIST_DATA['short_list'])) / log(2))
self.share.tkdata['pp_short_h'].set(h_some + h_add3)
self.share.tkdata['pc_any_h'].set(
int(numchars * log(len(STRING_DATA['all_char'])) / log(2)))
self.share.tkdata['pc_some_h'].set(
int(numchars * log(len(STRING_DATA['some_char'])) / log(2)))
def config_results(self) -> None:
"""
Configure fonts and display widths in results frames to provide
a more readable display of results. Called from make_pass().
"""
self.share.pp_raw_show.config(fg=COLORS['pass_fg'])
self.share.pp_extra_show.config(fg=COLORS['pass_fg'])
self.share.pp_short_show.config(fg=COLORS['pass_fg'])
self.share.pc_any_show.config(fg=COLORS['pass_fg'])
self.share.pc_some_show.config(fg=COLORS['pass_fg'])
# Need to indicate when passphrases exceeds length of result field,
# then reset to default when pass-string length is shortened.
# Use pp_extra_len, the likely longest passphrase, to trigger change.
passphrase_len = self.share.tkdata['pp_extra_len'].get()
# Need a special case for wider Chinese characters; 34 equivalent to 52
# Use 64% to generalize in case STRING_LENGTH changes.
results_width = const.STRING_LENGTH
if self.share.choose_wordlist.get() == '此開卷第 Story of the Stone' \
and passphrase_len > const.STRING_LENGTH * 0.64:
results_width = const.STRING_LENGTH * 0.64
if passphrase_len > results_width:
self.share.pp_raw_show.config(fg=COLORS['long_fg'])
self.share.pp_extra_show.config(fg=COLORS['long_fg'])
self.share.pp_short_show.config(fg=COLORS['long_fg'])
elif passphrase_len <= results_width:
self.share.pp_raw_show.config(fg=COLORS['pass_fg'])
self.share.pp_extra_show.config(fg=COLORS['pass_fg'])
self.share.pp_short_show.config(fg=COLORS['pass_fg'])
# Need to show right-most of phrase in case length exceeds field width.
self.share.pp_raw_show.xview_moveto(1)
self.share.pp_extra_show.xview_moveto(1)
self.share.pp_short_show.xview_moveto(1)
# Need to also indicate long passcodes.
passcode_len = int(self.share.numchars_entry.get())
if passcode_len > const.STRING_LENGTH:
self.share.pc_any_show.config(fg=COLORS['long_fg'])
self.share.pc_some_show.config(fg=COLORS['long_fg'])
elif passcode_len <= const.STRING_LENGTH:
self.share.pc_any_show.config(fg=COLORS['pass_fg'])
self.share.pc_some_show.config(fg=COLORS['pass_fg'])
def reset(self):
"""
Restore original word and character lists with default values.
Removes exclusions and current passphrase and passcode results.
Call get_words() to restore full word lists.
"""
self.share.tkdata['pc_any'].set('')
self.share.tkdata['pc_any_len'].set(0)
self.share.tkdata['pc_any_h'].set(0)
self.share.tkdata['pc_some'].set('')
self.share.tkdata['pc_some_len'].set(0)
self.share.tkdata['pc_some_h'].set(0)
self.share.exclude_entry.delete(0, 'end')
self.share.tkdata['excluded'].set('')
STRING_DATA['all_unused'] = ''
STRING_DATA['symbols'] = SYMBOLS
STRING_DATA['digi'] = digits
STRING_DATA['caps'] = ascii_uppercase
STRING_DATA['all_char'] = ascii_letters + digits + punctuation
STRING_DATA['some_char'] = ascii_letters + digits + SYMBOLS
self.get_words()
app.update_idletasks()
class PassViewer(tk.Canvas):
"""
Set up GUI widgets and display results from Modeler in the main
window.
"""
# Using __slots__ for Class attributes will decrease memory usage and
# may increase performance.
__slots__ = ('share',
'exclude_frame',
'generate_btn',
'l_and_h_header',
'l_and_h_header2',
'numchars_label',
'numwords_label',
'pc_any_head',
'pc_section_head',
'pc_some_head',
'pp_extra_head',
'pp_raw_head',
'pp_section_head',
'pp_short_head',
'quit_button',
'result_frame1',
'result_frame2',
)
def __init__(self, share):
super().__init__()
self.share = share
# Need to set up default fonts and sizes for all windows.
self.share.setfonts()
# Data widgets that are passed(shared) between Modeler and Viewer classes.
self.share.tkdata = {
'available': tk.IntVar(),
'pp_raw_h': tk.IntVar(),
'pp_extra_h': tk.IntVar(),
'pp_short_h': tk.IntVar(),
'pp_raw_len': tk.IntVar(),
'pp_extra_len': tk.IntVar(),
'pp_short_len': tk.IntVar(),
'phrase_raw': tk.StringVar(),
'phrase_plus': tk.StringVar(),
'phrase_short': tk.StringVar(),
'pc_any_len': tk.IntVar(),
'pc_some_len': tk.IntVar(),
'pc_any_h': tk.IntVar(),
'pc_some_h': tk.IntVar(),
'pc_any': tk.StringVar(),
'pc_some': tk.StringVar(),
'no_caps_chkbtn': tk.BooleanVar(),
'excluded': tk.StringVar()
}
# Passphrase section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Generally sorted by row.
self.share.choose_wordlist = ttk.Combobox()
self.share.available_head = tk.Label()
self.share.available_show = tk.Label()
self.numwords_label = tk.Label()
self.share.numwords_entry = tk.Entry()
self.l_and_h_header = tk.Label()
self.pp_section_head = tk.Label()
self.result_frame1 = ResultFrame1(share)
self.pp_raw_head = tk.Label()
self.pp_extra_head = tk.Label()
self.pp_short_head = tk.Label()
# End passphrase section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Passcode section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.result_frame2 = ResultFrame2(share)
self.pc_section_head = tk.Label()
self.numchars_label = tk.Label()
self.share.numchars_entry = tk.Entry()
self.l_and_h_header2 = tk.Label()
self.pc_any_head = tk.Label()
self.pc_some_head = tk.Label()
# End passcode section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.exclude_frame = ExcludeFrame(share)
self.share.compliment_txt = tk.Label()
# There are problems of tk.Button text showing up on macOS, so use ttk.
# Explicit styles are needed for buttons to show properly on MacOS.
# ... even then, background and pressed colors won't be recognized.
self.generate_btn = ttk.Button()
self.quit_button = ttk.Button()
self.config_widgets()
self.config_master()
self.config_buttons()
self.grid_master()
self.share.checkfiles()
self.share.getwords()
def config_widgets(self) -> None:
"""
All Class attributes are configured here.
:return: None
"""
# Set starting entries in pass-string fields in the tkdata dictionary.
result_fields = ('phrase_raw', 'phrase_plus', 'phrase_short',
'pc_any', 'pc_some')
for field in result_fields:
self.share.tkdata[field].set(value=STUBRESULT)
# Passphrase section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Generally sorted by row order.
self.share.choose_wordlist.configure(state='readonly', width=24)
self.share.choose_wordlist.bind('<<ComboboxSelected>>',
lambda _: self.share.getwords())
self.share.available_head.config(text='# available words:',
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.share.available_show.config(textvariable=self.share.tkdata['available'],
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.numwords_label.config(text='# words',
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.share.numwords_entry.config(width=3)
# Use 4 words as default passphrase length.
self.share.numwords_entry.insert(0, '4')
# MacOS needs a larger font and altered spacing than Linux or Windows.
if MY_OS == 'dar':
p_font = ('TkHeadingFont', 16)
hl_text = 'H L'
else:
p_font = ('TkHeadingFont', 12)
hl_text = ' H L'
self.l_and_h_header.config(text=hl_text, width=10,
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.pp_section_head.config(text='Passphrase wordlists',
font=p_font,
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.pp_raw_head.config(text="Any words from list",
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.pp_extra_head.config(text="... plus 3 characters",
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.pp_short_head.config(text="...words less than 7 letters",
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
# End passphrase section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Passcode section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.pc_section_head.config(text='Passcodes', font=p_font,
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.numchars_label.config(text='# characters',
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.share.numchars_entry.config(width=4)
self.share.numchars_entry.insert(tk.INSERT, '0')
self.l_and_h_header2.config(text=hl_text, width=10,
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.pc_any_head.config(text='Any characters',
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.pc_some_head.config(text="More likely usable characters",
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
# End passcode section %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.share.compliment_txt.config(relief='flat', border=0,
fg='orange', bg=COLORS['master_bg'])
def config_master(self) -> None:
"""Set up main window menus and bindings.
"""
# Note that self.master is an internal attribute and refers to the
# PassController Tk top window. Outside the PassViewer class,
# use 'app' to refer to the PassController Tk mainloop. However,
# in PassViewer, 'app' should be used in lambda functions.
self.config(bg=COLORS['master_bg'])
if MY_OS == 'dar':
ttk.Style().theme_use('alt')
# Need the master canvas to properly fill the app window.
self.master.columnconfigure(3, weight=1)
for _row in range(11):
self.master.rowconfigure(_row, weight=1)
# Set up universal and OS-specific keybindings and menus
self.master.bind_all('<Escape>', lambda _: utils.quit_gui(app))
self.master.bind('<Return>', lambda _: self.share.makepass())
self.master.bind('<KP_Enter>', lambda _: self.share.makepass())
cmdkey = 'Command' if MY_OS == 'dar' else 'Control' # is 'lin' or 'win'.
self.master.bind_all(f'<{f"{cmdkey}"}-equal>', lambda _: self.share.growfont())
self.master.bind_all(f'<{f"{cmdkey}"}-minus>', lambda _: self.share.shrinkfont())
self.master.bind(f'<{f"{cmdkey}"}-q>', lambda _: utils.quit_gui(app))
self.master.bind(f'<{f"{cmdkey}"}-g>', lambda _: self.share.makepass())
self.master.bind(f'<{f"{cmdkey}"}-o>', lambda _: self.share.scratchpad())
self.master.bind(f'<{f"{cmdkey}"}-r>', lambda _: self.share.reset())
self.master.bind('<Shift-Control-C>', lambda _: self.share.complimentme())
# Need to specify Ctrl-A for Linux b/c in tkinter that key is
# bound to <<LineStart>>, not <<SelectAll>>, for some reason?
if MY_OS in 'lin':
def select_all():
app.focus_get().event_generate('<<SelectAll>>')
self.master.bind_all('<Control-a>', lambda _: select_all())
# Need to specify OS-specific right-click mouse button only in
# pass-string fields of master window
right_button = '<Button-2>' if MY_OS == 'dar' else '<Button-3>'
self.share.pp_raw_show.bind(
f'{right_button}', lambda _: utils.click_cmds(app))
self.share.pp_extra_show.bind(
f'{right_button}', lambda _: utils.click_cmds(app))
self.share.pp_short_show.bind(
f'{right_button}', lambda _: utils.click_cmds(app))
self.share.pc_any_show.bind(
f'{right_button}', lambda _: utils.click_cmds(app))
self.share.pc_some_show.bind(
f'{right_button}', lambda _: utils.click_cmds(app))
# Create menu instance and add pull-down menus
menubar = tk.Menu(self.master)
self.master.config(menu=menubar)
os_accelerator = 'Command' if MY_OS == 'dar' else 'Ctrl'
file = tk.Menu(self.master, tearoff=0)
menubar.add_cascade(label='Passphrase', menu=file)
file.add_command(label='Generate',
command=self.share.makepass,
accelerator=f'{os_accelerator}+G')
file.add_command(label='Reset',
command=self.share.reset,
accelerator=f'{os_accelerator}+R')
file.add_command(label='Open a scratch pad',
command=self.share.scratchpad,
accelerator=f'{os_accelerator}+O')
file.add(tk.SEPARATOR)
file.add_command(label='Quit',
command=lambda: utils.quit_gui(app, gui=True),
# MacOS doesn't recognize 'Command+Q' as an accelerator
# b/c can't override that system's native Command+Q,
accelerator=f'{os_accelerator}+Q')
edit = tk.Menu(self.master, tearoff=0)
menubar.add_cascade(label='Edit', menu=edit)
edit.add_command(label='Select all',
command=lambda: app.focus_get().event_generate('<<SelectAll>>'),
accelerator=f'{os_accelerator}+A')
edit.add_command(label='Copy',
command=lambda: app.focus_get().event_generate('<<Copy>>'),
accelerator=f'{os_accelerator}+C')
edit.add_command(label='Paste',
command=lambda: app.focus_get().event_generate('<<Paste>>'),
accelerator=f'{os_accelerator}+V')
edit.add_command(label='Cut',
command=lambda: app.focus_get().event_generate('<<Cut>>'),
accelerator=f'{os_accelerator}+X')
view = tk.Menu(self.master, tearoff=0)
fontsize = tk.Menu(self.master, tearoff=0)
menubar.add_cascade(label='View', menu=view)
view.add_command(label='Font color changes?',
command=self.share.fontcolor)
view.add_cascade(label='Font size...', menu=fontsize)
# MacOS substitutes in appropriate key symbols for accelerators;
# Linux and Windows just use the literal strings.
if MY_OS in 'lin, win':
fontsize.add_command(label='Bigger font',
command=self.share.growfont,
accelerator=f'{os_accelerator}+=(plus)')
fontsize.add_command(label='Smaller font',
command=self.share.shrinkfont,
accelerator=f'{os_accelerator}+-(minus)')
elif MY_OS == 'dar':
fontsize.add_command(label='Bigger font',
command=self.share.growfont,
accelerator=f'{os_accelerator}+=')
fontsize.add_command(label='Smaller font',
command=self.share.shrinkfont,
accelerator=f'{os_accelerator}+-')
fontsize.add_command(label='Default size',
command=self.share.defaultfontsize)
help_menu = tk.Menu(self.master, tearoff=0)
tips = tk.Menu(self.master, tearoff=0)
menubar.add_cascade(label='Help', menu=help_menu)
help_menu.add_cascade(label='Tips...', menu=tips)
tips.add_command(label='Mouse right-click does stuff!')
tips.add_command(label=' ...So do common keyboard commands.')
tips.add_command(label='Return/Enter key also Generates.')
tips.add_command(label='Menu Passphrase>Open.. opens a scratch pad.')
tips.add_command(label='Very long results may be in blue font.')
tips.add_command(label='Esc key exits program from any window.')
help_menu.add_command(label="What's going on here?",
command=self.share.explain)
help_menu.add_command(label='About',
command=self.share.about)
# Need Ctrl+Shift+C for all OS b/c Command-Shift-C is a macOS
# default for color palette.
help_menu.add_command(label="I need a compliment",
command=self.share.complimentme,
accelerator='Ctrl+Shift+C')
def config_buttons(self) -> None:
"""Set up all buttons used in master window.
"""
# There are problems of tk.Button text showing up on macOS, so use ttk.
# Explicit styles are needed for buttons to show properly on macOS.
# ... even then, background and pressed colors won't be recognized.
style = ttk.Style()
style.map("My.TButton",
foreground=[('active', COLORS['pass_fg'])],
background=[('pressed', COLORS['dataframe_bg']),
('active', COLORS['pass_bg'])])
self.generate_btn.configure(style="My.TButton", text='Generate!',
command=self.share.makepass)
self.generate_btn.focus()
self.quit_button.configure(style="My.TButton", text='Quit',
width=0,
command=lambda: utils.quit_gui(app, gui=True))
def grid_master(self) -> None:
"""Grid widgets in master (Canvas).
"""
# This self.grid fills out the inherited tk.Frame, padding gives border.
# Padding depends on app.minsize/maxsize in PassController
# Frame background color, COLORS['master_bg'], is set in config_master().
self.grid(column=0, row=0,
rowspan=12, columnspan=4,
padx=3, pady=3, sticky=tk.NSEW)
# Grid the Frames.
self.result_frame1.grid(column=1, row=2,
padx=(5, 10), ipady=5,
columnspan=3, rowspan=3, sticky=tk.NSEW)
self.result_frame2.grid(column=1, row=7,
padx=(5, 10), ipady=5,
columnspan=3, rowspan=2, sticky=tk.NSEW)
self.exclude_frame.grid(column=0, row=9,
padx=(10, 0), pady=(10, 10),
columnspan=2, rowspan=3, sticky=tk.NSEW)
# Widgets not gridded here are gridded in their respective Frame Classes.
# %%%%%%%%%%%%%%%%%%%%%%%% sorted by row number %%%%%%%%%%%%%%%%%%%%%%%
# Passphrase widgets %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.pp_section_head.grid(column=0, row=0, pady=(10, 5),
padx=(10, 5), sticky=tk.W)
self.share.choose_wordlist.grid(column=1, row=0, pady=(10, 5), padx=5,
columnspan=2, sticky=tk.W)
self.share.available_head.grid(column=3, row=0, pady=(10, 0),
padx=(5, 0), sticky=tk.W)
# Need separate Label spacing for each OS:
padx = (124, 0) if MY_OS == 'dar' else (130, 0) # 'lin' or 'win'.
self.share.available_show.grid(column=3, row=0, pady=(10, 0),
padx=padx, sticky=tk.W)
self.numwords_label.grid(column=0, row=1, padx=(10, 5), sticky=tk.W)
self.share.numwords_entry.grid(
column=0, row=1, padx=(10, 90), sticky=tk.E)
self.l_and_h_header.grid(column=1, row=1, padx=0, sticky=tk.W)
self.pp_raw_head.grid(column=0, row=2, pady=(6, 4), padx=(10, 0),
sticky=tk.E)
self.pp_extra_head.grid(column=0, row=3, pady=(2, 2), padx=(10, 0),
sticky=tk.E)
self.pp_short_head.grid(column=0, row=4, pady=(4, 6), padx=(10, 0),
sticky=tk.E)
# Need to pad and span to center the button between two results frames.
# Different x-padding keeps alignment in different platforms.
if MY_OS == 'lin':
padx = (40, 0)
elif MY_OS == 'win':
padx = (30, 0)
else: # is macOS
padx = (0, 0)
self.generate_btn.grid(column=3, row=5, rowspan=2,
padx=padx, pady=(10, 5), sticky=tk.W)
# Passcode widgets & others %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
self.pc_section_head.grid(column=0, row=5,
pady=(12, 6), padx=(10, 5),
sticky=tk.W)
self.numchars_label.grid(column=0, row=6,
pady=0, padx=(10, 5),
sticky=tk.W)
self.share.numchars_entry.grid(column=0, row=6,
pady=0, padx=(0, 55),
sticky=tk.E)
self.l_and_h_header2.grid(column=1, row=6,
pady=0, padx=0,
sticky=tk.W)
self.pc_any_head.grid(column=0, row=7,
pady=(6, 0), padx=(10, 0),
sticky=tk.E)
self.pc_some_head.grid(column=0, row=8,
pady=(0, 6), padx=(10, 0),
sticky=tk.E)
self.quit_button.grid(column=3, row=11,
pady=(0, 15), padx=(0, 15),
sticky=tk.E)
# Compliment text will display to right of Exclude Frame.
self.share.compliment_txt.grid(column=2, row=11, columnspan=2,
pady=(0, 15), padx=5,
sticky=tk.W)
class ResultFrame1(tk.Frame):
"""
The frame that contains the generated passphrase results and metrics.
"""
__slots__ = ('pp_extra_h_lbl', 'pp_extra_len_lbl', 'pp_raw_h_lbl',
'pp_raw_len_lbl', 'pp_short_h_lbl', 'pp_short_len_lbl')
def __init__(self, share):
super().__init__()
self.share = share
self.config(borderwidth=3,
relief='sunken',
background=COLORS['dataframe_bg'])
for _row in range(3):
self.rowconfigure(index=_row, weight=1)
self.columnconfigure(index=2, weight=1)
# Results are shown as Entry() instead of Text() b/c textvariable
# is easier to code than .insert(). Otherwise, identical.
label_param = dict(fg=COLORS['master_fg'],
bg=COLORS['dataframe_bg'],
width=3)
entry_param = dict(font=self.share.result_font,
fg=COLORS['stubpass_fg'],
bg=COLORS['pass_bg'],
width=const.STRING_LENGTH)
self.pp_raw_h_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_raw_h'])
self.pp_raw_len_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_raw_len'])
self.share.pp_raw_show = tk.Entry(self, **entry_param,
textvariable=self.share.tkdata['phrase_raw'])
self.pp_extra_h_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_extra_h'])
self.pp_extra_len_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_extra_len'])
self.share.pp_extra_show = tk.Entry(self, **entry_param,
textvariable=self.share.tkdata['phrase_plus'])
self.pp_short_h_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_short_h'])
self.pp_short_len_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pp_short_len'])
self.share.pp_short_show = tk.Entry(self, **entry_param,
textvariable=self.share.tkdata['phrase_short'])
# Grid all here for easy comparisons.
self.pp_raw_h_lbl.grid(column=0, row=0, padx=(5, 0), pady=(6, 4))
self.pp_raw_len_lbl.grid(column=1, row=0, padx=(5, 0))
self.share.pp_raw_show.grid(column=2, row=0, padx=5,
sticky=tk.EW)
self.pp_extra_h_lbl.grid(column=0, row=1, padx=(5, 0), pady=(2, 2))
self.pp_extra_len_lbl.grid(column=1, row=1, padx=(5, 0))
self.share.pp_extra_show.grid(column=2, row=1, padx=5,
sticky=tk.EW)
self.pp_short_h_lbl.grid(column=0, row=2, padx=(5, 0), pady=(4, 6))
self.pp_short_len_lbl.grid(column=1, row=2, padx=(5, 0))
self.share.pp_short_show.grid(column=2, row=2, padx=5,
sticky=tk.EW)
class ResultFrame2(tk.Frame):
"""
The frame that contains the generated passcode results and metrics.
"""
__slots__ = ('pc_any_h_lbl', 'pc_any_len_lbl', 'pc_some_h_lbl',
'pc_some_len_lbl',)
def __init__(self, share):
super().__init__()
self.share = share
self.config(borderwidth=3,
relief='sunken',
background=COLORS['dataframe_bg'])
for _row in range(2):
self.rowconfigure(_row, weight=1)
self.columnconfigure(index=2, weight=1)
# Results are shown as Entry() instead of Text() b/c textvariable
# is easier to code than .insert(). Otherwise, identical.
label_param = dict(fg=COLORS['master_fg'],
bg=COLORS['dataframe_bg'],
width=3)
entry_param = dict(font=self.share.result_font,
fg=COLORS['stubpass_fg'],
bg=COLORS['pass_bg'],
width=const.STRING_LENGTH)
self.pc_any_len_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pc_any_len'])
self.pc_any_h_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pc_any_h'])
self.share.pc_any_show = tk.Entry(self, **entry_param,
textvariable=self.share.tkdata['pc_any'])
self.pc_some_len_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pc_some_len'])
self.pc_some_h_lbl = tk.Label(self, **label_param,
textvariable=self.share.tkdata['pc_some_h'])
self.share.pc_some_show = tk.Entry(self, **entry_param,
textvariable=self.share.tkdata['pc_some'])
# Grid all here for easy comparisons.
# Results' *_show grids maintain equal widths when sticky=tk.EW.
self.pc_any_h_lbl.grid(column=0, row=0, padx=(5, 0), pady=(6, 3))
self.pc_any_len_lbl.grid(column=1, row=0, padx=(5, 0))
self.share.pc_any_show.grid(column=2, row=0, padx=5,
columnspan=2, sticky=tk.EW)
self.pc_some_h_lbl.grid(column=0, row=1, padx=(5, 0), pady=(3, 6))
self.pc_some_len_lbl.grid(column=1, row=1, padx=(5, 0))
self.share.pc_some_show.grid(column=2, row=1, padx=5,
columnspan=2, sticky=tk.EW)
class ExcludeFrame(tk.Frame):
"""
The frame that contains widget for excluding specific characters.
"""
__slots__ = ('exclude_head', 'exclude_info_b', 'excluded_head',
'excluded_show', 'no_caps_chkbtn', 'no_caps_head',
'reset_button',)
def __init__(self, share):
super().__init__()
self.share = share
self.configure(borderwidth=3,
relief='ridge',
bg=COLORS['master_bg'])
self.exclude_head = tk.Label(self,
text='Exclude character(s):',
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.share.exclude_entry = tk.Entry(self, width=2)
self.excluded_head = tk.Label(self,
text='Currently excluded:',
fg=COLORS['master_fg'], bg=COLORS['master_bg'])
self.excluded_show = tk.Label(self,
textvariable=self.share.tkdata['excluded'],
fg='orange', bg=COLORS['master_bg'])
self.no_caps_head = tk.Label(self,
text='No capitalized words',
fg=COLORS['pass_bg'], bg=COLORS['master_bg'])
self.no_caps_chkbtn = tk.Checkbutton(self,
variable=self.share.tkdata['no_caps_chkbtn'],
command=self.share.getwords,
bg=COLORS['master_bg'])
# There are problems of tk.Button text showing up on macOS, so use ttk.
# Explicit styles are needed for buttons to show properly on macOS.
# ... even then, background and pressed colors won't be recognized.
self.reset_button = ttk.Button(self, style="My.TButton",
text='Reset',
width=0,
command=self.share.reset)
self.exclude_info_b = ttk.Button(self, style="My.TButton",
text='?',
width=0,
command=self.share.excludemsg)
# Grid all here for easy comparisons.