ghostbytes 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghostbytes/__init__.py +3 -0
- ghostbytes/__main__.py +9 -0
- ghostbytes/crypto/config.py +155 -0
- ghostbytes/crypto/crypto.py +162 -0
- ghostbytes/crypto/crypto.pyi +149 -0
- ghostbytes/crypto/kyber.py +257 -0
- ghostbytes/crypto/oaep_extension.py +125 -0
- ghostbytes/crypto/primitives.py +360 -0
- ghostbytes/error.py +177 -0
- ghostbytes/gui/gui.py +2809 -0
- ghostbytes/gui/theme.py +95 -0
- ghostbytes/gui/wrappers.py +566 -0
- ghostbytes/img/icon.ico +0 -0
- ghostbytes/img/icon.png +0 -0
- ghostbytes/tools/benchmark.py +218 -0
- ghostbytes/tools/rand.py +118 -0
- ghostbytes/tools/shred.py +384 -0
- ghostbytes/tools/tools.pyi +67 -0
- ghostbytes-1.0.0.dist-info/METADATA +182 -0
- ghostbytes-1.0.0.dist-info/RECORD +22 -0
- ghostbytes-1.0.0.dist-info/WHEEL +4 -0
- ghostbytes-1.0.0.dist-info/entry_points.txt +3 -0
ghostbytes/gui/gui.py
ADDED
|
@@ -0,0 +1,2809 @@
|
|
|
1
|
+
"""Ghostbytes desktop application interface."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import ctypes
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import textwrap
|
|
8
|
+
import threading
|
|
9
|
+
import webbrowser
|
|
10
|
+
from tkinter import PhotoImage, filedialog
|
|
11
|
+
|
|
12
|
+
import customtkinter as ctk
|
|
13
|
+
from CTkMessagebox import CTkMessagebox
|
|
14
|
+
from ctkfontawesome import icon_to_ctkimage
|
|
15
|
+
from PIL import Image
|
|
16
|
+
from ghostbytes import __version__, __license__, __link__
|
|
17
|
+
from ghostbytes.crypto.config import (
|
|
18
|
+
AVAIL_ALG,
|
|
19
|
+
AVAIL_HASH,
|
|
20
|
+
AVAIL_HASH_STR,
|
|
21
|
+
AVAIL_RANDOM_STR,
|
|
22
|
+
ENCRYPTED_SUFFIX,
|
|
23
|
+
RSA_KEY_OUT_FORMAT,
|
|
24
|
+
OVERWRITE_OPTIONS,
|
|
25
|
+
COMMON_RSA_SIZE,
|
|
26
|
+
RSA_SIZE_WARNING_THRESHOLD,
|
|
27
|
+
CryptoConfig,
|
|
28
|
+
)
|
|
29
|
+
from ghostbytes.error import GeneralError, invalid_argument
|
|
30
|
+
from ghostbytes.gui import wrappers as wr
|
|
31
|
+
from ghostbytes.gui.theme import (
|
|
32
|
+
ABOUT_BOX_CONTENT,
|
|
33
|
+
ACCENT,
|
|
34
|
+
ACCENT_BORDER,
|
|
35
|
+
ACCENT_HOVER,
|
|
36
|
+
ACCENT_ON,
|
|
37
|
+
ACCENT_TINT,
|
|
38
|
+
ACTIONS,
|
|
39
|
+
CARD_HEIGHT,
|
|
40
|
+
CARD_WIDTH,
|
|
41
|
+
CAPABILITIES,
|
|
42
|
+
DANGER,
|
|
43
|
+
ICON_ICO,
|
|
44
|
+
ICON_PNG,
|
|
45
|
+
MONO_FONT,
|
|
46
|
+
SIDEBAR_LABELS,
|
|
47
|
+
SUCCESS,
|
|
48
|
+
TABS,
|
|
49
|
+
THEME,
|
|
50
|
+
WARNING,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def _set_windows_app_id():
|
|
54
|
+
if os.name == "nt":
|
|
55
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
|
56
|
+
"Ghostbytes.Ghostbytes")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class App(ctk.CTk):
|
|
60
|
+
"""Main Ghostbytes application window."""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
_set_windows_app_id()
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self.title("Ghostbytes - An file encryption utility")
|
|
67
|
+
self._set_appearance_mode("system")
|
|
68
|
+
try:
|
|
69
|
+
if os.name == "nt":
|
|
70
|
+
self.iconbitmap(default=str(ICON_ICO))
|
|
71
|
+
else:
|
|
72
|
+
self._taskbar_icon = PhotoImage(file=str(ICON_PNG))
|
|
73
|
+
self.iconphoto(True, self._taskbar_icon)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
self.geometry("1200x750")
|
|
77
|
+
self.propagate(False)
|
|
78
|
+
self.grid_rowconfigure(0, weight=1)
|
|
79
|
+
self.grid_columnconfigure(1, weight=1)
|
|
80
|
+
|
|
81
|
+
# Session-scoped "don't warn me again" flag for the large-RSA-key
|
|
82
|
+
# warning (generation & use of keys above RSA_SIZE_WARNING_THRESHOLD).
|
|
83
|
+
self._suppress_rsa_warning = False
|
|
84
|
+
|
|
85
|
+
self.sidebar_buttons = []
|
|
86
|
+
self._nav_icons = {}
|
|
87
|
+
self.content = ctk.CTkScrollableFrame(
|
|
88
|
+
self,
|
|
89
|
+
corner_radius=0,
|
|
90
|
+
fg_color=THEME["content_bg"]
|
|
91
|
+
)
|
|
92
|
+
self.content.grid(row=0, column=1, sticky="nsew")
|
|
93
|
+
self.content.grid_columnconfigure(0, weight=1)
|
|
94
|
+
|
|
95
|
+
self.tab = TABS["HEADER"][0]
|
|
96
|
+
self._build_sidebar()
|
|
97
|
+
self._build_context()
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------ #
|
|
100
|
+
# Sidebar / navigation
|
|
101
|
+
# ------------------------------------------------------------------ #
|
|
102
|
+
|
|
103
|
+
def _build_sidebar(self):
|
|
104
|
+
sidebar = ctk.CTkFrame(self, width=210, fg_color=THEME["sidebar_bg"])
|
|
105
|
+
sidebar.propagate(False)
|
|
106
|
+
sidebar.grid(row=0, column=0, sticky="nsw")
|
|
107
|
+
|
|
108
|
+
brand = ctk.CTkFrame(sidebar, fg_color="transparent")
|
|
109
|
+
brand.grid(row=0, column=0, sticky="ew", padx=18, pady=(22, 16))
|
|
110
|
+
ctk.CTkLabel(
|
|
111
|
+
brand,
|
|
112
|
+
text="Ghostbytes",
|
|
113
|
+
text_color=THEME["content_text"],
|
|
114
|
+
font=self._font(
|
|
115
|
+
16,
|
|
116
|
+
"bold")).pack(
|
|
117
|
+
side="left",
|
|
118
|
+
padx=(
|
|
119
|
+
8,
|
|
120
|
+
0))
|
|
121
|
+
|
|
122
|
+
row_counter = 1
|
|
123
|
+
for group, tabs in TABS.items():
|
|
124
|
+
if group not in ("HEADER", "FOOTER"):
|
|
125
|
+
label = ctk.CTkLabel(
|
|
126
|
+
sidebar, text_color=THEME["gray_text"], font=self._font(
|
|
127
|
+
11, weight="bold"), text=SIDEBAR_LABELS.get(
|
|
128
|
+
group, group.title()), anchor="w")
|
|
129
|
+
label.grid(
|
|
130
|
+
row=row_counter,
|
|
131
|
+
column=0,
|
|
132
|
+
sticky="ew",
|
|
133
|
+
padx=18,
|
|
134
|
+
pady=(
|
|
135
|
+
14,
|
|
136
|
+
3))
|
|
137
|
+
row_counter += 1
|
|
138
|
+
for tab in tabs:
|
|
139
|
+
active_icon = icon_to_ctkimage(
|
|
140
|
+
tab[0], fill=ACCENT, scale_to_width=15)
|
|
141
|
+
inactive_icon = icon_to_ctkimage(
|
|
142
|
+
tab[0], fill=THEME["text_fg"], scale_to_width=15)
|
|
143
|
+
is_active = tab == self.tab
|
|
144
|
+
tab_button = ctk.CTkButton(
|
|
145
|
+
sidebar,
|
|
146
|
+
anchor="w",
|
|
147
|
+
corner_radius=8,
|
|
148
|
+
text_color=ACCENT if is_active else THEME["text_fg"],
|
|
149
|
+
fg_color=ACCENT_TINT if is_active else "transparent",
|
|
150
|
+
hover_color=THEME["box_color"],
|
|
151
|
+
font=self._font(size=13),
|
|
152
|
+
text=tab[1],
|
|
153
|
+
image=active_icon if is_active else inactive_icon,
|
|
154
|
+
command=lambda tab=tab: self._change_tabs(tab)
|
|
155
|
+
)
|
|
156
|
+
tab_button.grid(
|
|
157
|
+
row=row_counter,
|
|
158
|
+
column=0,
|
|
159
|
+
sticky="ew",
|
|
160
|
+
padx=12,
|
|
161
|
+
pady=2 if group != "FOOTER" else (30, 8)
|
|
162
|
+
)
|
|
163
|
+
self._nav_icons[tab_button] = (active_icon, inactive_icon)
|
|
164
|
+
self.sidebar_buttons.append(tab_button)
|
|
165
|
+
row_counter += 1
|
|
166
|
+
sidebar.grid_rowconfigure(row_counter, weight=2)
|
|
167
|
+
|
|
168
|
+
def _change_tabs(self, new_tab):
|
|
169
|
+
self.tab = new_tab
|
|
170
|
+
for btn in self.sidebar_buttons:
|
|
171
|
+
active_icon, inactive_icon = self._nav_icons[btn]
|
|
172
|
+
if btn._text == new_tab[1]:
|
|
173
|
+
btn.configure(
|
|
174
|
+
fg_color=ACCENT_TINT,
|
|
175
|
+
text_color=ACCENT,
|
|
176
|
+
image=active_icon)
|
|
177
|
+
else:
|
|
178
|
+
btn.configure(
|
|
179
|
+
fg_color="transparent",
|
|
180
|
+
text_color=THEME["text_fg"],
|
|
181
|
+
image=inactive_icon)
|
|
182
|
+
|
|
183
|
+
self._build_context()
|
|
184
|
+
|
|
185
|
+
def _find_tab(self, label):
|
|
186
|
+
for tabs in TABS.values():
|
|
187
|
+
for tab in tabs:
|
|
188
|
+
if tab[1] == label:
|
|
189
|
+
return tab
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _build_context(self):
|
|
193
|
+
switch = {
|
|
194
|
+
"Home": self._build_home,
|
|
195
|
+
"Encrypt / Decrypt": self._build_crypto,
|
|
196
|
+
"Generate Key Pair": self._build_genkey,
|
|
197
|
+
"Verify Key Pair": self._build_verifykey,
|
|
198
|
+
"Key Information": self._build_keyinfo,
|
|
199
|
+
"Hash File(s) (Checksum)": self._build_hashfile,
|
|
200
|
+
"Random": self._build_random,
|
|
201
|
+
"Password Generator": self._build_pwdgen,
|
|
202
|
+
"Benchmark": self._build_benchmark,
|
|
203
|
+
"Secure Delete": self._build_sdelete,
|
|
204
|
+
"Wipe Free Space": self._build_wipe_space,
|
|
205
|
+
"About": self._build_about
|
|
206
|
+
}
|
|
207
|
+
if self.tab[1] not in switch:
|
|
208
|
+
self._error_win(
|
|
209
|
+
"GUI Internal Error",
|
|
210
|
+
f"Cannot find build function for tab `{
|
|
211
|
+
self.tab[1]}`",
|
|
212
|
+
True)
|
|
213
|
+
|
|
214
|
+
for widget in self.content.winfo_children():
|
|
215
|
+
widget.destroy()
|
|
216
|
+
switch[self.tab[1]]()
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------ #
|
|
219
|
+
# Home
|
|
220
|
+
# ------------------------------------------------------------------ #
|
|
221
|
+
|
|
222
|
+
def _build_home(self):
|
|
223
|
+
app_details = ctk.CTkFrame(
|
|
224
|
+
self.content,
|
|
225
|
+
corner_radius=0,
|
|
226
|
+
fg_color="transparent")
|
|
227
|
+
app_details.grid(row=0, column=0, sticky="new", padx=32, pady=(28, 18))
|
|
228
|
+
app_details.grid_columnconfigure(1, weight=1)
|
|
229
|
+
app_details.grid_columnconfigure(2, weight=0, minsize=250)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
icon = ctk.CTkImage(
|
|
233
|
+
Image.open(ICON_PNG),
|
|
234
|
+
Image.open(ICON_PNG),
|
|
235
|
+
(64, 64)
|
|
236
|
+
)
|
|
237
|
+
ctk.CTkLabel(
|
|
238
|
+
app_details,
|
|
239
|
+
image=icon,
|
|
240
|
+
text="",
|
|
241
|
+
width=64).grid(
|
|
242
|
+
row=0,
|
|
243
|
+
column=0,
|
|
244
|
+
sticky="nw")
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
text_frame = ctk.CTkFrame(app_details, fg_color="transparent")
|
|
249
|
+
text_frame.grid(row=0, column=1, sticky="new", padx=(15, 0))
|
|
250
|
+
|
|
251
|
+
ctk.CTkLabel(
|
|
252
|
+
text_frame, text="Ghostbytes", text_color=THEME["content_text"],
|
|
253
|
+
font=self._font(24, "bold"), anchor="w"
|
|
254
|
+
).pack(anchor="w")
|
|
255
|
+
ctk.CTkLabel(
|
|
256
|
+
text_frame, text="File Encryption Utility", text_color=ACCENT,
|
|
257
|
+
font=self._font(13, "bold"), anchor="w"
|
|
258
|
+
).pack(anchor="w")
|
|
259
|
+
ctk.CTkLabel(
|
|
260
|
+
text_frame,
|
|
261
|
+
text="Military-grade cryptographic toolkit for file encryption, key "
|
|
262
|
+
"management, and secure data handling. Built for privacy.",
|
|
263
|
+
text_color=THEME["slight_gray"],
|
|
264
|
+
font=self._font(12),
|
|
265
|
+
anchor="w",
|
|
266
|
+
justify="left",
|
|
267
|
+
wraplength=500).pack(
|
|
268
|
+
anchor="w",
|
|
269
|
+
pady=(
|
|
270
|
+
4,
|
|
271
|
+
0))
|
|
272
|
+
|
|
273
|
+
about = ctk.CTkFrame(
|
|
274
|
+
app_details,
|
|
275
|
+
fg_color=THEME["box_color"],
|
|
276
|
+
border_width=1,
|
|
277
|
+
border_color=THEME["box_border"],
|
|
278
|
+
corner_radius=THEME["corner_rad"])
|
|
279
|
+
about.grid(row=0, column=2, sticky="ne", padx=(24, 0))
|
|
280
|
+
about.grid_columnconfigure(1, weight=1)
|
|
281
|
+
ctk.CTkLabel(
|
|
282
|
+
about,
|
|
283
|
+
text="About Ghostbytes",
|
|
284
|
+
text_color=THEME["content_text"],
|
|
285
|
+
font=self._font(
|
|
286
|
+
13,
|
|
287
|
+
"bold"),
|
|
288
|
+
anchor="w").grid(
|
|
289
|
+
row=0,
|
|
290
|
+
column=0,
|
|
291
|
+
columnspan=2,
|
|
292
|
+
sticky="w",
|
|
293
|
+
padx=14,
|
|
294
|
+
pady=(
|
|
295
|
+
10,
|
|
296
|
+
4))
|
|
297
|
+
for index, (label, value) in enumerate(
|
|
298
|
+
(("Version", __version__), ("License", __license__), ("Source", __link__))):
|
|
299
|
+
ctk.CTkLabel(
|
|
300
|
+
about,
|
|
301
|
+
text=label,
|
|
302
|
+
text_color=THEME["slight_gray"],
|
|
303
|
+
font=self._font(10),
|
|
304
|
+
anchor="w").grid(
|
|
305
|
+
row=index + 1,
|
|
306
|
+
column=0,
|
|
307
|
+
sticky="w",
|
|
308
|
+
padx=14,
|
|
309
|
+
pady=2)
|
|
310
|
+
value_label = ctk.CTkLabel(
|
|
311
|
+
about,
|
|
312
|
+
text=value,
|
|
313
|
+
text_color=ACCENT if label == "Source" else THEME["content_text"],
|
|
314
|
+
font=self._mono_font(10),
|
|
315
|
+
anchor="w",
|
|
316
|
+
justify="left",
|
|
317
|
+
wraplength=185,
|
|
318
|
+
cursor="hand2" if label == "Source" else None)
|
|
319
|
+
value_label.grid(
|
|
320
|
+
row=index + 1,
|
|
321
|
+
column=1,
|
|
322
|
+
sticky="w",
|
|
323
|
+
padx=(
|
|
324
|
+
8,
|
|
325
|
+
14),
|
|
326
|
+
pady=2)
|
|
327
|
+
if label == "Source":
|
|
328
|
+
value_label.bind(
|
|
329
|
+
"<Button-1>",
|
|
330
|
+
lambda _event,
|
|
331
|
+
url=value: webbrowser.open_new_tab(url))
|
|
332
|
+
ctk.CTkLabel(
|
|
333
|
+
about,
|
|
334
|
+
text="Privacy is not a feature. It's a foundation",
|
|
335
|
+
text_color=THEME["slight_gray"],
|
|
336
|
+
font=self._font(
|
|
337
|
+
12,
|
|
338
|
+
"bold"),
|
|
339
|
+
anchor="w").grid(
|
|
340
|
+
row=4,
|
|
341
|
+
column=0,
|
|
342
|
+
columnspan=2,
|
|
343
|
+
sticky="w",
|
|
344
|
+
padx=14,
|
|
345
|
+
pady=(
|
|
346
|
+
8,
|
|
347
|
+
10))
|
|
348
|
+
|
|
349
|
+
divider = ctk.CTkFrame(
|
|
350
|
+
self.content,
|
|
351
|
+
height=1,
|
|
352
|
+
fg_color=THEME["box_border"])
|
|
353
|
+
divider.grid(row=1, column=0, sticky="ew", padx=32)
|
|
354
|
+
|
|
355
|
+
quick_actions = ctk.CTkFrame(self.content, fg_color="transparent")
|
|
356
|
+
quick_actions.grid(
|
|
357
|
+
row=2,
|
|
358
|
+
column=0,
|
|
359
|
+
sticky="new",
|
|
360
|
+
padx=32,
|
|
361
|
+
pady=(
|
|
362
|
+
20,
|
|
363
|
+
20))
|
|
364
|
+
|
|
365
|
+
ctk.CTkLabel(
|
|
366
|
+
quick_actions,
|
|
367
|
+
text="Quick Actions",
|
|
368
|
+
text_color=THEME["content_text"],
|
|
369
|
+
font=self._font(
|
|
370
|
+
15,
|
|
371
|
+
"bold"),
|
|
372
|
+
anchor="w").grid(
|
|
373
|
+
row=0,
|
|
374
|
+
column=0,
|
|
375
|
+
sticky="w",
|
|
376
|
+
pady=(
|
|
377
|
+
0,
|
|
378
|
+
5))
|
|
379
|
+
|
|
380
|
+
cards = [
|
|
381
|
+
self._build_action_card(quick_actions, icon_name, title, desc, target)
|
|
382
|
+
for icon_name, title, desc, target in ACTIONS
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
quick_actions.bind(
|
|
386
|
+
"<Configure>",
|
|
387
|
+
lambda e: self._home_organise(
|
|
388
|
+
quick_actions,
|
|
389
|
+
cards))
|
|
390
|
+
self.after(50, lambda: self._home_organise(quick_actions, cards))
|
|
391
|
+
|
|
392
|
+
def _build_action_card(self, parent, icon_name, title, desc, target):
|
|
393
|
+
card = ctk.CTkFrame(
|
|
394
|
+
parent,
|
|
395
|
+
width=CARD_WIDTH,
|
|
396
|
+
height=CARD_HEIGHT,
|
|
397
|
+
corner_radius=THEME["corner_rad"],
|
|
398
|
+
fg_color=THEME["box_color"],
|
|
399
|
+
border_color=THEME["box_border"],
|
|
400
|
+
border_width=THEME["box_border_width"],
|
|
401
|
+
cursor="hand2")
|
|
402
|
+
card.grid_propagate(False)
|
|
403
|
+
card.grid_columnconfigure(1, weight=1)
|
|
404
|
+
|
|
405
|
+
badge = ctk.CTkFrame(
|
|
406
|
+
card,
|
|
407
|
+
width=32,
|
|
408
|
+
height=32,
|
|
409
|
+
corner_radius=8,
|
|
410
|
+
fg_color=ACCENT_TINT)
|
|
411
|
+
badge.grid(row=0, column=0, rowspan=2, sticky="nw", padx=12, pady=12)
|
|
412
|
+
badge.grid_propagate(False)
|
|
413
|
+
ctk.CTkLabel(
|
|
414
|
+
badge,
|
|
415
|
+
text="",
|
|
416
|
+
image=icon_to_ctkimage(
|
|
417
|
+
icon_name,
|
|
418
|
+
fill=ACCENT,
|
|
419
|
+
scale_to_width=14)).place(
|
|
420
|
+
relx=0.5,
|
|
421
|
+
rely=0.5,
|
|
422
|
+
anchor="center")
|
|
423
|
+
|
|
424
|
+
ctk.CTkLabel(
|
|
425
|
+
card, text=title, text_color=THEME["content_text"],
|
|
426
|
+
font=self._font(13, "bold"), anchor="w"
|
|
427
|
+
).grid(row=0, column=1, sticky="sw", padx=(0, 10), pady=(14, 0))
|
|
428
|
+
|
|
429
|
+
ctk.CTkLabel(
|
|
430
|
+
card,
|
|
431
|
+
text=desc,
|
|
432
|
+
text_color=THEME["slight_gray"],
|
|
433
|
+
font=self._font(11),
|
|
434
|
+
anchor="nw",
|
|
435
|
+
justify="left",
|
|
436
|
+
wraplength=CARD_WIDTH -
|
|
437
|
+
60).grid(
|
|
438
|
+
row=1,
|
|
439
|
+
column=1,
|
|
440
|
+
sticky="nw",
|
|
441
|
+
padx=(
|
|
442
|
+
0,
|
|
443
|
+
10),
|
|
444
|
+
pady=(
|
|
445
|
+
2,
|
|
446
|
+
10))
|
|
447
|
+
|
|
448
|
+
def go(_event=None):
|
|
449
|
+
tab = self._find_tab(target)
|
|
450
|
+
if tab:
|
|
451
|
+
self._change_tabs(tab)
|
|
452
|
+
|
|
453
|
+
self._bind_recursive(card, go)
|
|
454
|
+
card.bind("<Enter>", lambda e: card.configure(border_color=ACCENT))
|
|
455
|
+
card.bind(
|
|
456
|
+
"<Leave>", lambda e: card.configure(
|
|
457
|
+
border_color=THEME["box_border"]))
|
|
458
|
+
|
|
459
|
+
return card
|
|
460
|
+
|
|
461
|
+
def _bind_recursive(self, widget, command):
|
|
462
|
+
widget.bind("<Button-1>", command)
|
|
463
|
+
for child in widget.winfo_children():
|
|
464
|
+
self._bind_recursive(child, command)
|
|
465
|
+
|
|
466
|
+
def _home_organise(self, quick_actions, cards):
|
|
467
|
+
"""Re-grid action cards into as many columns as fit the current frame width."""
|
|
468
|
+
if not quick_actions.winfo_exists():
|
|
469
|
+
return # the user navigated away before this deferred call fired
|
|
470
|
+
|
|
471
|
+
frame_width = quick_actions.winfo_width()
|
|
472
|
+
if frame_width < 10:
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
cols = max(1, frame_width // (CARD_WIDTH + 10))
|
|
476
|
+
|
|
477
|
+
for idx, card in enumerate(cards):
|
|
478
|
+
row, col = divmod(idx, cols)
|
|
479
|
+
card.grid(row=row + 1, column=col, sticky="nw", padx=5, pady=5)
|
|
480
|
+
|
|
481
|
+
num_rows = -(-len(cards) // cols)
|
|
482
|
+
heading_h = 30
|
|
483
|
+
new_height = heading_h + num_rows * (CARD_HEIGHT + 10) + 10
|
|
484
|
+
quick_actions.configure(height=new_height)
|
|
485
|
+
|
|
486
|
+
# ------------------------------------------------------------------ #
|
|
487
|
+
# Encrypt / Decrypt
|
|
488
|
+
# ------------------------------------------------------------------ #
|
|
489
|
+
|
|
490
|
+
def _build_crypto(self):
|
|
491
|
+
self._page_header(
|
|
492
|
+
"lock",
|
|
493
|
+
"Encrypt / Decrypt",
|
|
494
|
+
"Secure or restore one or more files.")
|
|
495
|
+
page = self._page(row=1)
|
|
496
|
+
|
|
497
|
+
row = 0
|
|
498
|
+
self._section(page, row, "Mode")
|
|
499
|
+
row += 1
|
|
500
|
+
mode_var = ctk.StringVar(value="Encrypt")
|
|
501
|
+
ctk.CTkSegmentedButton(
|
|
502
|
+
page,
|
|
503
|
+
values=[
|
|
504
|
+
"Encrypt",
|
|
505
|
+
"Decrypt"],
|
|
506
|
+
variable=mode_var,
|
|
507
|
+
fg_color=THEME["box_color"],
|
|
508
|
+
selected_color=ACCENT,
|
|
509
|
+
selected_hover_color=ACCENT_HOVER,
|
|
510
|
+
unselected_color=THEME["box_color"],
|
|
511
|
+
text_color=THEME["content_text"],
|
|
512
|
+
height=38,
|
|
513
|
+
font=self._font(
|
|
514
|
+
13,
|
|
515
|
+
"bold"),
|
|
516
|
+
command=lambda v: refresh()).grid(
|
|
517
|
+
row=row,
|
|
518
|
+
column=0,
|
|
519
|
+
columnspan=2,
|
|
520
|
+
sticky="ew")
|
|
521
|
+
row += 1
|
|
522
|
+
|
|
523
|
+
self._section(page, row, "Files")
|
|
524
|
+
row += 1
|
|
525
|
+
self._field_label(page, row, "Input files (comma and space separated)")
|
|
526
|
+
row += 1
|
|
527
|
+
in_entry = self._entry(page, row, "Choose one or more files")
|
|
528
|
+
selected_inputs = []
|
|
529
|
+
self._file_row(
|
|
530
|
+
page,
|
|
531
|
+
row,
|
|
532
|
+
in_entry,
|
|
533
|
+
multiple=True,
|
|
534
|
+
selected=selected_inputs,
|
|
535
|
+
on_browse=lambda: auto_output())
|
|
536
|
+
row += 1
|
|
537
|
+
|
|
538
|
+
self._field_label(page, row, "Output file")
|
|
539
|
+
row += 1
|
|
540
|
+
|
|
541
|
+
def auto_output(*_):
|
|
542
|
+
src = selected_inputs[0] if selected_inputs else in_entry.get().split(",")[
|
|
543
|
+
0].strip()
|
|
544
|
+
if not src:
|
|
545
|
+
return
|
|
546
|
+
out_entry.delete(0, "end")
|
|
547
|
+
if mode_var.get() == "Encrypt":
|
|
548
|
+
out_entry.insert(0, src + ENCRYPTED_SUFFIX)
|
|
549
|
+
else:
|
|
550
|
+
out_entry.insert(0, wr.remove_encrypted_suffix(src))
|
|
551
|
+
|
|
552
|
+
out_entry = self._entry(page, row, "Where to save the result")
|
|
553
|
+
self._file_row(page, row, out_entry, save=True)
|
|
554
|
+
row += 1
|
|
555
|
+
|
|
556
|
+
in_entry.bind("<KeyRelease>", auto_output)
|
|
557
|
+
|
|
558
|
+
self._section(page, row, "Protection")
|
|
559
|
+
row += 1
|
|
560
|
+
key_type_var = ctk.StringVar(value="AES (password)")
|
|
561
|
+
ctk.CTkSegmentedButton(
|
|
562
|
+
page,
|
|
563
|
+
values=[
|
|
564
|
+
"AES (password)",
|
|
565
|
+
"ML-KEM (Post Quantum, pub-key auth)",
|
|
566
|
+
"RSA (pub-key auth)"],
|
|
567
|
+
variable=key_type_var,
|
|
568
|
+
fg_color=THEME["box_color"],
|
|
569
|
+
selected_color=ACCENT,
|
|
570
|
+
selected_hover_color=ACCENT_HOVER,
|
|
571
|
+
unselected_color=THEME["box_color"],
|
|
572
|
+
text_color=THEME["content_text"],
|
|
573
|
+
height=38,
|
|
574
|
+
font=self._font(
|
|
575
|
+
13,
|
|
576
|
+
"bold"),
|
|
577
|
+
command=lambda _value: sync_protection_algorithm()).grid(
|
|
578
|
+
row=row,
|
|
579
|
+
column=0,
|
|
580
|
+
columnspan=2,
|
|
581
|
+
sticky="ew",
|
|
582
|
+
pady=(
|
|
583
|
+
0,
|
|
584
|
+
6))
|
|
585
|
+
row += 1
|
|
586
|
+
key_row = row
|
|
587
|
+
row += 1
|
|
588
|
+
constructing_config = CryptoConfig()
|
|
589
|
+
|
|
590
|
+
pwd_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
591
|
+
pwd_frame.grid_columnconfigure(0, weight=1)
|
|
592
|
+
pwd_entry = ctk.CTkEntry(
|
|
593
|
+
pwd_frame, placeholder_text="Password", show="•",
|
|
594
|
+
fg_color=THEME["box_color"], border_color=THEME["box_border"],
|
|
595
|
+
text_color=THEME["content_text"]
|
|
596
|
+
)
|
|
597
|
+
pwd_entry.grid(row=0, column=0, sticky="ew", pady=(0, 6))
|
|
598
|
+
confirm_entry = ctk.CTkEntry(
|
|
599
|
+
pwd_frame, placeholder_text="Confirm password", show="•",
|
|
600
|
+
fg_color=THEME["box_color"], border_color=THEME["box_border"],
|
|
601
|
+
text_color=THEME["content_text"]
|
|
602
|
+
)
|
|
603
|
+
confirm_entry.grid(row=1, column=0, sticky="ew", pady=(0, 6))
|
|
604
|
+
|
|
605
|
+
rsa_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
606
|
+
rsa_frame.grid_columnconfigure(0, weight=1)
|
|
607
|
+
key_entry = ctk.CTkEntry(
|
|
608
|
+
rsa_frame, placeholder_text="RSA key file (.pem)",
|
|
609
|
+
fg_color=THEME["box_color"], border_color=THEME["box_border"],
|
|
610
|
+
text_color=THEME["content_text"]
|
|
611
|
+
)
|
|
612
|
+
key_entry.grid(row=0, column=0, sticky="ew", pady=(0, 6))
|
|
613
|
+
self._file_row(rsa_frame, 0, key_entry)
|
|
614
|
+
pass_entry = ctk.CTkEntry(
|
|
615
|
+
rsa_frame, placeholder_text="Key passphrase (if any)", show="•",
|
|
616
|
+
fg_color=THEME["box_color"], border_color=THEME["box_border"],
|
|
617
|
+
text_color=THEME["content_text"]
|
|
618
|
+
)
|
|
619
|
+
pass_entry.grid(
|
|
620
|
+
row=1,
|
|
621
|
+
column=0,
|
|
622
|
+
columnspan=2,
|
|
623
|
+
sticky="ew",
|
|
624
|
+
pady=(
|
|
625
|
+
0,
|
|
626
|
+
6))
|
|
627
|
+
rsa_mode_var, _menu = self._option_grid(
|
|
628
|
+
rsa_frame, 2, [
|
|
629
|
+
"Hybrid (any file size)", "Direct RSA-OAEP (small files only)"], label=None)
|
|
630
|
+
|
|
631
|
+
def selected_algorithm():
|
|
632
|
+
if key_type_var.get().startswith("AES"):
|
|
633
|
+
return "aes"
|
|
634
|
+
if key_type_var.get().startswith("RSA"):
|
|
635
|
+
return "extended_oaep" if rsa_mode_var.get().startswith("Hybrid") else "rsa-oaep"
|
|
636
|
+
if constructing_config.algorithm not in (
|
|
637
|
+
"ML-KEM-768", "ML-KEM-1024"):
|
|
638
|
+
constructing_config.algorithm = "ML-KEM-768"
|
|
639
|
+
return constructing_config.algorithm
|
|
640
|
+
|
|
641
|
+
def sync_protection_algorithm(*_):
|
|
642
|
+
constructing_config.algorithm = selected_algorithm()
|
|
643
|
+
refresh()
|
|
644
|
+
|
|
645
|
+
rsa_mode_var.trace_add("write", sync_protection_algorithm)
|
|
646
|
+
|
|
647
|
+
mlkem_frame = ctk.CTkFrame(
|
|
648
|
+
page,
|
|
649
|
+
fg_color=ACCENT_TINT,
|
|
650
|
+
border_width=1,
|
|
651
|
+
border_color=ACCENT_BORDER)
|
|
652
|
+
mlkem_frame.grid_columnconfigure(0, weight=1)
|
|
653
|
+
mlkem_key_entry = ctk.CTkEntry(
|
|
654
|
+
mlkem_frame,
|
|
655
|
+
placeholder_text="ML-KEM key file (.pem or .der)",
|
|
656
|
+
fg_color=THEME["box_color"],
|
|
657
|
+
border_color=THEME["box_border"],
|
|
658
|
+
text_color=THEME["content_text"])
|
|
659
|
+
mlkem_key_entry.grid(
|
|
660
|
+
row=0,
|
|
661
|
+
column=0,
|
|
662
|
+
columnspan=1,
|
|
663
|
+
sticky="ew",
|
|
664
|
+
padx=12,
|
|
665
|
+
pady=(
|
|
666
|
+
2,
|
|
667
|
+
6))
|
|
668
|
+
self._file_row(mlkem_frame, 0, mlkem_key_entry)
|
|
669
|
+
mlkem_pass_entry = ctk.CTkEntry(
|
|
670
|
+
mlkem_frame,
|
|
671
|
+
placeholder_text="Key passphrase (if any)",
|
|
672
|
+
show="•",
|
|
673
|
+
fg_color=THEME["box_color"],
|
|
674
|
+
border_color=THEME["box_border"],
|
|
675
|
+
text_color=THEME["content_text"])
|
|
676
|
+
mlkem_pass_entry.grid(
|
|
677
|
+
row=1,
|
|
678
|
+
column=0,
|
|
679
|
+
columnspan=2,
|
|
680
|
+
sticky="ew",
|
|
681
|
+
padx=12,
|
|
682
|
+
pady=(
|
|
683
|
+
0,
|
|
684
|
+
6))
|
|
685
|
+
ctk.CTkLabel(
|
|
686
|
+
mlkem_frame,
|
|
687
|
+
text="ML-KEM is post-quantum lattice-based cryptography standardized by FIPS 203. "
|
|
688
|
+
"The selected parameter set uses hybrid AES encryption.",
|
|
689
|
+
text_color=THEME["content_text"],
|
|
690
|
+
font=self._font(11),
|
|
691
|
+
justify="left",
|
|
692
|
+
anchor="w",
|
|
693
|
+
wraplength=540).grid(
|
|
694
|
+
row=2,
|
|
695
|
+
column=0,
|
|
696
|
+
columnspan=2,
|
|
697
|
+
sticky="ew",
|
|
698
|
+
padx=12,
|
|
699
|
+
pady=10)
|
|
700
|
+
|
|
701
|
+
adv_fields = {}
|
|
702
|
+
self._advanced(
|
|
703
|
+
page, row, lambda inner: adv_fields.update(
|
|
704
|
+
self._build_config_fields(inner)))
|
|
705
|
+
row += 1
|
|
706
|
+
|
|
707
|
+
config_actions = ctk.CTkFrame(page, fg_color="transparent")
|
|
708
|
+
config_actions.grid(
|
|
709
|
+
row=row,
|
|
710
|
+
column=0,
|
|
711
|
+
columnspan=2,
|
|
712
|
+
sticky="ew",
|
|
713
|
+
pady=(
|
|
714
|
+
2,
|
|
715
|
+
8))
|
|
716
|
+
|
|
717
|
+
def apply_config(config):
|
|
718
|
+
constructing_config.__dict__.update(config.__dict__)
|
|
719
|
+
if config.algorithm == "aes":
|
|
720
|
+
key_type_var.set("AES (password)")
|
|
721
|
+
elif config.algorithm == "extended_oaep":
|
|
722
|
+
key_type_var.set("RSA (pub-key auth)")
|
|
723
|
+
rsa_mode_var.set("Hybrid (any file size)")
|
|
724
|
+
elif config.algorithm == "rsa-oaep":
|
|
725
|
+
key_type_var.set("RSA (pub-key auth)")
|
|
726
|
+
rsa_mode_var.set("Direct RSA-OAEP (small files only)")
|
|
727
|
+
elif config.algorithm.startswith("ML-KEM-"):
|
|
728
|
+
key_type_var.set("ML-KEM (Post Quantum, pub-key auth)")
|
|
729
|
+
adv_fields["store_iv"].set(config.store_iv)
|
|
730
|
+
adv_fields["mac_len"].delete(0, "end")
|
|
731
|
+
adv_fields["mac_len"].insert(0, str(config.mac_len))
|
|
732
|
+
adv_fields["kdf_salt"].delete(0, "end")
|
|
733
|
+
adv_fields["kdf_salt"].insert(
|
|
734
|
+
0, f"base64:{
|
|
735
|
+
base64.b64encode(
|
|
736
|
+
config.kdf_salt).decode('ascii')}")
|
|
737
|
+
adv_fields["kdf_time"].delete(0, "end")
|
|
738
|
+
adv_fields["kdf_time"].insert(0, str(config.kdf_time_cost))
|
|
739
|
+
adv_fields["kdf_mem"].delete(0, "end")
|
|
740
|
+
adv_fields["kdf_mem"].insert(
|
|
741
|
+
0, str(config.kdf_memory_cost // 1024))
|
|
742
|
+
adv_fields["kdf_par"].delete(0, "end")
|
|
743
|
+
adv_fields["kdf_par"].insert(0, str(config.kdf_parallelism))
|
|
744
|
+
adv_fields["hash"].set(
|
|
745
|
+
AVAIL_HASH_STR[AVAIL_HASH.index(config.hash_func)])
|
|
746
|
+
adv_fields["rand_func"].set(config.rand_func)
|
|
747
|
+
refresh()
|
|
748
|
+
|
|
749
|
+
def generate_config():
|
|
750
|
+
try:
|
|
751
|
+
config = self._config_from_fields(
|
|
752
|
+
adv_fields, allow_empty_salt=True, algorithm=selected_algorithm())
|
|
753
|
+
config.generate_random_salt()
|
|
754
|
+
constructing_config.kdf_salt = config.kdf_salt
|
|
755
|
+
adv_fields["kdf_salt"].delete(0, "end")
|
|
756
|
+
adv_fields["kdf_salt"].insert(
|
|
757
|
+
0, f"base64:{
|
|
758
|
+
base64.b64encode(
|
|
759
|
+
constructing_config.kdf_salt).decode('ascii')}")
|
|
760
|
+
except GeneralError as error:
|
|
761
|
+
self._error_win("Config generation failed", str(error))
|
|
762
|
+
|
|
763
|
+
def export_config():
|
|
764
|
+
try:
|
|
765
|
+
config = self._config_from_fields(
|
|
766
|
+
adv_fields, algorithm=selected_algorithm())
|
|
767
|
+
constructing_config.__dict__.update(config.__dict__)
|
|
768
|
+
path = filedialog.asksaveasfilename(
|
|
769
|
+
defaultextension=".conf", filetypes=(
|
|
770
|
+
("ghostbytes config", "*.conf"), ("All files", "*.*")), )
|
|
771
|
+
if path:
|
|
772
|
+
constructing_config.export_file(path)
|
|
773
|
+
status.configure(
|
|
774
|
+
text=f"Config saved to {path}",
|
|
775
|
+
text_color=SUCCESS)
|
|
776
|
+
except (OSError, ValueError) as e:
|
|
777
|
+
self._error_win("Config export failed", str(e))
|
|
778
|
+
|
|
779
|
+
def import_config():
|
|
780
|
+
path = filedialog.askopenfilename(filetypes=(
|
|
781
|
+
("ghostbytes config", "*.conf"), ("All files", "*.*")), )
|
|
782
|
+
if not path:
|
|
783
|
+
return
|
|
784
|
+
try:
|
|
785
|
+
apply_config(CryptoConfig.import_file(path))
|
|
786
|
+
status.configure(
|
|
787
|
+
text=f"Config loaded from {path}",
|
|
788
|
+
text_color=SUCCESS)
|
|
789
|
+
except (OSError, ValueError, UnicodeDecodeError) as e:
|
|
790
|
+
self._error_win("Config import failed", str(e))
|
|
791
|
+
|
|
792
|
+
generate_button = self._ghost_button(
|
|
793
|
+
config_actions,
|
|
794
|
+
"Generate Config (Random Salt)",
|
|
795
|
+
generate_config,
|
|
796
|
+
width=190)
|
|
797
|
+
export_button = self._ghost_button(
|
|
798
|
+
config_actions, "Export Config", export_config, width=120)
|
|
799
|
+
import_button = self._ghost_button(
|
|
800
|
+
config_actions, "Import Config", import_config, width=120)
|
|
801
|
+
generate_button.grid(row=0, column=0, sticky="w")
|
|
802
|
+
export_button.grid(row=0, column=1, sticky="w", padx=(8, 0))
|
|
803
|
+
import_button.grid(row=0, column=2, sticky="w", padx=(8, 0))
|
|
804
|
+
row += 1
|
|
805
|
+
|
|
806
|
+
btn, bar, status = self._run_row(page, row, "Run", None)
|
|
807
|
+
|
|
808
|
+
def refresh(*_):
|
|
809
|
+
aes_selected = key_type_var.get().startswith("AES")
|
|
810
|
+
mlkem_selected = key_type_var.get().startswith("ML-KEM")
|
|
811
|
+
kdf_visible = aes_selected or mlkem_selected
|
|
812
|
+
for widget in adv_fields.get("kdf_widgets", []):
|
|
813
|
+
if kdf_visible:
|
|
814
|
+
widget.grid()
|
|
815
|
+
else:
|
|
816
|
+
widget.grid_remove()
|
|
817
|
+
for widget in adv_fields.get("hash_widgets", []):
|
|
818
|
+
if key_type_var.get().startswith("RSA"):
|
|
819
|
+
widget.grid()
|
|
820
|
+
else:
|
|
821
|
+
widget.grid_remove()
|
|
822
|
+
for widget in adv_fields.get("rand_widgets", []):
|
|
823
|
+
if not aes_selected:
|
|
824
|
+
widget.grid()
|
|
825
|
+
else:
|
|
826
|
+
widget.grid_remove()
|
|
827
|
+
if mode_var.get() == "Encrypt":
|
|
828
|
+
generate_button.grid()
|
|
829
|
+
export_button.grid()
|
|
830
|
+
import_button.grid_remove()
|
|
831
|
+
else:
|
|
832
|
+
generate_button.grid_remove()
|
|
833
|
+
export_button.grid_remove()
|
|
834
|
+
import_button.grid()
|
|
835
|
+
if aes_selected:
|
|
836
|
+
rsa_frame.grid_forget()
|
|
837
|
+
mlkem_frame.grid_forget()
|
|
838
|
+
pwd_frame.grid(
|
|
839
|
+
row=key_row,
|
|
840
|
+
column=0,
|
|
841
|
+
columnspan=2,
|
|
842
|
+
sticky="ew")
|
|
843
|
+
if mode_var.get() == "Encrypt":
|
|
844
|
+
confirm_entry.grid()
|
|
845
|
+
else:
|
|
846
|
+
confirm_entry.grid_remove()
|
|
847
|
+
elif key_type_var.get().startswith("RSA"):
|
|
848
|
+
pwd_frame.grid_forget()
|
|
849
|
+
mlkem_frame.grid_forget()
|
|
850
|
+
rsa_frame.grid(
|
|
851
|
+
row=key_row,
|
|
852
|
+
column=0,
|
|
853
|
+
columnspan=2,
|
|
854
|
+
sticky="ew")
|
|
855
|
+
else:
|
|
856
|
+
pwd_frame.grid_forget()
|
|
857
|
+
rsa_frame.grid_forget()
|
|
858
|
+
mlkem_frame.grid(
|
|
859
|
+
row=key_row,
|
|
860
|
+
column=0,
|
|
861
|
+
columnspan=2,
|
|
862
|
+
sticky="ew")
|
|
863
|
+
auto_output()
|
|
864
|
+
|
|
865
|
+
refresh()
|
|
866
|
+
|
|
867
|
+
def do_run():
|
|
868
|
+
try:
|
|
869
|
+
config = self._config_from_fields(
|
|
870
|
+
adv_fields, algorithm=selected_algorithm())
|
|
871
|
+
except ValueError as e:
|
|
872
|
+
self._error_win("Invalid advanced settings", str(e))
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
src_paths = selected_inputs or [
|
|
876
|
+
line.strip() for line in in_entry.get().split(",") if line.strip()]
|
|
877
|
+
src, dst = (src_paths[0] if src_paths else ""), out_entry.get()
|
|
878
|
+
if not src or not dst:
|
|
879
|
+
self._error_win(
|
|
880
|
+
"Missing information",
|
|
881
|
+
"Please choose input file(s) and an output file path.")
|
|
882
|
+
return
|
|
883
|
+
|
|
884
|
+
mode = mode_var.get()
|
|
885
|
+
|
|
886
|
+
def launch(key, passphrase):
|
|
887
|
+
if mode == "Encrypt":
|
|
888
|
+
def work():
|
|
889
|
+
return wr.encrypt_paths(
|
|
890
|
+
src_paths, dst, config, key, passphrase)
|
|
891
|
+
else:
|
|
892
|
+
def work():
|
|
893
|
+
return wr.decrypt_paths(
|
|
894
|
+
src_paths, dst, config, key, passphrase)
|
|
895
|
+
self._run_async(
|
|
896
|
+
work,
|
|
897
|
+
btn,
|
|
898
|
+
bar,
|
|
899
|
+
status,
|
|
900
|
+
on_success=lambda res: status.configure(
|
|
901
|
+
text=f"Saved to {res}",
|
|
902
|
+
text_color=SUCCESS),
|
|
903
|
+
start_msg="Encrypting…" if mode == "Encrypt" else "Decrypting…",
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
if key_type_var.get().startswith("AES"):
|
|
907
|
+
pw = pwd_entry.get()
|
|
908
|
+
if not pw:
|
|
909
|
+
self._error_win(
|
|
910
|
+
"Missing information",
|
|
911
|
+
"Please enter a password.")
|
|
912
|
+
return
|
|
913
|
+
if mode == "Encrypt" and pw != confirm_entry.get():
|
|
914
|
+
self._error_win(
|
|
915
|
+
"Password mismatch",
|
|
916
|
+
"Password and confirmation do not match.")
|
|
917
|
+
return
|
|
918
|
+
config.algorithm = "aes"
|
|
919
|
+
launch(pw.encode("utf-8"), None)
|
|
920
|
+
elif key_type_var.get().startswith("RSA"):
|
|
921
|
+
key_path = key_entry.get()
|
|
922
|
+
if not key_path:
|
|
923
|
+
self._error_win(
|
|
924
|
+
"Missing information",
|
|
925
|
+
"Please choose an RSA key file.")
|
|
926
|
+
return
|
|
927
|
+
algorithm = (
|
|
928
|
+
"extended_oaep"
|
|
929
|
+
if rsa_mode_var.get().startswith("Hybrid")
|
|
930
|
+
else "rsa-oaep")
|
|
931
|
+
try:
|
|
932
|
+
with open(key_path, "rb") as f:
|
|
933
|
+
key = f.read()
|
|
934
|
+
except OSError as e:
|
|
935
|
+
self._error_win("File error", str(e))
|
|
936
|
+
return
|
|
937
|
+
passphrase = pass_entry.get() or None
|
|
938
|
+
config.algorithm = algorithm
|
|
939
|
+
launch(key, passphrase)
|
|
940
|
+
else:
|
|
941
|
+
key_path = mlkem_key_entry.get()
|
|
942
|
+
if not key_path:
|
|
943
|
+
self._error_win(
|
|
944
|
+
"Missing information",
|
|
945
|
+
"Please choose an ML-KEM key file.")
|
|
946
|
+
return
|
|
947
|
+
try:
|
|
948
|
+
with open(key_path, "rb") as f:
|
|
949
|
+
key = f.read()
|
|
950
|
+
except OSError as e:
|
|
951
|
+
self._error_win("File error", str(e))
|
|
952
|
+
return
|
|
953
|
+
try:
|
|
954
|
+
algorithm = wr.detect_mlkem_algorithm(
|
|
955
|
+
key, mlkem_pass_entry.get() or None)
|
|
956
|
+
except Exception as e:
|
|
957
|
+
self._error_win("Key error", str(e))
|
|
958
|
+
return
|
|
959
|
+
config.algorithm = algorithm
|
|
960
|
+
launch(key, mlkem_pass_entry.get() or None)
|
|
961
|
+
|
|
962
|
+
btn.configure(command=do_run)
|
|
963
|
+
|
|
964
|
+
# ------------------------------------------------------------------ #
|
|
965
|
+
# RSA key generation / verification / info
|
|
966
|
+
# ------------------------------------------------------------------ #
|
|
967
|
+
|
|
968
|
+
def _build_genkey(self):
|
|
969
|
+
self._page_header(
|
|
970
|
+
"key",
|
|
971
|
+
"Generate Key Pair",
|
|
972
|
+
"Create an RSA or ML-KEM public/private key pair.")
|
|
973
|
+
page = self._page(row=1)
|
|
974
|
+
row = 0
|
|
975
|
+
self._section(page, row, "Key type")
|
|
976
|
+
row += 1
|
|
977
|
+
key_type_var = ctk.StringVar(value="RSA")
|
|
978
|
+
ctk.CTkSegmentedButton(
|
|
979
|
+
page,
|
|
980
|
+
values=[
|
|
981
|
+
"ML-KEM",
|
|
982
|
+
"RSA"],
|
|
983
|
+
variable=key_type_var,
|
|
984
|
+
fg_color=THEME["box_color"],
|
|
985
|
+
selected_color=ACCENT,
|
|
986
|
+
selected_hover_color=ACCENT_HOVER,
|
|
987
|
+
unselected_color=THEME["box_color"],
|
|
988
|
+
text_color=THEME["content_text"],
|
|
989
|
+
height=38,
|
|
990
|
+
font=self._font(
|
|
991
|
+
13,
|
|
992
|
+
"bold"),
|
|
993
|
+
command=lambda _value: refresh_type(),
|
|
994
|
+
).grid(
|
|
995
|
+
row=row,
|
|
996
|
+
column=0,
|
|
997
|
+
columnspan=2,
|
|
998
|
+
sticky="ew")
|
|
999
|
+
row += 1
|
|
1000
|
+
|
|
1001
|
+
rsa_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
1002
|
+
rsa_frame.grid_columnconfigure(0, weight=1)
|
|
1003
|
+
self._field_label(rsa_frame, 0, "RSA key size")
|
|
1004
|
+
rsa_size_var, _ = self._option(
|
|
1005
|
+
rsa_frame, 1, [str(s) for s in COMMON_RSA_SIZE] + ["Custom…"],
|
|
1006
|
+
default="2048", command=lambda _value: refresh_rsa_size()
|
|
1007
|
+
)
|
|
1008
|
+
custom_rsa_entry = self._entry(
|
|
1009
|
+
rsa_frame, 2, "Custom RSA key size in bits")
|
|
1010
|
+
custom_rsa_entry.grid_remove()
|
|
1011
|
+
self._field_label(rsa_frame, 3, "Public exponent")
|
|
1012
|
+
exponent_entry = self._entry(rsa_frame, 4, default="65537")
|
|
1013
|
+
self._field_label(rsa_frame, 5, "Output format")
|
|
1014
|
+
rsa_format_var, _ = self._option(rsa_frame, 6, RSA_KEY_OUT_FORMAT)
|
|
1015
|
+
|
|
1016
|
+
mlkem_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
1017
|
+
mlkem_frame.grid_columnconfigure(0, weight=1)
|
|
1018
|
+
self._field_label(mlkem_frame, 0, "ML-KEM parameter set")
|
|
1019
|
+
mlkem_values = [
|
|
1020
|
+
algorithm for algorithm in AVAIL_ALG if algorithm.startswith("ML-KEM-")]
|
|
1021
|
+
mlkem_alg_var, _ = self._option(mlkem_frame, 1, mlkem_values)
|
|
1022
|
+
ctk.CTkLabel(
|
|
1023
|
+
mlkem_frame,
|
|
1024
|
+
text=(
|
|
1025
|
+
"ML-KEM-768 corresponds to Post-Quantum Security Level 3; "
|
|
1026
|
+
"ML-KEM-1024 corresponds to Level 5 (NIST) "
|
|
1027
|
+
"(Equivalent to AES-256)."),
|
|
1028
|
+
text_color=THEME["gray_text"],
|
|
1029
|
+
font=self._font(10),
|
|
1030
|
+
anchor="w",
|
|
1031
|
+
justify="left",
|
|
1032
|
+
wraplength=560).grid(
|
|
1033
|
+
row=2,
|
|
1034
|
+
column=0,
|
|
1035
|
+
sticky="w",
|
|
1036
|
+
pady=(
|
|
1037
|
+
4,
|
|
1038
|
+
0))
|
|
1039
|
+
self._field_label(mlkem_frame, 3, "Output format")
|
|
1040
|
+
mlkem_format_var, _ = self._option(mlkem_frame, 4, ["PEM", "DER"])
|
|
1041
|
+
|
|
1042
|
+
rsa_frame.grid(row=row, column=0, columnspan=2, sticky="ew")
|
|
1043
|
+
row += 1
|
|
1044
|
+
self._field_label(
|
|
1045
|
+
page, row, "Passphrase (optional, protects private key)")
|
|
1046
|
+
row += 1
|
|
1047
|
+
pass_entry = self._entry(
|
|
1048
|
+
page, row, "Leave blank for no passphrase", show="•")
|
|
1049
|
+
row += 1
|
|
1050
|
+
self._section(page, row, "Save to")
|
|
1051
|
+
row += 1
|
|
1052
|
+
self._field_label(page, row, "Private key file")
|
|
1053
|
+
row += 1
|
|
1054
|
+
private_entry = self._entry(
|
|
1055
|
+
page, row, "Browse for the private key output file")
|
|
1056
|
+
self._file_row(page, row, private_entry, save=True,
|
|
1057
|
+
on_browse=lambda: derive_public_path())
|
|
1058
|
+
row += 1
|
|
1059
|
+
self._field_label(page, row, "Public key file")
|
|
1060
|
+
row += 1
|
|
1061
|
+
public_entry = self._entry(page, row, "Public key output file")
|
|
1062
|
+
self._file_row(page, row, public_entry, save=True,
|
|
1063
|
+
on_browse=lambda: derive_private_from_public())
|
|
1064
|
+
row += 1
|
|
1065
|
+
|
|
1066
|
+
def derive_public_path(_event=None):
|
|
1067
|
+
private_path = private_entry.get()
|
|
1068
|
+
if private_path:
|
|
1069
|
+
stem, extension = os.path.splitext(private_path)
|
|
1070
|
+
public_entry.delete(0, "end")
|
|
1071
|
+
public_entry.insert(0, f"{stem}_pub{extension}")
|
|
1072
|
+
|
|
1073
|
+
def derive_private_from_public(_event=None):
|
|
1074
|
+
public_path = public_entry.get()
|
|
1075
|
+
if public_path:
|
|
1076
|
+
stem, extension = os.path.splitext(public_path)
|
|
1077
|
+
private_entry.delete(0, "end")
|
|
1078
|
+
private_entry.insert(
|
|
1079
|
+
0, f"{stem[:-4] if stem.endswith('_pub') else stem}{extension}")
|
|
1080
|
+
|
|
1081
|
+
private_entry.bind("<KeyRelease>", derive_public_path)
|
|
1082
|
+
private_entry.bind("<FocusOut>", derive_public_path)
|
|
1083
|
+
btn, bar, status = self._run_row(page, row, "Generate", None)
|
|
1084
|
+
|
|
1085
|
+
def refresh_type():
|
|
1086
|
+
if key_type_var.get() == "RSA":
|
|
1087
|
+
mlkem_frame.grid_forget()
|
|
1088
|
+
rsa_frame.grid(row=2, column=0, columnspan=2, sticky="ew")
|
|
1089
|
+
else:
|
|
1090
|
+
rsa_frame.grid_forget()
|
|
1091
|
+
mlkem_frame.grid(row=2, column=0, columnspan=2, sticky="ew")
|
|
1092
|
+
|
|
1093
|
+
def refresh_rsa_size():
|
|
1094
|
+
if rsa_size_var.get() == "Custom…":
|
|
1095
|
+
custom_rsa_entry.grid()
|
|
1096
|
+
else:
|
|
1097
|
+
custom_rsa_entry.grid_remove()
|
|
1098
|
+
|
|
1099
|
+
def do_run():
|
|
1100
|
+
priv_path = private_entry.get().strip()
|
|
1101
|
+
pub_path = public_entry.get().strip()
|
|
1102
|
+
if not priv_path or not pub_path:
|
|
1103
|
+
self._error_win(
|
|
1104
|
+
"Missing information",
|
|
1105
|
+
"Please choose private and public key output files.")
|
|
1106
|
+
return
|
|
1107
|
+
|
|
1108
|
+
key_size = None
|
|
1109
|
+
if key_type_var.get() == "RSA":
|
|
1110
|
+
try:
|
|
1111
|
+
key_size = int(
|
|
1112
|
+
custom_rsa_entry.get()) if rsa_size_var.get() == "Custom…" else int(
|
|
1113
|
+
rsa_size_var.get())
|
|
1114
|
+
except ValueError:
|
|
1115
|
+
self._error_win(
|
|
1116
|
+
"Invalid input",
|
|
1117
|
+
"RSA key size must be numeric.")
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
def work():
|
|
1121
|
+
if key_type_var.get() == "RSA":
|
|
1122
|
+
pub, priv = wr.generate_rsa_keypair(key_size, int(
|
|
1123
|
+
exponent_entry.get()), pass_entry.get() or None, rsa_format_var.get())
|
|
1124
|
+
else:
|
|
1125
|
+
pub, priv = wr.generate_mlkem_keypair(
|
|
1126
|
+
mlkem_alg_var.get(), mlkem_format_var.get(), pass_entry.get() or None)
|
|
1127
|
+
os.makedirs(os.path.dirname(priv_path) or ".", exist_ok=True)
|
|
1128
|
+
with open(pub_path, "wb") as file:
|
|
1129
|
+
file.write(pub)
|
|
1130
|
+
with open(priv_path, "wb") as file:
|
|
1131
|
+
file.write(priv)
|
|
1132
|
+
return pub_path, priv_path
|
|
1133
|
+
|
|
1134
|
+
def launch():
|
|
1135
|
+
self._run_async(
|
|
1136
|
+
work,
|
|
1137
|
+
btn,
|
|
1138
|
+
bar,
|
|
1139
|
+
status,
|
|
1140
|
+
on_success=lambda result: status.configure(
|
|
1141
|
+
text=f"Saved {
|
|
1142
|
+
os.path.basename(
|
|
1143
|
+
result[0])} and {
|
|
1144
|
+
os.path.basename(
|
|
1145
|
+
result[1])}",
|
|
1146
|
+
text_color=SUCCESS),
|
|
1147
|
+
start_msg="Generating key pair…",
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
if key_size is None:
|
|
1151
|
+
launch()
|
|
1152
|
+
else:
|
|
1153
|
+
self._confirm_large_rsa(key_size, launch)
|
|
1154
|
+
|
|
1155
|
+
btn.configure(command=do_run)
|
|
1156
|
+
refresh_type()
|
|
1157
|
+
|
|
1158
|
+
def _build_verifykey(self):
|
|
1159
|
+
self._page_header(
|
|
1160
|
+
"circle-check",
|
|
1161
|
+
"Verify Key Pair",
|
|
1162
|
+
"Check whether a public and private key match; key types are detected automatically.")
|
|
1163
|
+
page = self._page(row=1)
|
|
1164
|
+
|
|
1165
|
+
row = 0
|
|
1166
|
+
self._field_label(page, row, "Public key file")
|
|
1167
|
+
row += 1
|
|
1168
|
+
pub_entry = self._entry(page, row, "Public key (.pem)")
|
|
1169
|
+
self._file_row(
|
|
1170
|
+
page,
|
|
1171
|
+
row,
|
|
1172
|
+
pub_entry,
|
|
1173
|
+
on_browse=lambda: derive_private_path())
|
|
1174
|
+
row += 1
|
|
1175
|
+
|
|
1176
|
+
self._field_label(page, row, "Private key file")
|
|
1177
|
+
row += 1
|
|
1178
|
+
priv_entry = self._entry(page, row, "Private key (.pem)")
|
|
1179
|
+
self._file_row(
|
|
1180
|
+
page,
|
|
1181
|
+
row,
|
|
1182
|
+
priv_entry,
|
|
1183
|
+
on_browse=lambda: derive_public_path())
|
|
1184
|
+
row += 1
|
|
1185
|
+
|
|
1186
|
+
def derive_private_path(_event=None):
|
|
1187
|
+
public_path = pub_entry.get()
|
|
1188
|
+
if public_path:
|
|
1189
|
+
stem, extension = os.path.splitext(public_path)
|
|
1190
|
+
private_entry_value = f"{stem[:-
|
|
1191
|
+
4] if stem.endswith('_pub') else stem}{extension}"
|
|
1192
|
+
priv_entry.delete(0, "end")
|
|
1193
|
+
priv_entry.insert(0, private_entry_value)
|
|
1194
|
+
|
|
1195
|
+
def derive_public_path(_event=None):
|
|
1196
|
+
private_path = priv_entry.get()
|
|
1197
|
+
if private_path:
|
|
1198
|
+
stem, extension = os.path.splitext(private_path)
|
|
1199
|
+
pub_entry.delete(0, "end")
|
|
1200
|
+
pub_entry.insert(0, f"{stem}_pub{extension}")
|
|
1201
|
+
|
|
1202
|
+
pub_entry.bind("<KeyRelease>", derive_private_path)
|
|
1203
|
+
pub_entry.bind("<FocusOut>", derive_private_path)
|
|
1204
|
+
priv_entry.bind("<KeyRelease>", derive_public_path)
|
|
1205
|
+
priv_entry.bind("<FocusOut>", derive_public_path)
|
|
1206
|
+
|
|
1207
|
+
self._field_label(page, row, "Passphrase (if any)")
|
|
1208
|
+
row += 1
|
|
1209
|
+
pass_entry = self._entry(page, row, show="•")
|
|
1210
|
+
row += 1
|
|
1211
|
+
|
|
1212
|
+
btn, bar, status = self._run_row(page, row, "Verify", None)
|
|
1213
|
+
|
|
1214
|
+
def do_run():
|
|
1215
|
+
pub_path, priv_path = pub_entry.get(), priv_entry.get()
|
|
1216
|
+
if not pub_path or not priv_path:
|
|
1217
|
+
self._error_win(
|
|
1218
|
+
"Missing information",
|
|
1219
|
+
"Please choose both key files.")
|
|
1220
|
+
return
|
|
1221
|
+
|
|
1222
|
+
def work():
|
|
1223
|
+
with open(pub_path, "rb") as f:
|
|
1224
|
+
pub = f.read()
|
|
1225
|
+
with open(priv_path, "rb") as f:
|
|
1226
|
+
priv = f.read()
|
|
1227
|
+
return wr.verify_keypair(pub, priv, pass_entry.get() or None)
|
|
1228
|
+
|
|
1229
|
+
def on_success(matches):
|
|
1230
|
+
if matches:
|
|
1231
|
+
status.configure(
|
|
1232
|
+
text="✓ The key pair matches.",
|
|
1233
|
+
text_color=SUCCESS)
|
|
1234
|
+
else:
|
|
1235
|
+
status.configure(
|
|
1236
|
+
text="✗ The key pair does NOT match.",
|
|
1237
|
+
text_color=DANGER)
|
|
1238
|
+
|
|
1239
|
+
self._run_async(
|
|
1240
|
+
work,
|
|
1241
|
+
btn,
|
|
1242
|
+
bar,
|
|
1243
|
+
status,
|
|
1244
|
+
on_success=on_success,
|
|
1245
|
+
start_msg="Verifying…")
|
|
1246
|
+
|
|
1247
|
+
btn.configure(command=do_run)
|
|
1248
|
+
|
|
1249
|
+
def _build_keyinfo(self):
|
|
1250
|
+
self._page_header(
|
|
1251
|
+
"key",
|
|
1252
|
+
"Key Information",
|
|
1253
|
+
"Inspect a key and check whether it is cryptographically usable.")
|
|
1254
|
+
page = self._page(row=1)
|
|
1255
|
+
|
|
1256
|
+
row = 0
|
|
1257
|
+
self._field_label(page, row, "Key file")
|
|
1258
|
+
row += 1
|
|
1259
|
+
key_entry = self._entry(page, row, "Public or private key (.pem/.der)")
|
|
1260
|
+
self._file_row(page, row, key_entry)
|
|
1261
|
+
row += 1
|
|
1262
|
+
|
|
1263
|
+
self._field_label(page, row, "Passphrase (if any)")
|
|
1264
|
+
row += 1
|
|
1265
|
+
pass_entry = self._entry(page, row, show="•")
|
|
1266
|
+
row += 1
|
|
1267
|
+
|
|
1268
|
+
btn, bar, status = self._run_row(page, row, "Inspect", None)
|
|
1269
|
+
row += 2
|
|
1270
|
+
|
|
1271
|
+
result_box = ctk.CTkFrame(
|
|
1272
|
+
page,
|
|
1273
|
+
fg_color=THEME["box_color"],
|
|
1274
|
+
border_width=1,
|
|
1275
|
+
border_color=THEME["box_border"],
|
|
1276
|
+
corner_radius=THEME["corner_rad"])
|
|
1277
|
+
result_box.grid(
|
|
1278
|
+
row=row,
|
|
1279
|
+
column=0,
|
|
1280
|
+
columnspan=2,
|
|
1281
|
+
sticky="ew",
|
|
1282
|
+
pady=(
|
|
1283
|
+
6,
|
|
1284
|
+
0))
|
|
1285
|
+
result_box.grid_columnconfigure(1, weight=1)
|
|
1286
|
+
result_box.grid_remove()
|
|
1287
|
+
|
|
1288
|
+
def do_run():
|
|
1289
|
+
path = key_entry.get()
|
|
1290
|
+
if not path:
|
|
1291
|
+
self._error_win(
|
|
1292
|
+
"Missing information",
|
|
1293
|
+
"Please choose a key file.")
|
|
1294
|
+
return
|
|
1295
|
+
|
|
1296
|
+
def work():
|
|
1297
|
+
with open(path, "rb") as f:
|
|
1298
|
+
data = f.read()
|
|
1299
|
+
return wr.key_info(data, pass_entry.get() or None)
|
|
1300
|
+
|
|
1301
|
+
def on_success(info):
|
|
1302
|
+
for child in result_box.winfo_children():
|
|
1303
|
+
child.destroy()
|
|
1304
|
+
for i, (k, v) in enumerate(info.items()):
|
|
1305
|
+
ctk.CTkLabel(
|
|
1306
|
+
result_box,
|
|
1307
|
+
text=k,
|
|
1308
|
+
text_color=THEME["slight_gray"],
|
|
1309
|
+
font=self._font(12)).grid(
|
|
1310
|
+
row=i,
|
|
1311
|
+
column=0,
|
|
1312
|
+
sticky="w",
|
|
1313
|
+
padx=14,
|
|
1314
|
+
pady=8)
|
|
1315
|
+
ctk.CTkLabel(
|
|
1316
|
+
result_box,
|
|
1317
|
+
text=v,
|
|
1318
|
+
text_color=THEME["content_text"],
|
|
1319
|
+
font=self._mono_font(12)).grid(
|
|
1320
|
+
row=i,
|
|
1321
|
+
column=1,
|
|
1322
|
+
sticky="w",
|
|
1323
|
+
padx=14,
|
|
1324
|
+
pady=8)
|
|
1325
|
+
result_box.grid()
|
|
1326
|
+
|
|
1327
|
+
self._run_async(
|
|
1328
|
+
work,
|
|
1329
|
+
btn,
|
|
1330
|
+
bar,
|
|
1331
|
+
status,
|
|
1332
|
+
on_success=on_success,
|
|
1333
|
+
start_msg="Reading key…")
|
|
1334
|
+
|
|
1335
|
+
btn.configure(command=do_run)
|
|
1336
|
+
|
|
1337
|
+
# ------------------------------------------------------------------ #
|
|
1338
|
+
# Hash files (single, multi-file, or whole folder), Random, Password
|
|
1339
|
+
# generator, Benchmark
|
|
1340
|
+
# ------------------------------------------------------------------ #
|
|
1341
|
+
|
|
1342
|
+
def _build_hashfile(self):
|
|
1343
|
+
self._page_header(
|
|
1344
|
+
"hashtag",
|
|
1345
|
+
"Hash File(s) (Checksum)",
|
|
1346
|
+
"Compute a cryptographic digest for files or an entire folder.")
|
|
1347
|
+
page = self._page(row=1)
|
|
1348
|
+
|
|
1349
|
+
row = 0
|
|
1350
|
+
self._section(page, row, "Source")
|
|
1351
|
+
row += 1
|
|
1352
|
+
mode_var = ctk.StringVar(value="Files")
|
|
1353
|
+
ctk.CTkSegmentedButton(
|
|
1354
|
+
page,
|
|
1355
|
+
values=[
|
|
1356
|
+
"Files",
|
|
1357
|
+
"Folder"],
|
|
1358
|
+
variable=mode_var,
|
|
1359
|
+
fg_color=THEME["box_color"],
|
|
1360
|
+
selected_color=ACCENT,
|
|
1361
|
+
selected_hover_color=ACCENT_HOVER,
|
|
1362
|
+
unselected_color=THEME["box_color"],
|
|
1363
|
+
text_color=THEME["content_text"],
|
|
1364
|
+
command=lambda v: refresh()).grid(
|
|
1365
|
+
row=row,
|
|
1366
|
+
column=0,
|
|
1367
|
+
columnspan=2,
|
|
1368
|
+
sticky="w",
|
|
1369
|
+
pady=(
|
|
1370
|
+
0,
|
|
1371
|
+
6))
|
|
1372
|
+
row += 1
|
|
1373
|
+
|
|
1374
|
+
self._field_label(page, row, "Hash algorithm")
|
|
1375
|
+
row += 1
|
|
1376
|
+
alg_var, _ = self._option(page, row, AVAIL_HASH_STR)
|
|
1377
|
+
row += 1
|
|
1378
|
+
mode_row = row
|
|
1379
|
+
row += 1
|
|
1380
|
+
|
|
1381
|
+
files_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
1382
|
+
files_frame.grid_columnconfigure(0, weight=1)
|
|
1383
|
+
files_box = ctk.CTkTextbox(
|
|
1384
|
+
files_frame,
|
|
1385
|
+
height=70,
|
|
1386
|
+
fg_color=THEME["box_color"],
|
|
1387
|
+
text_color=THEME["content_text"],
|
|
1388
|
+
font=self._mono_font(11))
|
|
1389
|
+
files_box.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 6))
|
|
1390
|
+
files_box.configure(state="disabled")
|
|
1391
|
+
selected = []
|
|
1392
|
+
|
|
1393
|
+
def browse_files():
|
|
1394
|
+
paths = filedialog.askopenfilenames()
|
|
1395
|
+
if paths:
|
|
1396
|
+
selected.clear()
|
|
1397
|
+
selected.extend(paths)
|
|
1398
|
+
files_box.configure(state="normal")
|
|
1399
|
+
files_box.delete("1.0", "end")
|
|
1400
|
+
files_box.insert("1.0", "\n".join(selected))
|
|
1401
|
+
files_box.configure(state="disabled")
|
|
1402
|
+
refresh_checksum_path()
|
|
1403
|
+
|
|
1404
|
+
self._ghost_button(
|
|
1405
|
+
files_frame,
|
|
1406
|
+
"Choose files…",
|
|
1407
|
+
browse_files).grid(
|
|
1408
|
+
row=1,
|
|
1409
|
+
column=1,
|
|
1410
|
+
sticky="e")
|
|
1411
|
+
|
|
1412
|
+
folder_frame = ctk.CTkFrame(page, fg_color="transparent")
|
|
1413
|
+
folder_frame.grid_columnconfigure(0, weight=1)
|
|
1414
|
+
folder_entry = ctk.CTkEntry(
|
|
1415
|
+
folder_frame,
|
|
1416
|
+
placeholder_text="Folder to hash (recursive)",
|
|
1417
|
+
fg_color=THEME["box_color"],
|
|
1418
|
+
border_color=THEME["box_border"],
|
|
1419
|
+
text_color=THEME["content_text"])
|
|
1420
|
+
folder_entry.grid(row=0, column=0, sticky="ew")
|
|
1421
|
+
self._file_row(folder_frame, 0, folder_entry, dir_only=True)
|
|
1422
|
+
|
|
1423
|
+
def refresh(*_):
|
|
1424
|
+
if mode_var.get() == "Files":
|
|
1425
|
+
folder_frame.grid_forget()
|
|
1426
|
+
files_frame.grid(
|
|
1427
|
+
row=mode_row,
|
|
1428
|
+
column=0,
|
|
1429
|
+
columnspan=2,
|
|
1430
|
+
sticky="ew")
|
|
1431
|
+
else:
|
|
1432
|
+
files_frame.grid_forget()
|
|
1433
|
+
folder_frame.grid(
|
|
1434
|
+
row=mode_row,
|
|
1435
|
+
column=0,
|
|
1436
|
+
columnspan=2,
|
|
1437
|
+
sticky="ew")
|
|
1438
|
+
|
|
1439
|
+
refresh()
|
|
1440
|
+
|
|
1441
|
+
self._section(page, row, "Output")
|
|
1442
|
+
row += 1
|
|
1443
|
+
save_var = self._checkbox(page, row, "Save results to a checksum file")
|
|
1444
|
+
row += 1
|
|
1445
|
+
out_entry = self._entry(page, row, "Where to save the checksum file")
|
|
1446
|
+
browse_btn = self._file_row(page, row, out_entry, save=True)
|
|
1447
|
+
out_entry.grid_remove()
|
|
1448
|
+
browse_btn.grid_remove()
|
|
1449
|
+
row += 1
|
|
1450
|
+
|
|
1451
|
+
def toggle_save(*_):
|
|
1452
|
+
if save_var.get():
|
|
1453
|
+
out_entry.grid()
|
|
1454
|
+
browse_btn.grid()
|
|
1455
|
+
refresh_checksum_path()
|
|
1456
|
+
else:
|
|
1457
|
+
out_entry.grid_remove()
|
|
1458
|
+
browse_btn.grid_remove()
|
|
1459
|
+
|
|
1460
|
+
def refresh_checksum_path(*_):
|
|
1461
|
+
if not save_var.get():
|
|
1462
|
+
return
|
|
1463
|
+
if mode_var.get() == "Files" and selected:
|
|
1464
|
+
base = wr.checksum_root(selected)
|
|
1465
|
+
elif mode_var.get() == "Folder" and folder_entry.get():
|
|
1466
|
+
base = wr.checksum_root([folder_entry.get()])
|
|
1467
|
+
else:
|
|
1468
|
+
base = "."
|
|
1469
|
+
out_entry.delete(0, "end")
|
|
1470
|
+
out_entry.insert(
|
|
1471
|
+
0, os.path.join(
|
|
1472
|
+
base, wr.checksum_suffix(
|
|
1473
|
+
alg_var.get())))
|
|
1474
|
+
|
|
1475
|
+
save_var.trace_add("write", lambda *_: toggle_save())
|
|
1476
|
+
alg_var.trace_add("write", refresh_checksum_path)
|
|
1477
|
+
folder_entry.bind("<KeyRelease>", refresh_checksum_path)
|
|
1478
|
+
|
|
1479
|
+
bar = ctk.CTkProgressBar(
|
|
1480
|
+
page, mode="determinate", progress_color=ACCENT)
|
|
1481
|
+
bar.set(0)
|
|
1482
|
+
bar.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
|
1483
|
+
row += 1
|
|
1484
|
+
status = ctk.CTkLabel(
|
|
1485
|
+
page,
|
|
1486
|
+
text="",
|
|
1487
|
+
text_color=THEME["slight_gray"],
|
|
1488
|
+
font=self._font(11),
|
|
1489
|
+
anchor="w")
|
|
1490
|
+
status.grid(row=row, column=0, sticky="w", pady=(6, 0))
|
|
1491
|
+
btn = self._primary_button(page, "Compute Hashes")
|
|
1492
|
+
btn.grid(row=row, column=1, sticky="e", pady=(6, 0))
|
|
1493
|
+
row += 1
|
|
1494
|
+
|
|
1495
|
+
results_box = ctk.CTkTextbox(
|
|
1496
|
+
page,
|
|
1497
|
+
height=180,
|
|
1498
|
+
fg_color=THEME["box_color"],
|
|
1499
|
+
text_color=THEME["content_text"],
|
|
1500
|
+
font=self._mono_font(11))
|
|
1501
|
+
results_box.grid(
|
|
1502
|
+
row=row,
|
|
1503
|
+
column=0,
|
|
1504
|
+
columnspan=2,
|
|
1505
|
+
sticky="ew",
|
|
1506
|
+
pady=(
|
|
1507
|
+
10,
|
|
1508
|
+
0))
|
|
1509
|
+
results_box.configure(state="disabled")
|
|
1510
|
+
|
|
1511
|
+
def do_run():
|
|
1512
|
+
alg = alg_var.get()
|
|
1513
|
+
if mode_var.get() == "Files":
|
|
1514
|
+
if not selected:
|
|
1515
|
+
self._error_win(
|
|
1516
|
+
"Missing information",
|
|
1517
|
+
"Please choose at least one file.")
|
|
1518
|
+
return
|
|
1519
|
+
entries = [(os.path.basename(p), p) for p in selected]
|
|
1520
|
+
else:
|
|
1521
|
+
folder = folder_entry.get()
|
|
1522
|
+
if not folder:
|
|
1523
|
+
self._error_win(
|
|
1524
|
+
"Missing information",
|
|
1525
|
+
"Please choose a folder.")
|
|
1526
|
+
return
|
|
1527
|
+
try:
|
|
1528
|
+
entries = wr.collect_folder_files(folder)
|
|
1529
|
+
except Exception as e:
|
|
1530
|
+
self._error_win("Folder error", str(e))
|
|
1531
|
+
return
|
|
1532
|
+
if not entries:
|
|
1533
|
+
self._error_win(
|
|
1534
|
+
"Empty folder",
|
|
1535
|
+
"No files were found in that folder.")
|
|
1536
|
+
return
|
|
1537
|
+
|
|
1538
|
+
save_checksum = save_var.get()
|
|
1539
|
+
out_path = out_entry.get() if save_checksum else None
|
|
1540
|
+
if save_checksum and not out_path:
|
|
1541
|
+
self._error_win(
|
|
1542
|
+
"Missing information",
|
|
1543
|
+
"Please choose where to save the checksum file.")
|
|
1544
|
+
return
|
|
1545
|
+
|
|
1546
|
+
btn.configure(state="disabled")
|
|
1547
|
+
bar.set(0)
|
|
1548
|
+
results_box.configure(state="normal")
|
|
1549
|
+
results_box.delete("1.0", "end")
|
|
1550
|
+
results_box.configure(state="disabled")
|
|
1551
|
+
status.configure(text="Starting…", text_color=THEME["slight_gray"])
|
|
1552
|
+
|
|
1553
|
+
def progress_cb(done, total, label):
|
|
1554
|
+
self.after(
|
|
1555
|
+
0,
|
|
1556
|
+
lambda: (
|
|
1557
|
+
bar.set(
|
|
1558
|
+
done / total),
|
|
1559
|
+
status.configure(
|
|
1560
|
+
text=f"{done}/{total}: {label}")))
|
|
1561
|
+
|
|
1562
|
+
def target():
|
|
1563
|
+
try:
|
|
1564
|
+
hash_entries = (
|
|
1565
|
+
wr.checksum_entries_for_output(entries, out_path)
|
|
1566
|
+
if save_checksum else entries
|
|
1567
|
+
)
|
|
1568
|
+
results = wr.hash_paths(
|
|
1569
|
+
hash_entries, alg, progress_cb=progress_cb)
|
|
1570
|
+
if save_checksum:
|
|
1571
|
+
wr.write_checksum_file(out_path, results)
|
|
1572
|
+
except Exception as e:
|
|
1573
|
+
message = str(e)
|
|
1574
|
+
self.after(
|
|
1575
|
+
0, lambda: (
|
|
1576
|
+
btn.configure(
|
|
1577
|
+
state="normal"), status.configure(
|
|
1578
|
+
text=f"Failed: {message}", text_color=DANGER), self._error_win(
|
|
1579
|
+
"Operation Failed", message)))
|
|
1580
|
+
return
|
|
1581
|
+
|
|
1582
|
+
def finish():
|
|
1583
|
+
btn.configure(state="normal")
|
|
1584
|
+
bar.set(1.0)
|
|
1585
|
+
results_box.configure(state="normal")
|
|
1586
|
+
results_box.delete("1.0", "end")
|
|
1587
|
+
for label, digest in results:
|
|
1588
|
+
results_box.insert("end", f"{digest} {label}\n")
|
|
1589
|
+
results_box.configure(state="disabled")
|
|
1590
|
+
msg = f"Done — {len(results)} file(s) hashed."
|
|
1591
|
+
if save_checksum:
|
|
1592
|
+
msg += f" Saved to {out_path}"
|
|
1593
|
+
status.configure(text=msg, text_color=SUCCESS)
|
|
1594
|
+
|
|
1595
|
+
self.after(0, finish)
|
|
1596
|
+
|
|
1597
|
+
threading.Thread(target=target, daemon=True).start()
|
|
1598
|
+
|
|
1599
|
+
btn.configure(command=do_run)
|
|
1600
|
+
|
|
1601
|
+
def _build_random(self):
|
|
1602
|
+
self._page_header(
|
|
1603
|
+
"dice",
|
|
1604
|
+
"Random",
|
|
1605
|
+
"Generate cryptographically random data.")
|
|
1606
|
+
page = self._page(row=1)
|
|
1607
|
+
|
|
1608
|
+
row = 0
|
|
1609
|
+
self._field_label(page, row, "Source")
|
|
1610
|
+
row += 1
|
|
1611
|
+
src_var, _ = self._option(page, row, AVAIL_RANDOM_STR)
|
|
1612
|
+
row += 1
|
|
1613
|
+
|
|
1614
|
+
self._field_label(page, row, "Length (bytes)")
|
|
1615
|
+
row += 1
|
|
1616
|
+
len_entry = self._entry(page, row, default="32")
|
|
1617
|
+
row += 1
|
|
1618
|
+
|
|
1619
|
+
self._field_label(page, row, "Output format")
|
|
1620
|
+
row += 1
|
|
1621
|
+
out_var, _ = self._option(page, row, ["Hex", "Base64", "Save to file"])
|
|
1622
|
+
row += 1
|
|
1623
|
+
|
|
1624
|
+
out_entry = self._entry(page, row, "Where to save the random data")
|
|
1625
|
+
browse_btn = self._file_row(page, row, out_entry, save=True)
|
|
1626
|
+
out_entry.grid_remove()
|
|
1627
|
+
browse_btn.grid_remove()
|
|
1628
|
+
row += 1
|
|
1629
|
+
|
|
1630
|
+
def toggle_output(*_):
|
|
1631
|
+
if out_var.get() == "Save to file":
|
|
1632
|
+
out_entry.grid()
|
|
1633
|
+
browse_btn.grid()
|
|
1634
|
+
else:
|
|
1635
|
+
out_entry.grid_remove()
|
|
1636
|
+
browse_btn.grid_remove()
|
|
1637
|
+
|
|
1638
|
+
out_var.trace_add("write", lambda *_: toggle_output())
|
|
1639
|
+
|
|
1640
|
+
btn, bar, status = self._run_row(page, row, "Generate", None)
|
|
1641
|
+
row += 2
|
|
1642
|
+
|
|
1643
|
+
result_box = ctk.CTkTextbox(
|
|
1644
|
+
page,
|
|
1645
|
+
height=90,
|
|
1646
|
+
fg_color=THEME["box_color"],
|
|
1647
|
+
text_color=THEME["content_text"],
|
|
1648
|
+
font=self._mono_font(12))
|
|
1649
|
+
result_box.grid(
|
|
1650
|
+
row=row,
|
|
1651
|
+
column=0,
|
|
1652
|
+
columnspan=2,
|
|
1653
|
+
sticky="ew",
|
|
1654
|
+
pady=(
|
|
1655
|
+
0,
|
|
1656
|
+
0))
|
|
1657
|
+
result_box.configure(state="disabled")
|
|
1658
|
+
|
|
1659
|
+
toggle_output()
|
|
1660
|
+
|
|
1661
|
+
def do_run():
|
|
1662
|
+
try:
|
|
1663
|
+
length = int(len_entry.get())
|
|
1664
|
+
except ValueError:
|
|
1665
|
+
self._error_win(
|
|
1666
|
+
"Invalid input",
|
|
1667
|
+
"Length must be a whole number.")
|
|
1668
|
+
return
|
|
1669
|
+
|
|
1670
|
+
def work():
|
|
1671
|
+
return wr.random_bytes(src_var.get(), length)
|
|
1672
|
+
|
|
1673
|
+
def on_success(data):
|
|
1674
|
+
fmt = out_var.get()
|
|
1675
|
+
if fmt == "Save to file":
|
|
1676
|
+
path = out_entry.get()
|
|
1677
|
+
if not path:
|
|
1678
|
+
status.configure(
|
|
1679
|
+
text="No output file chosen — showing as hex instead.",
|
|
1680
|
+
text_color=WARNING)
|
|
1681
|
+
text = data.hex()
|
|
1682
|
+
else:
|
|
1683
|
+
with open(path, "wb") as f:
|
|
1684
|
+
f.write(data)
|
|
1685
|
+
status.configure(
|
|
1686
|
+
text=f"Saved to {path}", text_color=SUCCESS)
|
|
1687
|
+
return
|
|
1688
|
+
elif fmt == "Base64":
|
|
1689
|
+
text = base64.b64encode(data).decode("ascii")
|
|
1690
|
+
else:
|
|
1691
|
+
text = data.hex()
|
|
1692
|
+
result_box.configure(state="normal")
|
|
1693
|
+
result_box.delete("1.0", "end")
|
|
1694
|
+
result_box.insert("1.0", text)
|
|
1695
|
+
result_box.configure(state="disabled")
|
|
1696
|
+
|
|
1697
|
+
self._run_async(
|
|
1698
|
+
work,
|
|
1699
|
+
btn,
|
|
1700
|
+
bar,
|
|
1701
|
+
status,
|
|
1702
|
+
on_success=on_success,
|
|
1703
|
+
start_msg="Generating…")
|
|
1704
|
+
|
|
1705
|
+
btn.configure(command=do_run)
|
|
1706
|
+
|
|
1707
|
+
def _build_pwdgen(self):
|
|
1708
|
+
self._page_header(
|
|
1709
|
+
"dice",
|
|
1710
|
+
"Password Generator",
|
|
1711
|
+
"Create a strong, random passphrase.")
|
|
1712
|
+
page = self._page(row=1)
|
|
1713
|
+
|
|
1714
|
+
row = 0
|
|
1715
|
+
self._field_label(page, row, "Length (characters)")
|
|
1716
|
+
row += 1
|
|
1717
|
+
len_entry = self._entry(page, row, default="16")
|
|
1718
|
+
row += 1
|
|
1719
|
+
|
|
1720
|
+
self._field_label(page, row, "Random source")
|
|
1721
|
+
row += 1
|
|
1722
|
+
src_var, _ = self._option(
|
|
1723
|
+
page, row, AVAIL_RANDOM_STR, command=lambda v: try_generate(False))
|
|
1724
|
+
row += 1
|
|
1725
|
+
|
|
1726
|
+
upper_var = self._checkbox(page, row, "Uppercase letters (A–Z)", True)
|
|
1727
|
+
row += 1
|
|
1728
|
+
lower_var = self._checkbox(page, row, "Lowercase letters (a–z)", True)
|
|
1729
|
+
row += 1
|
|
1730
|
+
digit_var = self._checkbox(page, row, "Digits (0–9)", True)
|
|
1731
|
+
row += 1
|
|
1732
|
+
symbol_var = self._checkbox(page, row, "Symbols (!@#$…)", True)
|
|
1733
|
+
row += 1
|
|
1734
|
+
|
|
1735
|
+
result_entry = ctk.CTkEntry(
|
|
1736
|
+
page,
|
|
1737
|
+
fg_color=THEME["box_color"],
|
|
1738
|
+
border_color=THEME["box_border"],
|
|
1739
|
+
text_color=THEME["content_text"],
|
|
1740
|
+
font=self._mono_font(14))
|
|
1741
|
+
result_entry.grid(row=row, column=0, sticky="ew", pady=(10, 0))
|
|
1742
|
+
|
|
1743
|
+
def copy():
|
|
1744
|
+
self.clipboard_clear()
|
|
1745
|
+
self.clipboard_append(result_entry.get())
|
|
1746
|
+
status.configure(text="Copied to clipboard.", text_color=SUCCESS)
|
|
1747
|
+
|
|
1748
|
+
self._ghost_button(
|
|
1749
|
+
page, "Copy", copy, width=70).grid(
|
|
1750
|
+
row=row, column=1, sticky="w", padx=(
|
|
1751
|
+
8, 0), pady=(
|
|
1752
|
+
10, 0))
|
|
1753
|
+
row += 1
|
|
1754
|
+
|
|
1755
|
+
status = ctk.CTkLabel(
|
|
1756
|
+
page,
|
|
1757
|
+
text="",
|
|
1758
|
+
text_color=THEME["slight_gray"],
|
|
1759
|
+
font=self._font(11),
|
|
1760
|
+
anchor="w")
|
|
1761
|
+
status.grid(row=row, column=0, sticky="w", pady=(6, 0))
|
|
1762
|
+
|
|
1763
|
+
def try_generate(show_errors):
|
|
1764
|
+
try:
|
|
1765
|
+
length = int(len_entry.get())
|
|
1766
|
+
except ValueError:
|
|
1767
|
+
if show_errors:
|
|
1768
|
+
self._error_win(
|
|
1769
|
+
"Invalid input",
|
|
1770
|
+
"Length must be a whole number.")
|
|
1771
|
+
return
|
|
1772
|
+
try:
|
|
1773
|
+
pwd = wr.generate_password(
|
|
1774
|
+
length,
|
|
1775
|
+
upper_var.get(),
|
|
1776
|
+
lower_var.get(),
|
|
1777
|
+
digit_var.get(),
|
|
1778
|
+
symbol_var.get(),
|
|
1779
|
+
random_source=src_var.get())
|
|
1780
|
+
except GeneralError as e:
|
|
1781
|
+
if show_errors:
|
|
1782
|
+
self._error_win("Cannot generate", str(e))
|
|
1783
|
+
return
|
|
1784
|
+
result_entry.delete(0, "end")
|
|
1785
|
+
result_entry.insert(0, pwd)
|
|
1786
|
+
status.configure(text="", text_color=THEME["slight_gray"])
|
|
1787
|
+
|
|
1788
|
+
self._primary_button(
|
|
1789
|
+
page,
|
|
1790
|
+
"Regenerate",
|
|
1791
|
+
lambda: try_generate(True)).grid(
|
|
1792
|
+
row=row,
|
|
1793
|
+
column=1,
|
|
1794
|
+
sticky="e",
|
|
1795
|
+
pady=(
|
|
1796
|
+
6,
|
|
1797
|
+
0))
|
|
1798
|
+
|
|
1799
|
+
for var in (upper_var, lower_var, digit_var, symbol_var):
|
|
1800
|
+
var.trace_add("write", lambda *_: try_generate(False))
|
|
1801
|
+
len_entry.bind("<KeyRelease>", lambda e: try_generate(False))
|
|
1802
|
+
|
|
1803
|
+
try_generate(True)
|
|
1804
|
+
|
|
1805
|
+
def _build_benchmark(self):
|
|
1806
|
+
self._page_header(
|
|
1807
|
+
"gauge-high",
|
|
1808
|
+
"Benchmark",
|
|
1809
|
+
"Measure this machine's cryptographic performance.")
|
|
1810
|
+
page = self._page(row=1)
|
|
1811
|
+
|
|
1812
|
+
row = 0
|
|
1813
|
+
self._field_label(page, row, "Data size (bytes, default 1024)")
|
|
1814
|
+
row += 1
|
|
1815
|
+
len_entry = self._entry(page, row, default="1024")
|
|
1816
|
+
row += 1
|
|
1817
|
+
self._field_label(page, row, "RSA key size")
|
|
1818
|
+
row += 1
|
|
1819
|
+
rsa_var, _ = self._option(
|
|
1820
|
+
page, row, [
|
|
1821
|
+
str(size) for size in COMMON_RSA_SIZE], default="2048")
|
|
1822
|
+
row += 1
|
|
1823
|
+
ctk.CTkLabel(
|
|
1824
|
+
page,
|
|
1825
|
+
text="Direct RSA-OAEP cannot benchmark messages larger than the selected key permits; "
|
|
1826
|
+
"those rows will report an error instead of a timing.",
|
|
1827
|
+
text_color=THEME["gray_text"],
|
|
1828
|
+
font=self._font(10),
|
|
1829
|
+
anchor="w",
|
|
1830
|
+
justify="left",
|
|
1831
|
+
wraplength=560).grid(
|
|
1832
|
+
row=row,
|
|
1833
|
+
column=0,
|
|
1834
|
+
columnspan=2,
|
|
1835
|
+
sticky="w",
|
|
1836
|
+
pady=(
|
|
1837
|
+
0,
|
|
1838
|
+
4))
|
|
1839
|
+
row += 1
|
|
1840
|
+
|
|
1841
|
+
bar = ctk.CTkProgressBar(
|
|
1842
|
+
page, mode="determinate", progress_color=ACCENT)
|
|
1843
|
+
bar.set(0)
|
|
1844
|
+
bar.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
|
1845
|
+
row += 1
|
|
1846
|
+
status = ctk.CTkLabel(
|
|
1847
|
+
page,
|
|
1848
|
+
text="",
|
|
1849
|
+
text_color=THEME["slight_gray"],
|
|
1850
|
+
font=self._font(11),
|
|
1851
|
+
anchor="w")
|
|
1852
|
+
status.grid(row=row, column=0, sticky="w", pady=(6, 8))
|
|
1853
|
+
btn = self._primary_button(page, "Run Benchmark")
|
|
1854
|
+
btn.grid(row=row, column=1, sticky="e", pady=(6, 8))
|
|
1855
|
+
row += 1
|
|
1856
|
+
|
|
1857
|
+
results_box = ctk.CTkTextbox(
|
|
1858
|
+
page,
|
|
1859
|
+
height=260,
|
|
1860
|
+
fg_color=THEME["box_color"],
|
|
1861
|
+
text_color=THEME["content_text"],
|
|
1862
|
+
font=self._mono_font(12))
|
|
1863
|
+
results_box.grid(
|
|
1864
|
+
row=row,
|
|
1865
|
+
column=0,
|
|
1866
|
+
columnspan=2,
|
|
1867
|
+
sticky="ew",
|
|
1868
|
+
pady=(
|
|
1869
|
+
0,
|
|
1870
|
+
0))
|
|
1871
|
+
results_box.configure(state="disabled")
|
|
1872
|
+
|
|
1873
|
+
def do_run():
|
|
1874
|
+
try:
|
|
1875
|
+
length = int(len_entry.get())
|
|
1876
|
+
keysize = int(rsa_var.get())
|
|
1877
|
+
except ValueError:
|
|
1878
|
+
self._error_win("Invalid input",
|
|
1879
|
+
"Data size and key size must be numeric.")
|
|
1880
|
+
return
|
|
1881
|
+
|
|
1882
|
+
def launch():
|
|
1883
|
+
algorithms = AVAIL_HASH_STR + AVAIL_ALG + AVAIL_RANDOM_STR
|
|
1884
|
+
btn.configure(state="disabled")
|
|
1885
|
+
bar.set(0)
|
|
1886
|
+
results_box.configure(state="normal")
|
|
1887
|
+
results_box.delete("1.0", "end")
|
|
1888
|
+
results_box.configure(state="disabled")
|
|
1889
|
+
|
|
1890
|
+
def target():
|
|
1891
|
+
for i, alg in enumerate(algorithms):
|
|
1892
|
+
try:
|
|
1893
|
+
result = wr.run_benchmark(alg, length, keysize)
|
|
1894
|
+
ok = True
|
|
1895
|
+
except Exception as e:
|
|
1896
|
+
result = str(e)
|
|
1897
|
+
ok = False
|
|
1898
|
+
self.after(
|
|
1899
|
+
0,
|
|
1900
|
+
lambda alg=alg, result=result, ok=ok, i=i: self._benchmark_row(
|
|
1901
|
+
results_box, bar, status, alg, result, ok, i + 1, len(algorithms)
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
self.after(0, lambda: btn.configure(state="normal"))
|
|
1905
|
+
|
|
1906
|
+
threading.Thread(target=target, daemon=True).start()
|
|
1907
|
+
|
|
1908
|
+
self._confirm_large_rsa(keysize, launch)
|
|
1909
|
+
|
|
1910
|
+
btn.configure(command=do_run)
|
|
1911
|
+
|
|
1912
|
+
def _benchmark_row(self, box, bar, status, alg, result, ok, done, total):
|
|
1913
|
+
box.configure(state="normal")
|
|
1914
|
+
|
|
1915
|
+
# Section banners — printed once, right before the first algorithm
|
|
1916
|
+
# of each category.
|
|
1917
|
+
if alg in AVAIL_HASH_STR and AVAIL_HASH_STR.index(alg) == 0:
|
|
1918
|
+
box.insert("end", "========== HASH ALGORITHMS ==========\n")
|
|
1919
|
+
if alg in AVAIL_ALG and AVAIL_ALG.index(alg) == 0:
|
|
1920
|
+
box.insert("end", "\n========== CRYPTO ALGORITHMS ==========\n")
|
|
1921
|
+
if alg in AVAIL_RANDOM_STR and AVAIL_RANDOM_STR.index(alg) == 0:
|
|
1922
|
+
box.insert("end", "\n========== RANDOM ALGORITHMS ==========\n")
|
|
1923
|
+
|
|
1924
|
+
if not ok:
|
|
1925
|
+
# Show exactly which algorithm the error came from, and wrap the
|
|
1926
|
+
# (often long) formatted CryptoError/GeneralError message with a
|
|
1927
|
+
# hanging indent so the table stays readable.
|
|
1928
|
+
wrapped = textwrap.fill(
|
|
1929
|
+
result,
|
|
1930
|
+
width=96,
|
|
1931
|
+
subsequent_indent=" " * 22,
|
|
1932
|
+
break_long_words=False)
|
|
1933
|
+
box.insert("end", f"{alg:<22}{wrapped}\n")
|
|
1934
|
+
elif alg in AVAIL_ALG:
|
|
1935
|
+
(enc, dec), keygen = result
|
|
1936
|
+
box.insert(
|
|
1937
|
+
"end", f"{
|
|
1938
|
+
enc[0]:<22}{
|
|
1939
|
+
enc[1]}\n{
|
|
1940
|
+
dec[0]:<22}{
|
|
1941
|
+
dec[1]}\n{
|
|
1942
|
+
keygen[0]:<22}{
|
|
1943
|
+
keygen[1]}\n")
|
|
1944
|
+
else:
|
|
1945
|
+
name, value = result
|
|
1946
|
+
box.insert("end", f"{name:<22}{value}\n")
|
|
1947
|
+
|
|
1948
|
+
box.see("end")
|
|
1949
|
+
box.configure(state="disabled")
|
|
1950
|
+
bar.set(done / total)
|
|
1951
|
+
status.configure(
|
|
1952
|
+
text=f"{done}/{total} complete",
|
|
1953
|
+
text_color=THEME["slight_gray"] if done < total else SUCCESS
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
# ------------------------------------------------------------------ #
|
|
1957
|
+
# Secure delete / wipe free space
|
|
1958
|
+
# ------------------------------------------------------------------ #
|
|
1959
|
+
|
|
1960
|
+
def _build_sdelete(self):
|
|
1961
|
+
self._page_header(
|
|
1962
|
+
"trash",
|
|
1963
|
+
"Secure Delete",
|
|
1964
|
+
"Permanently remove a file with multi-pass overwrite.")
|
|
1965
|
+
page = self._page(row=1)
|
|
1966
|
+
|
|
1967
|
+
row = 0
|
|
1968
|
+
self._field_label(page, row, "File to shred")
|
|
1969
|
+
row += 1
|
|
1970
|
+
file_entry = self._entry(page, row, "File to permanently remove")
|
|
1971
|
+
shred_paths = []
|
|
1972
|
+
self._file_row(
|
|
1973
|
+
page,
|
|
1974
|
+
row,
|
|
1975
|
+
file_entry,
|
|
1976
|
+
multiple=True,
|
|
1977
|
+
selected=shred_paths)
|
|
1978
|
+
row += 1
|
|
1979
|
+
|
|
1980
|
+
self._field_label(page, row, "Overwrite pattern")
|
|
1981
|
+
row += 1
|
|
1982
|
+
method_var, _ = self._option(page, row, OVERWRITE_OPTIONS)
|
|
1983
|
+
row += 1
|
|
1984
|
+
|
|
1985
|
+
self._field_label(
|
|
1986
|
+
page, row, "Random source (used by 'random'/'gutmann')")
|
|
1987
|
+
row += 1
|
|
1988
|
+
rand_var, _ = self._option(page, row, AVAIL_RANDOM_STR)
|
|
1989
|
+
row += 1
|
|
1990
|
+
|
|
1991
|
+
self._field_label(page, row, "Passes")
|
|
1992
|
+
row += 1
|
|
1993
|
+
repeat_entry = self._entry(page, row, default="1")
|
|
1994
|
+
row += 1
|
|
1995
|
+
|
|
1996
|
+
self._field_label(page, row, "Chunk size (KB)")
|
|
1997
|
+
row += 1
|
|
1998
|
+
chunk_entry = self._entry(page, row, default="1024")
|
|
1999
|
+
row += 1
|
|
2000
|
+
|
|
2001
|
+
zero_var = self._checkbox(
|
|
2002
|
+
page, row, "Zero-out after overwriting", True)
|
|
2003
|
+
row += 1
|
|
2004
|
+
ctk.CTkLabel(
|
|
2005
|
+
page, text="Zero-out adds one additional overwrite pass.",
|
|
2006
|
+
text_color=THEME["gray_text"], font=self._font(10), anchor="w"
|
|
2007
|
+
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(0, 4))
|
|
2008
|
+
row += 1
|
|
2009
|
+
delete_var = self._checkbox(
|
|
2010
|
+
page, row, "Rename and delete the file afterwards", True)
|
|
2011
|
+
row += 1
|
|
2012
|
+
|
|
2013
|
+
btn, bar, status = self._run_row(page, row, "Shred File", None)
|
|
2014
|
+
|
|
2015
|
+
def do_run():
|
|
2016
|
+
paths = shred_paths or [
|
|
2017
|
+
path.strip() for path in file_entry.get().split(",") if path.strip()]
|
|
2018
|
+
if not paths:
|
|
2019
|
+
self._error_win("Missing information", "Please choose a file.")
|
|
2020
|
+
return
|
|
2021
|
+
try:
|
|
2022
|
+
chunksize = int(chunk_entry.get())
|
|
2023
|
+
repeat = int(repeat_entry.get())
|
|
2024
|
+
except ValueError:
|
|
2025
|
+
self._error_win("Invalid input",
|
|
2026
|
+
"Chunk size and passes must be numeric.")
|
|
2027
|
+
return
|
|
2028
|
+
|
|
2029
|
+
if method_var.get() == "gutmann" and repeat > 1:
|
|
2030
|
+
warning = CTkMessagebox(
|
|
2031
|
+
self,
|
|
2032
|
+
icon="warning",
|
|
2033
|
+
title="Gutmann already uses 32 passes",
|
|
2034
|
+
message="Gutmann performs 32 passes. Additional passes repeat the full sequence. Continue?",
|
|
2035
|
+
option_1="Cancel",
|
|
2036
|
+
option_2="Continue")
|
|
2037
|
+
if warning.get() != "Continue":
|
|
2038
|
+
return
|
|
2039
|
+
|
|
2040
|
+
confirm = CTkMessagebox(
|
|
2041
|
+
self,
|
|
2042
|
+
icon="warning",
|
|
2043
|
+
title="This cannot be undone",
|
|
2044
|
+
message=f"{
|
|
2045
|
+
len(paths)} selected file(s) will be overwritten and permanently deleted. Continue?",
|
|
2046
|
+
option_1="Cancel",
|
|
2047
|
+
option_2="Shred")
|
|
2048
|
+
if confirm.get() != "Shred":
|
|
2049
|
+
return
|
|
2050
|
+
|
|
2051
|
+
def work():
|
|
2052
|
+
return wr.secure_delete_paths(
|
|
2053
|
+
paths,
|
|
2054
|
+
method_var.get(),
|
|
2055
|
+
zero_var.get(),
|
|
2056
|
+
delete_var.get(),
|
|
2057
|
+
chunksize,
|
|
2058
|
+
rand_var.get(),
|
|
2059
|
+
repeat)
|
|
2060
|
+
|
|
2061
|
+
self._run_async(
|
|
2062
|
+
work,
|
|
2063
|
+
btn,
|
|
2064
|
+
bar,
|
|
2065
|
+
status,
|
|
2066
|
+
on_success=lambda deleted: status.configure(
|
|
2067
|
+
text=f"{
|
|
2068
|
+
len(deleted)} file(s) securely removed.",
|
|
2069
|
+
text_color=SUCCESS),
|
|
2070
|
+
start_msg="Shredding…",
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
btn.configure(command=do_run)
|
|
2074
|
+
|
|
2075
|
+
def _build_wipe_space(self):
|
|
2076
|
+
self._page_header(
|
|
2077
|
+
"hard-drive",
|
|
2078
|
+
"Wipe Free Space",
|
|
2079
|
+
"Overwrite unused disk space so deleted files can't be recovered.")
|
|
2080
|
+
page = self._page(row=1)
|
|
2081
|
+
|
|
2082
|
+
try:
|
|
2083
|
+
partitions = wr.list_partitions()
|
|
2084
|
+
except Exception:
|
|
2085
|
+
partitions = []
|
|
2086
|
+
device_values = [p.device for p in partitions] or [
|
|
2087
|
+
"No partitions found"]
|
|
2088
|
+
|
|
2089
|
+
row = 0
|
|
2090
|
+
self._field_label(page, row, "Partition / device")
|
|
2091
|
+
row += 1
|
|
2092
|
+
device_var, _ = self._option(page, row, device_values)
|
|
2093
|
+
row += 1
|
|
2094
|
+
|
|
2095
|
+
self._field_label(page, row, "Fill pattern")
|
|
2096
|
+
row += 1
|
|
2097
|
+
fill_var, _ = self._option(page, row, ["Random data", "Zeros"])
|
|
2098
|
+
row += 1
|
|
2099
|
+
|
|
2100
|
+
self._field_label(page, row, "Random source (used by 'Random data')")
|
|
2101
|
+
row += 1
|
|
2102
|
+
rand_var, _ = self._option(page, row, AVAIL_RANDOM_STR)
|
|
2103
|
+
row += 1
|
|
2104
|
+
|
|
2105
|
+
self._field_label(page, row, "Chunk size (KB)")
|
|
2106
|
+
row += 1
|
|
2107
|
+
chunk_entry = self._entry(page, row, default="1024")
|
|
2108
|
+
row += 1
|
|
2109
|
+
|
|
2110
|
+
btn, bar, status = self._run_row(page, row, "Wipe Free Space", None)
|
|
2111
|
+
|
|
2112
|
+
def do_run():
|
|
2113
|
+
device = device_var.get()
|
|
2114
|
+
if device not in [p.device for p in partitions]:
|
|
2115
|
+
self._error_win(
|
|
2116
|
+
"No device", "No writable partition was found.")
|
|
2117
|
+
return
|
|
2118
|
+
try:
|
|
2119
|
+
chunksize = int(chunk_entry.get())
|
|
2120
|
+
except ValueError:
|
|
2121
|
+
self._error_win("Invalid input", "Chunk size must be numeric.")
|
|
2122
|
+
return
|
|
2123
|
+
|
|
2124
|
+
confirm = CTkMessagebox(
|
|
2125
|
+
self,
|
|
2126
|
+
icon="warning",
|
|
2127
|
+
title="This may take a while",
|
|
2128
|
+
message=f"This will fill all free space on '{device}' before removing the temporary file. Continue?",
|
|
2129
|
+
option_1="Cancel",
|
|
2130
|
+
option_2="Wipe")
|
|
2131
|
+
if confirm.get() != "Wipe":
|
|
2132
|
+
return
|
|
2133
|
+
|
|
2134
|
+
def work():
|
|
2135
|
+
wr.wipe_free_space(
|
|
2136
|
+
device,
|
|
2137
|
+
chunksize,
|
|
2138
|
+
fill_var.get() == "Zeros",
|
|
2139
|
+
rand_var.get())
|
|
2140
|
+
return device
|
|
2141
|
+
|
|
2142
|
+
self._run_async(
|
|
2143
|
+
work,
|
|
2144
|
+
btn,
|
|
2145
|
+
bar,
|
|
2146
|
+
status,
|
|
2147
|
+
on_success=lambda d: status.configure(
|
|
2148
|
+
text=f"Free space on '{d}' has been wiped.",
|
|
2149
|
+
text_color=SUCCESS),
|
|
2150
|
+
start_msg="Wiping free space… this can take a while.",
|
|
2151
|
+
)
|
|
2152
|
+
|
|
2153
|
+
btn.configure(command=do_run)
|
|
2154
|
+
|
|
2155
|
+
# ------------------------------------------------------------------ #
|
|
2156
|
+
# About
|
|
2157
|
+
# ------------------------------------------------------------------ #
|
|
2158
|
+
|
|
2159
|
+
def _build_about(self):
|
|
2160
|
+
wrap = ctk.CTkFrame(self.content, fg_color="transparent")
|
|
2161
|
+
wrap.grid(row=0, column=0, sticky="new", padx=32, pady=(36, 30))
|
|
2162
|
+
wrap.grid_columnconfigure(0, weight=1, minsize=0)
|
|
2163
|
+
|
|
2164
|
+
hero = ctk.CTkFrame(wrap, fg_color="transparent")
|
|
2165
|
+
hero.grid(row=0, column=0, sticky="ew")
|
|
2166
|
+
hero.grid_columnconfigure(1, weight=1)
|
|
2167
|
+
|
|
2168
|
+
try:
|
|
2169
|
+
icon = ctk.CTkImage(
|
|
2170
|
+
Image.open(ICON_PNG),
|
|
2171
|
+
Image.open(ICON_PNG),
|
|
2172
|
+
(84, 84)
|
|
2173
|
+
)
|
|
2174
|
+
ctk.CTkLabel(
|
|
2175
|
+
hero,
|
|
2176
|
+
width=84,
|
|
2177
|
+
image=icon,
|
|
2178
|
+
text="",
|
|
2179
|
+
anchor="nw").grid(
|
|
2180
|
+
row=0,
|
|
2181
|
+
column=0,
|
|
2182
|
+
rowspan=3,
|
|
2183
|
+
sticky="nw")
|
|
2184
|
+
except Exception:
|
|
2185
|
+
pass
|
|
2186
|
+
|
|
2187
|
+
ctk.CTkLabel(
|
|
2188
|
+
hero,
|
|
2189
|
+
text="Ghostbytes",
|
|
2190
|
+
text_color=THEME["content_text"],
|
|
2191
|
+
font=self._font(
|
|
2192
|
+
30,
|
|
2193
|
+
"bold"),
|
|
2194
|
+
anchor="w").grid(
|
|
2195
|
+
row=0,
|
|
2196
|
+
column=1,
|
|
2197
|
+
sticky="sw",
|
|
2198
|
+
padx=(
|
|
2199
|
+
18,
|
|
2200
|
+
0))
|
|
2201
|
+
ctk.CTkLabel(
|
|
2202
|
+
hero,
|
|
2203
|
+
text="File Encryption Utility",
|
|
2204
|
+
text_color=ACCENT,
|
|
2205
|
+
font=self._font(
|
|
2206
|
+
14,
|
|
2207
|
+
"bold"),
|
|
2208
|
+
anchor="w").grid(
|
|
2209
|
+
row=1,
|
|
2210
|
+
column=1,
|
|
2211
|
+
sticky="nw",
|
|
2212
|
+
padx=(
|
|
2213
|
+
18,
|
|
2214
|
+
0))
|
|
2215
|
+
ctk.CTkLabel(
|
|
2216
|
+
hero,
|
|
2217
|
+
text="A military-grade cryptographic toolkit for file encryption, key "
|
|
2218
|
+
"management, and secure data handling — built for privacy, from the "
|
|
2219
|
+
"ground up.",
|
|
2220
|
+
text_color=THEME["slight_gray"],
|
|
2221
|
+
font=self._font(13),
|
|
2222
|
+
anchor="w",
|
|
2223
|
+
justify="left",
|
|
2224
|
+
wraplength=560).grid(
|
|
2225
|
+
row=2,
|
|
2226
|
+
column=1,
|
|
2227
|
+
sticky="nw",
|
|
2228
|
+
padx=(
|
|
2229
|
+
18,
|
|
2230
|
+
0),
|
|
2231
|
+
pady=(
|
|
2232
|
+
6,
|
|
2233
|
+
0))
|
|
2234
|
+
|
|
2235
|
+
chips = ctk.CTkFrame(wrap, fg_color="transparent")
|
|
2236
|
+
chips.grid(row=1, column=0, sticky="w", pady=(20, 0))
|
|
2237
|
+
for i, cap in enumerate(CAPABILITIES):
|
|
2238
|
+
chip = ctk.CTkFrame(
|
|
2239
|
+
chips,
|
|
2240
|
+
fg_color=ACCENT_TINT,
|
|
2241
|
+
border_width=1,
|
|
2242
|
+
border_color=ACCENT_BORDER,
|
|
2243
|
+
corner_radius=999)
|
|
2244
|
+
chip.grid(
|
|
2245
|
+
row=0, column=i, sticky="w", padx=(
|
|
2246
|
+
0 if i == 0 else 8, 0))
|
|
2247
|
+
ctk.CTkLabel(
|
|
2248
|
+
chip,
|
|
2249
|
+
text=cap,
|
|
2250
|
+
text_color=ACCENT,
|
|
2251
|
+
font=self._font(
|
|
2252
|
+
11,
|
|
2253
|
+
"bold")).pack(
|
|
2254
|
+
padx=14,
|
|
2255
|
+
pady=6)
|
|
2256
|
+
|
|
2257
|
+
divider = ctk.CTkFrame(wrap, height=1, fg_color=THEME["box_border"])
|
|
2258
|
+
divider.grid(row=2, column=0, sticky="ew", pady=(28, 22))
|
|
2259
|
+
|
|
2260
|
+
info = ctk.CTkFrame(wrap, fg_color="transparent")
|
|
2261
|
+
info.grid(row=3, column=0, sticky="ew")
|
|
2262
|
+
info.grid_columnconfigure(2, weight=1)
|
|
2263
|
+
|
|
2264
|
+
for i, (icon_name, label, value) in enumerate(ABOUT_BOX_CONTENT):
|
|
2265
|
+
row_pad = (8, 4)
|
|
2266
|
+
|
|
2267
|
+
icon_badge = ctk.CTkLabel(
|
|
2268
|
+
info, text="", width=20, image=icon_to_ctkimage(
|
|
2269
|
+
icon_name, fill=ACCENT, scale_to_width=14))
|
|
2270
|
+
icon_badge.grid(
|
|
2271
|
+
row=i, column=0, sticky="w", padx=(
|
|
2272
|
+
0, 10), pady=row_pad)
|
|
2273
|
+
|
|
2274
|
+
ctk.CTkLabel(
|
|
2275
|
+
info,
|
|
2276
|
+
text=label,
|
|
2277
|
+
text_color=THEME["slight_gray"],
|
|
2278
|
+
font=self._font(12),
|
|
2279
|
+
anchor="w",
|
|
2280
|
+
width=70).grid(
|
|
2281
|
+
row=i,
|
|
2282
|
+
column=1,
|
|
2283
|
+
sticky="w",
|
|
2284
|
+
pady=row_pad)
|
|
2285
|
+
|
|
2286
|
+
content = ctk.CTkLabel(
|
|
2287
|
+
info,
|
|
2288
|
+
text=value,
|
|
2289
|
+
text_color=THEME["content_text"],
|
|
2290
|
+
font=self._mono_font(12),
|
|
2291
|
+
justify="left",
|
|
2292
|
+
anchor="w")
|
|
2293
|
+
content.grid(
|
|
2294
|
+
row=i, column=2, sticky="w", padx=(
|
|
2295
|
+
0, 20), pady=row_pad)
|
|
2296
|
+
|
|
2297
|
+
if label == "Source":
|
|
2298
|
+
content.configure(text_color=ACCENT, cursor="hand2")
|
|
2299
|
+
content.bind(
|
|
2300
|
+
"<Button-1>",
|
|
2301
|
+
lambda _e,
|
|
2302
|
+
url=value: webbrowser.open_new_tab(url))
|
|
2303
|
+
|
|
2304
|
+
# ------------------------------------------------------------------ #
|
|
2305
|
+
# Shared "every CryptoConfig field is editable" advanced-settings block
|
|
2306
|
+
# ------------------------------------------------------------------ #
|
|
2307
|
+
|
|
2308
|
+
def _build_config_fields(self, inner):
|
|
2309
|
+
fields = {}
|
|
2310
|
+
fields["hash"], hash_menu = self._option_grid(
|
|
2311
|
+
inner, 0, AVAIL_HASH_STR, label="Hash function")
|
|
2312
|
+
fields["store_iv"], _ = self._option_grid(
|
|
2313
|
+
inner, 1, ["append", "prepend"], label="Store IV / tag")
|
|
2314
|
+
fields["mac_len"] = self._entry_grid(
|
|
2315
|
+
inner, 2, "MAC length (4–16 bytes)", "16")
|
|
2316
|
+
fields["rand_func"], rand_menu = self._option_grid(
|
|
2317
|
+
inner, 3, AVAIL_RANDOM_STR, label="Random function")
|
|
2318
|
+
fields["kdf_salt"] = self._entry_grid(
|
|
2319
|
+
inner, 4, "KDF salt", CryptoConfig.kdf_salt.decode(
|
|
2320
|
+
"utf-8", "replace"), mono=True)
|
|
2321
|
+
fields["kdf_time"] = self._entry_grid(inner, 5, "KDF time cost", "16")
|
|
2322
|
+
fields["kdf_mem"] = self._entry_grid(
|
|
2323
|
+
inner, 6, "KDF memory (MB)", "128")
|
|
2324
|
+
fields["kdf_par"] = self._entry_grid(inner, 7, "KDF parallelism", "8")
|
|
2325
|
+
note = ctk.CTkLabel(
|
|
2326
|
+
inner,
|
|
2327
|
+
text="The KDF salt must match on every system that needs to decrypt this "
|
|
2328
|
+
"data — leave it default unless you control both ends.",
|
|
2329
|
+
text_color=THEME["gray_text"],
|
|
2330
|
+
font=self._font(10),
|
|
2331
|
+
anchor="w",
|
|
2332
|
+
justify="left",
|
|
2333
|
+
wraplength=380)
|
|
2334
|
+
note.grid(
|
|
2335
|
+
row=8, column=0, columnspan=2, sticky="w", padx=(
|
|
2336
|
+
12, 12), pady=(
|
|
2337
|
+
2, 10))
|
|
2338
|
+
fields["hash_widgets"] = [
|
|
2339
|
+
hash_menu, hash_menu.master.grid_slaves(
|
|
2340
|
+
row=0, column=0)[0]]
|
|
2341
|
+
fields["rand_widgets"] = [
|
|
2342
|
+
rand_menu, rand_menu.master.grid_slaves(
|
|
2343
|
+
row=3, column=0)[0]]
|
|
2344
|
+
fields["kdf_widgets"] = [
|
|
2345
|
+
fields["kdf_salt"], fields["kdf_salt"]._label_widget,
|
|
2346
|
+
fields["kdf_time"], fields["kdf_time"]._label_widget,
|
|
2347
|
+
fields["kdf_mem"], fields["kdf_mem"]._label_widget,
|
|
2348
|
+
fields["kdf_par"], fields["kdf_par"]._label_widget, note,
|
|
2349
|
+
]
|
|
2350
|
+
return fields
|
|
2351
|
+
|
|
2352
|
+
def _config_from_fields(
|
|
2353
|
+
self,
|
|
2354
|
+
fields,
|
|
2355
|
+
allow_empty_salt=False,
|
|
2356
|
+
algorithm=CryptoConfig.algorithm):
|
|
2357
|
+
try:
|
|
2358
|
+
mac_len = int(fields["mac_len"].get())
|
|
2359
|
+
kdf_time = int(fields["kdf_time"].get())
|
|
2360
|
+
kdf_mem = int(fields["kdf_mem"].get())
|
|
2361
|
+
kdf_par = int(fields["kdf_par"].get())
|
|
2362
|
+
except ValueError as exc:
|
|
2363
|
+
raise invalid_argument(
|
|
2364
|
+
"advanced settings",
|
|
2365
|
+
"must contain numeric MAC and KDF values") from exc
|
|
2366
|
+
|
|
2367
|
+
if not 4 <= mac_len <= 16:
|
|
2368
|
+
raise invalid_argument(
|
|
2369
|
+
"MAC length", "must be between 4 and 16 bytes")
|
|
2370
|
+
if kdf_time < 1 or kdf_mem < 1 or kdf_par < 1:
|
|
2371
|
+
raise invalid_argument("KDF settings", "must all be at least 1")
|
|
2372
|
+
|
|
2373
|
+
salt = fields["kdf_salt"].get()
|
|
2374
|
+
if not salt:
|
|
2375
|
+
if not allow_empty_salt:
|
|
2376
|
+
raise invalid_argument("KDF salt", "cannot be empty")
|
|
2377
|
+
salt = CryptoConfig.kdf_salt.decode("utf-8")
|
|
2378
|
+
elif salt.startswith("base64:"):
|
|
2379
|
+
try:
|
|
2380
|
+
salt = base64.b64decode(salt[7:], validate=True)
|
|
2381
|
+
except ValueError as exc:
|
|
2382
|
+
raise invalid_argument(
|
|
2383
|
+
"KDF salt", "must contain valid base64 data") from exc
|
|
2384
|
+
|
|
2385
|
+
return wr.build_config(
|
|
2386
|
+
algorithm=algorithm,
|
|
2387
|
+
store_iv=fields["store_iv"].get(),
|
|
2388
|
+
mac_len=mac_len,
|
|
2389
|
+
kdf_salt=salt,
|
|
2390
|
+
kdf_time_cost=kdf_time,
|
|
2391
|
+
kdf_memory_cost=kdf_mem * 1024,
|
|
2392
|
+
kdf_parallelism=kdf_par,
|
|
2393
|
+
hash_func_str=fields["hash"].get(),
|
|
2394
|
+
rand_func=fields["rand_func"].get(),
|
|
2395
|
+
)
|
|
2396
|
+
|
|
2397
|
+
# ------------------------------------------------------------------ #
|
|
2398
|
+
# Large-RSA-key warning (session-scoped "don't remind me")
|
|
2399
|
+
# ------------------------------------------------------------------ #
|
|
2400
|
+
|
|
2401
|
+
def _confirm_large_rsa(self, size_bits, on_continue):
|
|
2402
|
+
if size_bits <= RSA_SIZE_WARNING_THRESHOLD or self._suppress_rsa_warning:
|
|
2403
|
+
on_continue()
|
|
2404
|
+
return
|
|
2405
|
+
|
|
2406
|
+
msg = CTkMessagebox(
|
|
2407
|
+
self, icon="warning", title="Large RSA key",
|
|
2408
|
+
message=(
|
|
2409
|
+
f"This uses a {size_bits}-bit RSA key. Generating RSA keys in Python is "
|
|
2410
|
+
f"inefficient at this size. For better speed and reliability, use "
|
|
2411
|
+
f"OpenSSL instead — e.g.\n\n"
|
|
2412
|
+
f" openssl genrsa -out key.pem {size_bits}\n\n"
|
|
2413
|
+
f"and importing the resulting file here.\n\nContinue anyway?"
|
|
2414
|
+
),
|
|
2415
|
+
option_1="Cancel", option_2="Continue", option_3="Continue, don't ask again",
|
|
2416
|
+
)
|
|
2417
|
+
choice = msg.get()
|
|
2418
|
+
if choice == "Continue":
|
|
2419
|
+
on_continue()
|
|
2420
|
+
elif choice == "Continue, don't ask again":
|
|
2421
|
+
self._suppress_rsa_warning = True
|
|
2422
|
+
on_continue()
|
|
2423
|
+
# Cancel / closed -> do nothing
|
|
2424
|
+
|
|
2425
|
+
# ------------------------------------------------------------------ #
|
|
2426
|
+
# Generic form-building helpers used by every tool page
|
|
2427
|
+
# ------------------------------------------------------------------ #
|
|
2428
|
+
|
|
2429
|
+
def _page_header(self, icon, title, subtitle=None):
|
|
2430
|
+
header = ctk.CTkFrame(self.content, fg_color="transparent")
|
|
2431
|
+
header.grid(row=0, column=0, sticky="new", padx=32, pady=(28, 18))
|
|
2432
|
+
header.grid_columnconfigure(1, weight=1)
|
|
2433
|
+
|
|
2434
|
+
badge = ctk.CTkFrame(
|
|
2435
|
+
header,
|
|
2436
|
+
width=40,
|
|
2437
|
+
height=40,
|
|
2438
|
+
corner_radius=10,
|
|
2439
|
+
fg_color=ACCENT_TINT,
|
|
2440
|
+
border_width=1,
|
|
2441
|
+
border_color=ACCENT_BORDER)
|
|
2442
|
+
badge.grid(row=0, column=0, rowspan=2, sticky="nw")
|
|
2443
|
+
badge.grid_propagate(False)
|
|
2444
|
+
ctk.CTkLabel(
|
|
2445
|
+
badge,
|
|
2446
|
+
text="",
|
|
2447
|
+
image=icon_to_ctkimage(
|
|
2448
|
+
icon,
|
|
2449
|
+
fill=ACCENT,
|
|
2450
|
+
scale_to_width=18)).place(
|
|
2451
|
+
relx=0.5,
|
|
2452
|
+
rely=0.5,
|
|
2453
|
+
anchor="center")
|
|
2454
|
+
|
|
2455
|
+
ctk.CTkLabel(
|
|
2456
|
+
header, text=title, text_color=THEME["content_text"],
|
|
2457
|
+
font=self._font(21, "bold"), anchor="w"
|
|
2458
|
+
).grid(row=0, column=1, sticky="sw", padx=(12, 0))
|
|
2459
|
+
|
|
2460
|
+
if subtitle:
|
|
2461
|
+
ctk.CTkLabel(
|
|
2462
|
+
header, text=subtitle, text_color=THEME["slight_gray"],
|
|
2463
|
+
font=self._font(12), anchor="w", justify="left"
|
|
2464
|
+
).grid(row=1, column=1, sticky="nw", padx=(12, 0))
|
|
2465
|
+
|
|
2466
|
+
divider = ctk.CTkFrame(header, height=1, fg_color=THEME["box_border"])
|
|
2467
|
+
divider.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(18, 0))
|
|
2468
|
+
|
|
2469
|
+
return header
|
|
2470
|
+
|
|
2471
|
+
def _page(self, row=1):
|
|
2472
|
+
"""A plain (unbordered) content area that page fields sit directly
|
|
2473
|
+
on — grouped with `_section` headings rather than boxed in a card,
|
|
2474
|
+
so input areas read as one continuous form."""
|
|
2475
|
+
frame = ctk.CTkFrame(self.content, fg_color="transparent")
|
|
2476
|
+
frame.grid(row=row, column=0, sticky="new", padx=32, pady=(0, 36))
|
|
2477
|
+
frame.grid_columnconfigure(0, weight=3)
|
|
2478
|
+
frame.grid_columnconfigure(1, weight=1, minsize=120)
|
|
2479
|
+
return frame
|
|
2480
|
+
|
|
2481
|
+
def _section(self, parent, row, title):
|
|
2482
|
+
wrap = ctk.CTkFrame(parent, fg_color="transparent")
|
|
2483
|
+
wrap.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(18, 8))
|
|
2484
|
+
tick = ctk.CTkFrame(
|
|
2485
|
+
wrap,
|
|
2486
|
+
width=3,
|
|
2487
|
+
height=13,
|
|
2488
|
+
fg_color=ACCENT,
|
|
2489
|
+
corner_radius=2)
|
|
2490
|
+
tick.grid(row=0, column=0, sticky="w")
|
|
2491
|
+
ctk.CTkLabel(
|
|
2492
|
+
wrap,
|
|
2493
|
+
text=title,
|
|
2494
|
+
text_color=THEME["content_text"],
|
|
2495
|
+
font=self._font(
|
|
2496
|
+
12,
|
|
2497
|
+
"bold"),
|
|
2498
|
+
anchor="w").grid(
|
|
2499
|
+
row=0,
|
|
2500
|
+
column=1,
|
|
2501
|
+
sticky="w",
|
|
2502
|
+
padx=(
|
|
2503
|
+
8,
|
|
2504
|
+
0))
|
|
2505
|
+
return wrap
|
|
2506
|
+
|
|
2507
|
+
def _field_label(self, parent, row, text):
|
|
2508
|
+
ctk.CTkLabel(
|
|
2509
|
+
parent, text=text, text_color=THEME["slight_gray"],
|
|
2510
|
+
font=self._font(11), anchor="w"
|
|
2511
|
+
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(8, 3))
|
|
2512
|
+
|
|
2513
|
+
def _entry(self, parent, row, placeholder="", show=None, default=""):
|
|
2514
|
+
entry = ctk.CTkEntry(
|
|
2515
|
+
parent, placeholder_text=placeholder, show=show,
|
|
2516
|
+
fg_color=THEME["box_color"], border_color=THEME["box_border"],
|
|
2517
|
+
text_color=THEME["content_text"]
|
|
2518
|
+
)
|
|
2519
|
+
if default:
|
|
2520
|
+
entry.insert(0, default)
|
|
2521
|
+
entry.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(0, 2))
|
|
2522
|
+
return entry
|
|
2523
|
+
|
|
2524
|
+
def _file_row(self, parent, row, entry, save=False, dir_only=False, filetypes=(
|
|
2525
|
+
("All files", "*.*"),), multiple=False, selected=None, on_browse=None):
|
|
2526
|
+
entry.grid_configure(columnspan=1)
|
|
2527
|
+
|
|
2528
|
+
def browse():
|
|
2529
|
+
if dir_only:
|
|
2530
|
+
path = filedialog.askdirectory()
|
|
2531
|
+
elif multiple:
|
|
2532
|
+
paths = filedialog.askopenfilenames(filetypes=filetypes)
|
|
2533
|
+
if paths:
|
|
2534
|
+
entry.delete(0, "end")
|
|
2535
|
+
entry.insert(0, ", ".join(paths))
|
|
2536
|
+
if selected is not None:
|
|
2537
|
+
selected.clear()
|
|
2538
|
+
selected.extend(paths)
|
|
2539
|
+
if on_browse:
|
|
2540
|
+
on_browse()
|
|
2541
|
+
return
|
|
2542
|
+
elif save:
|
|
2543
|
+
path = filedialog.asksaveasfilename(filetypes=filetypes)
|
|
2544
|
+
else:
|
|
2545
|
+
path = filedialog.askopenfilename(filetypes=filetypes)
|
|
2546
|
+
if path:
|
|
2547
|
+
entry.delete(0, "end")
|
|
2548
|
+
entry.insert(0, path)
|
|
2549
|
+
if on_browse:
|
|
2550
|
+
on_browse()
|
|
2551
|
+
|
|
2552
|
+
button = self._ghost_button(parent, "Browse", browse, width=80)
|
|
2553
|
+
button.grid(row=row, column=1, sticky="e", padx=(8, 0), pady=(0, 2))
|
|
2554
|
+
return button
|
|
2555
|
+
|
|
2556
|
+
def _option(self, parent, row, values, default=None, command=None):
|
|
2557
|
+
var = ctk.StringVar(value=default or values[0])
|
|
2558
|
+
menu = ctk.CTkOptionMenu(
|
|
2559
|
+
parent,
|
|
2560
|
+
values=values,
|
|
2561
|
+
variable=var,
|
|
2562
|
+
fg_color=THEME["box_color"],
|
|
2563
|
+
button_color=ACCENT,
|
|
2564
|
+
button_hover_color=ACCENT_HOVER,
|
|
2565
|
+
text_color=THEME["content_text"],
|
|
2566
|
+
dropdown_fg_color=THEME["box_color"],
|
|
2567
|
+
dropdown_hover_color=ACCENT_TINT,
|
|
2568
|
+
command=command)
|
|
2569
|
+
menu.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(0, 2))
|
|
2570
|
+
return var, menu
|
|
2571
|
+
|
|
2572
|
+
def _option_grid(self, parent, row, values, label=None, default=None):
|
|
2573
|
+
if label:
|
|
2574
|
+
ctk.CTkLabel(
|
|
2575
|
+
parent,
|
|
2576
|
+
text=label,
|
|
2577
|
+
text_color=THEME["slight_gray"],
|
|
2578
|
+
font=self._font(11),
|
|
2579
|
+
anchor="w").grid(
|
|
2580
|
+
row=row,
|
|
2581
|
+
column=0,
|
|
2582
|
+
sticky="w",
|
|
2583
|
+
padx=(
|
|
2584
|
+
12,
|
|
2585
|
+
4),
|
|
2586
|
+
pady=4)
|
|
2587
|
+
var = ctk.StringVar(value=default or values[0])
|
|
2588
|
+
menu = ctk.CTkOptionMenu(
|
|
2589
|
+
parent,
|
|
2590
|
+
values=values,
|
|
2591
|
+
variable=var,
|
|
2592
|
+
fg_color=THEME["content_bg"],
|
|
2593
|
+
button_color=ACCENT,
|
|
2594
|
+
button_hover_color=ACCENT_HOVER,
|
|
2595
|
+
text_color=THEME["content_text"],
|
|
2596
|
+
dropdown_fg_color=THEME["content_bg"],
|
|
2597
|
+
dropdown_hover_color=ACCENT_TINT,
|
|
2598
|
+
width=170)
|
|
2599
|
+
col = 1 if label else 0
|
|
2600
|
+
span = 1 if label else 2
|
|
2601
|
+
menu.grid(
|
|
2602
|
+
row=row,
|
|
2603
|
+
column=col,
|
|
2604
|
+
columnspan=span,
|
|
2605
|
+
sticky="e",
|
|
2606
|
+
padx=(
|
|
2607
|
+
4,
|
|
2608
|
+
12),
|
|
2609
|
+
pady=4)
|
|
2610
|
+
return var, menu
|
|
2611
|
+
|
|
2612
|
+
def _entry_grid(self, parent, row, label, default="", mono=False):
|
|
2613
|
+
label_widget = ctk.CTkLabel(
|
|
2614
|
+
parent,
|
|
2615
|
+
text=label,
|
|
2616
|
+
text_color=THEME["slight_gray"],
|
|
2617
|
+
font=self._font(11),
|
|
2618
|
+
anchor="w")
|
|
2619
|
+
label_widget.grid(row=row, column=0, sticky="w", padx=(12, 4), pady=4)
|
|
2620
|
+
entry = ctk.CTkEntry(
|
|
2621
|
+
parent,
|
|
2622
|
+
fg_color=THEME["content_bg"],
|
|
2623
|
+
border_color=THEME["box_border"],
|
|
2624
|
+
text_color=THEME["content_text"],
|
|
2625
|
+
width=170,
|
|
2626
|
+
font=self._mono_font(11) if mono else self._font(11))
|
|
2627
|
+
if default:
|
|
2628
|
+
entry.insert(0, default)
|
|
2629
|
+
entry.grid(row=row, column=1, sticky="e", padx=(4, 12), pady=4)
|
|
2630
|
+
entry._label_widget = label_widget
|
|
2631
|
+
return entry
|
|
2632
|
+
|
|
2633
|
+
def _checkbox(self, parent, row, text, default=False):
|
|
2634
|
+
var = ctk.BooleanVar(value=default)
|
|
2635
|
+
ctk.CTkCheckBox(
|
|
2636
|
+
parent,
|
|
2637
|
+
text=text,
|
|
2638
|
+
variable=var,
|
|
2639
|
+
text_color=THEME["content_text"],
|
|
2640
|
+
fg_color=ACCENT,
|
|
2641
|
+
hover_color=ACCENT_HOVER,
|
|
2642
|
+
checkmark_color=ACCENT_ON).grid(
|
|
2643
|
+
row=row,
|
|
2644
|
+
column=0,
|
|
2645
|
+
columnspan=2,
|
|
2646
|
+
sticky="w",
|
|
2647
|
+
pady=4)
|
|
2648
|
+
return var
|
|
2649
|
+
|
|
2650
|
+
def _advanced(self, parent, row, build_fn):
|
|
2651
|
+
"""A collapsible "Advanced settings" section — this is where every
|
|
2652
|
+
CryptoConfig field lives. `build_fn(inner)` populates the (initially
|
|
2653
|
+
hidden) tinted inner panel with its own 2-column grid."""
|
|
2654
|
+
wrap = ctk.CTkFrame(parent, fg_color="transparent")
|
|
2655
|
+
wrap.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(8, 4))
|
|
2656
|
+
wrap.grid_columnconfigure(0, weight=1)
|
|
2657
|
+
|
|
2658
|
+
inner = ctk.CTkFrame(
|
|
2659
|
+
wrap,
|
|
2660
|
+
fg_color=ACCENT_TINT,
|
|
2661
|
+
border_width=1,
|
|
2662
|
+
border_color=ACCENT_BORDER,
|
|
2663
|
+
corner_radius=THEME["corner_rad"])
|
|
2664
|
+
inner.grid_columnconfigure(0, weight=1)
|
|
2665
|
+
inner.grid_columnconfigure(1, weight=1)
|
|
2666
|
+
|
|
2667
|
+
state = {"open": False}
|
|
2668
|
+
|
|
2669
|
+
def toggle():
|
|
2670
|
+
state["open"] = not state["open"]
|
|
2671
|
+
if state["open"]:
|
|
2672
|
+
inner.grid(row=1, column=0, sticky="ew", pady=(8, 0))
|
|
2673
|
+
btn.configure(
|
|
2674
|
+
text="▾ Advanced settings (all CryptoConfig fields)")
|
|
2675
|
+
else:
|
|
2676
|
+
inner.grid_forget()
|
|
2677
|
+
btn.configure(
|
|
2678
|
+
text="▸ Advanced settings (all CryptoConfig fields)")
|
|
2679
|
+
|
|
2680
|
+
btn = ctk.CTkButton(
|
|
2681
|
+
wrap,
|
|
2682
|
+
text="▸ Advanced settings (all CryptoConfig fields)",
|
|
2683
|
+
anchor="w",
|
|
2684
|
+
fg_color="transparent",
|
|
2685
|
+
hover_color=THEME["box_color"],
|
|
2686
|
+
text_color=ACCENT,
|
|
2687
|
+
font=self._font(
|
|
2688
|
+
12,
|
|
2689
|
+
"bold"),
|
|
2690
|
+
command=toggle)
|
|
2691
|
+
btn.grid(row=0, column=0, sticky="ew")
|
|
2692
|
+
|
|
2693
|
+
build_fn(inner)
|
|
2694
|
+
return inner
|
|
2695
|
+
|
|
2696
|
+
def _run_row(self, parent, row, text, command):
|
|
2697
|
+
bar = ctk.CTkProgressBar(
|
|
2698
|
+
parent,
|
|
2699
|
+
mode="indeterminate",
|
|
2700
|
+
progress_color=ACCENT)
|
|
2701
|
+
bar.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
|
2702
|
+
bar.grid_remove()
|
|
2703
|
+
|
|
2704
|
+
status = ctk.CTkLabel(
|
|
2705
|
+
parent,
|
|
2706
|
+
text="",
|
|
2707
|
+
text_color=THEME["slight_gray"],
|
|
2708
|
+
font=self._font(11),
|
|
2709
|
+
anchor="w")
|
|
2710
|
+
status.grid(row=row + 1, column=0, sticky="w", pady=(6, 0))
|
|
2711
|
+
|
|
2712
|
+
btn = self._primary_button(parent, text, command)
|
|
2713
|
+
btn.grid(row=row + 1, column=1, sticky="e", pady=(6, 0))
|
|
2714
|
+
return btn, bar, status
|
|
2715
|
+
|
|
2716
|
+
def _primary_button(self, parent, text, command=None, width=None):
|
|
2717
|
+
return ctk.CTkButton(
|
|
2718
|
+
parent, text=text, command=command, width=width or 140,
|
|
2719
|
+
fg_color=ACCENT, hover_color=ACCENT_HOVER, text_color=ACCENT_ON,
|
|
2720
|
+
font=self._font(13, "bold")
|
|
2721
|
+
)
|
|
2722
|
+
|
|
2723
|
+
def _ghost_button(self, parent, text, command=None, width=80):
|
|
2724
|
+
return ctk.CTkButton(
|
|
2725
|
+
parent, text=text, command=command, width=width,
|
|
2726
|
+
fg_color=THEME["box_color"], hover_color=ACCENT_TINT,
|
|
2727
|
+
border_width=1, border_color=THEME["box_border"],
|
|
2728
|
+
text_color=THEME["content_text"]
|
|
2729
|
+
)
|
|
2730
|
+
|
|
2731
|
+
# ------------------------------------------------------------------ #
|
|
2732
|
+
# Background work / error handling
|
|
2733
|
+
# ------------------------------------------------------------------ #
|
|
2734
|
+
|
|
2735
|
+
def _run_async(
|
|
2736
|
+
self,
|
|
2737
|
+
work,
|
|
2738
|
+
button,
|
|
2739
|
+
bar,
|
|
2740
|
+
status,
|
|
2741
|
+
on_success=None,
|
|
2742
|
+
start_msg="Working…"):
|
|
2743
|
+
"""Run `work()` on a background thread so the GUI stays responsive.
|
|
2744
|
+
`work` must be a zero-argument callable (use a lambda/closure to bind
|
|
2745
|
+
arguments). Any `CryptoError`/`GeneralError`/other exception raised
|
|
2746
|
+
is caught and shown in an error dialog; on success `on_success` is
|
|
2747
|
+
called (on the main thread) with the return value of `work`.
|
|
2748
|
+
"""
|
|
2749
|
+
button.configure(state="disabled")
|
|
2750
|
+
bar.grid()
|
|
2751
|
+
bar.start()
|
|
2752
|
+
status.configure(text=start_msg, text_color=THEME["slight_gray"])
|
|
2753
|
+
|
|
2754
|
+
def target():
|
|
2755
|
+
try:
|
|
2756
|
+
result = work()
|
|
2757
|
+
except Exception as e:
|
|
2758
|
+
# `e` is cleared once the except block ends, so capture it now
|
|
2759
|
+
message = str(e)
|
|
2760
|
+
self.after(
|
|
2761
|
+
0, lambda: self._async_fail(
|
|
2762
|
+
button, bar, status, message))
|
|
2763
|
+
return
|
|
2764
|
+
self.after(
|
|
2765
|
+
0,
|
|
2766
|
+
lambda: self._async_ok(
|
|
2767
|
+
button,
|
|
2768
|
+
bar,
|
|
2769
|
+
status,
|
|
2770
|
+
result,
|
|
2771
|
+
on_success))
|
|
2772
|
+
|
|
2773
|
+
threading.Thread(target=target, daemon=True).start()
|
|
2774
|
+
|
|
2775
|
+
def _async_fail(self, button, bar, status, message):
|
|
2776
|
+
bar.stop()
|
|
2777
|
+
bar.grid_remove()
|
|
2778
|
+
button.configure(state="normal")
|
|
2779
|
+
status.configure(text=f"Failed: {message}", text_color=DANGER)
|
|
2780
|
+
self._error_win("Operation Failed", message)
|
|
2781
|
+
|
|
2782
|
+
def _async_ok(self, button, bar, status, result, on_success):
|
|
2783
|
+
bar.stop()
|
|
2784
|
+
bar.grid_remove()
|
|
2785
|
+
button.configure(state="normal")
|
|
2786
|
+
status.configure(text="Done.", text_color=SUCCESS)
|
|
2787
|
+
if on_success:
|
|
2788
|
+
on_success(result)
|
|
2789
|
+
|
|
2790
|
+
def _error_win(self, title, content, _exit=False):
|
|
2791
|
+
msg = CTkMessagebox(
|
|
2792
|
+
self,
|
|
2793
|
+
icon="cancel",
|
|
2794
|
+
title=title,
|
|
2795
|
+
message=content,
|
|
2796
|
+
option_1="OK")
|
|
2797
|
+
if _exit and msg.get() is not None:
|
|
2798
|
+
sys.exit(1)
|
|
2799
|
+
|
|
2800
|
+
def _font(self, size=12, weight="normal", underline=False):
|
|
2801
|
+
return ctk.CTkFont(
|
|
2802
|
+
family=THEME["font"],
|
|
2803
|
+
size=size,
|
|
2804
|
+
weight=weight,
|
|
2805
|
+
underline=underline,
|
|
2806
|
+
)
|
|
2807
|
+
|
|
2808
|
+
def _mono_font(self, size=12, weight="normal"):
|
|
2809
|
+
return ctk.CTkFont(family=MONO_FONT, size=size, weight=weight)
|