""" CryptZ Ultimate v5 — GUI Module Full-featured interface with drag&drop, password strength, hotkeys, real progress, log panel, theme switching, context menu, etc. """ import os import threading import secrets import time import customtkinter as ctk from tkinter import filedialog, messagebox, Menu from typing import Optional import pyotp import qrcode from PIL import Image, ImageTk from crypto_engine import ( EncryptOptions, DecryptResult, encrypt, decrypt, verify_integrity, check_password_strength, generate_password, secure_delete, EXTENSION, ) # --- Theme setup --- ctk.set_appearance_mode("Dark") ctk.set_default_color_theme("blue") # Colors COLORS = { "strength_0": "#ff4444", "strength_1": "#ff8800", "strength_2": "#ffcc00", "strength_3": "#88cc00", "strength_4": "#00cc44", "sidebar_bg": "#1a1a2e", "accent": "#4361ee", "danger": "#ef233c", "success": "#06d6a0", } class CryptZApp(ctk.CTk): def __init__(self): super().__init__() self.title("CryptZ Ultimate v5 — AES-256-GCM Archiver") self.geometry("1100x800") self.minsize(900, 650) self.files_list: list[str] = [] self.hardware_key_path: Optional[str] = None self.is_busy = False self._setup_ui() self._setup_hotkeys() self._setup_dnd() # ===== UI SETUP ===== def _setup_ui(self): self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) self._build_sidebar() self._build_main_area() self._build_statusbar() def _build_sidebar(self): self.sidebar = ctk.CTkFrame(self, width=300, corner_radius=0) self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid_propagate(False) # Title ctk.CTkLabel(self.sidebar, text="⚙ ПАРАМЕТРЫ", font=("Segoe UI", 18, "bold")).pack(pady=(25, 20)) # Password pw_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent") pw_frame.pack(fill="x", padx=20, pady=(0, 5)) ctk.CTkLabel(pw_frame, text="Пароль:", font=("Segoe UI", 12)).pack(anchor="w") self.pw_entry_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent") self.pw_entry_frame.pack(fill="x", padx=20) self.password_var = ctk.StringVar() self.password_var.trace_add("write", self._on_password_change) self.pw_entry = ctk.CTkEntry( self.pw_entry_frame, textvariable=self.password_var, show="●", placeholder_text="Введите пароль...", height=36 ) self.pw_entry.pack(side="left", fill="x", expand=True) self.pw_show_btn = ctk.CTkButton( self.pw_entry_frame, text="👁", width=36, height=36, command=self._toggle_password_visibility, fg_color="#3d3d3d" ) self.pw_show_btn.pack(side="left", padx=(5, 0)) self.pw_gen_btn = ctk.CTkButton( self.pw_entry_frame, text="🎲", width=36, height=36, command=self._generate_password, fg_color="#3d3d3d" ) self.pw_gen_btn.pack(side="left", padx=(5, 0)) # Strength indicator self.strength_frame = ctk.CTkFrame(self.sidebar, height=6, fg_color="transparent") self.strength_frame.pack(fill="x", padx=20, pady=(4, 0)) self.strength_bar = ctk.CTkProgressBar(self.strength_frame, height=6) self.strength_bar.pack(fill="x") self.strength_bar.set(0) self.strength_label = ctk.CTkLabel(self.sidebar, text="", font=("Segoe UI", 10)) self.strength_label.pack(anchor="w", padx=20) # Checkboxes ctk.CTkLabel(self.sidebar, text="").pack() # spacer self.check_2fa = ctk.CTkCheckBox(self.sidebar, text="🔐 Использовать 2FA (TOTP)") self.check_2fa.pack(pady=6, anchor="w", padx=30) self.check_secure_del = ctk.CTkCheckBox(self.sidebar, text="🗑 Шредер (удалить оригиналы)") self.check_secure_del.pack(pady=6, anchor="w", padx=30) self.check_compress = ctk.CTkCheckBox(self.sidebar, text="📦 Сжатие (gzip)") self.check_compress.pack(pady=6, anchor="w", padx=30) self.check_compress.select() # File key ctk.CTkLabel(self.sidebar, text="").pack() self.hw_key_btn = ctk.CTkButton( self.sidebar, text="🔑 Файл-ключ: не выбран", command=self._load_hw_key, fg_color="#3d3d3d", hover_color="#555555" ) self.hw_key_btn.pack(fill="x", padx=20, pady=5) self.hw_key_clear_btn = ctk.CTkButton( self.sidebar, text="✕ Сбросить файл-ключ", command=self._clear_hw_key, fg_color="#555555", hover_color="#773333", height=28 ) self.hw_key_clear_btn.pack(fill="x", padx=20, pady=(0, 10)) # Max attempts attempts_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent") attempts_frame.pack(fill="x", padx=20, pady=5) ctk.CTkLabel(attempts_frame, text="Макс. попыток:", font=("Segoe UI", 12)).pack(side="left") self.attempts_slider = ctk.CTkSlider(attempts_frame, from_=1, to=20, number_of_steps=19, width=120) self.attempts_slider.set(5) self.attempts_slider.pack(side="left", padx=10) self.attempts_label = ctk.CTkLabel(attempts_frame, text="5") self.attempts_label.pack(side="left") self.attempts_slider.configure(command=self._on_attempts_change) # Theme switch ctk.CTkLabel(self.sidebar, text="").pack() theme_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent") theme_frame.pack(fill="x", padx=20, pady=10) ctk.CTkLabel(theme_frame, text="Тема:").pack(side="left") self.theme_menu = ctk.CTkOptionMenu( theme_frame, values=["Dark", "Light", "System"], command=self._change_theme, width=100 ) self.theme_menu.pack(side="right") def _build_main_area(self): main = ctk.CTkFrame(self) main.grid(row=0, column=1, sticky="nsew", padx=10, pady=10) main.grid_rowconfigure(1, weight=1) main.grid_columnconfigure(0, weight=1) # Toolbar toolbar = ctk.CTkFrame(main, height=50, fg_color="transparent") toolbar.grid(row=0, column=0, sticky="ew", pady=(0, 5)) self.btn_add_files = ctk.CTkButton(toolbar, text="📂 Файлы", command=self._add_files, width=100) self.btn_add_files.pack(side="left", padx=3) self.btn_add_folder = ctk.CTkButton(toolbar, text="📁 Папка", command=self._add_folder, width=100) self.btn_add_folder.pack(side="left", padx=3) self.btn_clear = ctk.CTkButton(toolbar, text="🧹 Очистить", command=self._clear_files, width=100, fg_color="#555") self.btn_clear.pack(side="left", padx=3) # Spacer ctk.CTkLabel(toolbar, text="").pack(side="left", fill="x", expand=True) self.btn_encrypt = ctk.CTkButton( toolbar, text="🔒 Зашифровать", command=self._do_encrypt, width=140, fg_color=COLORS["accent"], hover_color="#3451cc" ) self.btn_encrypt.pack(side="right", padx=3) self.btn_decrypt = ctk.CTkButton( toolbar, text="🔓 Расшифровать", command=self._do_decrypt, width=140, fg_color=COLORS["success"], hover_color="#04a87d" ) self.btn_decrypt.pack(side="right", padx=3) self.btn_verify = ctk.CTkButton( toolbar, text="✓ Проверить", command=self._do_verify, width=110, fg_color="#555" ) self.btn_verify.pack(side="right", padx=3) # File list self.file_frame = ctk.CTkFrame(main) self.file_frame.grid(row=1, column=0, sticky="nsew") self.drop_label = ctk.CTkLabel( self.file_frame, text="Перетащите файлы сюда\nили используйте кнопки выше", font=("Segoe UI", 16), text_color="#888888" ) self.drop_label.place(relx=0.5, rely=0.5, anchor="center") self.file_scroll = ctk.CTkScrollableFrame(self.file_frame) self.file_scroll.pack(fill="both", expand=True, padx=5, pady=5) # Progress progress_frame = ctk.CTkFrame(main, height=60, fg_color="transparent") progress_frame.grid(row=2, column=0, sticky="ew", pady=(5, 0)) self.progress_bar = ctk.CTkProgressBar(progress_frame, height=12) self.progress_bar.pack(fill="x", pady=(5, 2)) self.progress_bar.set(0) self.progress_label = ctk.CTkLabel(progress_frame, text="Готов к работе", font=("Segoe UI", 11)) self.progress_label.pack(anchor="w") # Log panel self.log_frame = ctk.CTkFrame(main, height=120) self.log_frame.grid(row=3, column=0, sticky="ew", pady=(5, 0)) ctk.CTkLabel(self.log_frame, text="📋 Журнал:", font=("Segoe UI", 11, "bold")).pack(anchor="w", padx=10, pady=(5, 0)) self.log_text = ctk.CTkTextbox(self.log_frame, height=80, font=("Consolas", 10)) self.log_text.pack(fill="both", expand=True, padx=5, pady=5) self.log_text.configure(state="disabled") def _build_statusbar(self): self.statusbar = ctk.CTkFrame(self, height=28, corner_radius=0) self.statusbar.grid(row=1, column=0, columnspan=2, sticky="ew") self.status_files = ctk.CTkLabel(self.statusbar, text="Файлов: 0", font=("Segoe UI", 10)) self.status_files.pack(side="left", padx=15) self.status_size = ctk.CTkLabel(self.statusbar, text="Размер: 0 B", font=("Segoe UI", 10)) self.status_size.pack(side="left", padx=15) self.status_op = ctk.CTkLabel(self.statusbar, text="", font=("Segoe UI", 10)) self.status_op.pack(side="right", padx=15) # ===== HOTKEYS ===== def _setup_hotkeys(self): self.bind("", lambda e: self._add_files()) self.bind("", lambda e: self._do_encrypt()) self.bind("", lambda e: self._do_decrypt()) self.bind("", lambda e: self._generate_password()) self.bind("", lambda e: self._clear_files()) # ===== DRAG & DROP ===== def _setup_dnd(self): """Try to setup tkinterdnd2. Silently skip if not available.""" try: from tkinterdnd2 import DND_FILES self.drop_target_register(DND_FILES) self.dnd_bind("<>", self._on_drop) except (ImportError, Exception): pass def _on_drop(self, event): """Handle drag & drop files.""" files = self.tk.splitlist(event.data) for f in files: if f not in self.files_list: self.files_list.append(f) self._refresh_file_list() # ===== FILE MANAGEMENT ===== def _add_files(self): paths = filedialog.askopenfilenames(title="Выбрать файлы") if paths: for p in paths: if p not in self.files_list: self.files_list.append(p) self._refresh_file_list() self._log(f"Добавлено файлов: {len(paths)}") def _add_folder(self): folder = filedialog.askdirectory(title="Выбрать папку") if folder: count = 0 for root, dirs, files in os.walk(folder): for fname in files: full = os.path.join(root, fname) if full not in self.files_list: self.files_list.append(full) count += 1 self._refresh_file_list() self._log(f"Добавлено из папки: {count} файлов") def _clear_files(self): self.files_list.clear() self._refresh_file_list() self._log("Список очищен") def _remove_file(self, path: str): if path in self.files_list: self.files_list.remove(path) self._refresh_file_list() def _refresh_file_list(self): # Clear scrollable frame for widget in self.file_scroll.winfo_children(): widget.destroy() if not self.files_list: self.drop_label.place(relx=0.5, rely=0.5, anchor="center") else: self.drop_label.place_forget() for filepath in self.files_list: row = ctk.CTkFrame(self.file_scroll, fg_color="transparent", height=30) row.pack(fill="x", pady=1) # Icon based on type ext = os.path.splitext(filepath)[1].lower() icon = "📄" if ext in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"): icon = "🖼" elif ext in (".mp3", ".wav", ".flac", ".ogg"): icon = "🎵" elif ext in (".mp4", ".avi", ".mkv", ".mov"): icon = "🎬" elif ext in (".zip", ".rar", ".7z", ".tar", ".gz"): icon = "📦" elif ext == EXTENSION: icon = "🔒" elif os.path.isdir(filepath): icon = "📁" name = os.path.basename(filepath) size = "" if os.path.isfile(filepath): s = os.path.getsize(filepath) size = self._format_size(s) label = ctk.CTkLabel(row, text=f"{icon} {name} ({size})", font=("Segoe UI", 11), anchor="w") label.pack(side="left", fill="x", expand=True, padx=5) del_btn = ctk.CTkButton( row, text="✕", width=28, height=28, fg_color="#553333", hover_color="#883333", command=lambda p=filepath: self._remove_file(p) ) del_btn.pack(side="right", padx=5) # Context menu on right-click label.bind("", lambda e, p=filepath: self._show_context_menu(e, p)) self._update_statusbar() def _show_context_menu(self, event, filepath): menu = Menu(self, tearoff=0) menu.add_command(label="Удалить из списка", command=lambda: self._remove_file(filepath)) menu.add_command(label="Открыть расположение", command=lambda: os.startfile(os.path.dirname(filepath))) menu.post(event.x_root, event.y_root) def _update_statusbar(self): count = len(self.files_list) total_size = sum(os.path.getsize(f) for f in self.files_list if os.path.isfile(f)) self.status_files.configure(text=f"Файлов: {count}") self.status_size.configure(text=f"Размер: {self._format_size(total_size)}") @staticmethod def _format_size(size_bytes: int) -> str: if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 ** 2: return f"{size_bytes / 1024:.1f} KB" elif size_bytes < 1024 ** 3: return f"{size_bytes / 1024**2:.1f} MB" else: return f"{size_bytes / 1024**3:.2f} GB" # ===== PASSWORD ===== def _on_password_change(self, *args): pw = self.password_var.get() if not pw: self.strength_bar.set(0) self.strength_label.configure(text="") return score, label = check_password_strength(pw) self.strength_bar.set(score / 4) color = COLORS[f"strength_{score}"] self.strength_bar.configure(progress_color=color) self.strength_label.configure(text=label, text_color=color) def _toggle_password_visibility(self): current = self.pw_entry.cget("show") self.pw_entry.configure(show="" if current == "●" else "●") self.pw_show_btn.configure(text="🙈" if current == "●" else "👁") def _generate_password(self): pw = generate_password(20) self.password_var.set(pw) self.pw_entry.configure(show="") self.pw_show_btn.configure(text="🙈") # Copy to clipboard self.clipboard_clear() self.clipboard_append(pw) self._log("Пароль сгенерирован и скопирован в буфер обмена") # ===== FILE KEY ===== def _load_hw_key(self): path = filedialog.askopenfilename(title="Выбрать файл-ключ") if path: self.hardware_key_path = path name = os.path.basename(path) self.hw_key_btn.configure(text=f"🔑 Ключ: {name[:20]}") self._log(f"Файл-ключ: {name}") def _clear_hw_key(self): self.hardware_key_path = None self.hw_key_btn.configure(text="🔑 Файл-ключ: не выбран") self._log("Файл-ключ сброшен") # ===== SIDEBAR CALLBACKS ===== def _on_attempts_change(self, value): self.attempts_label.configure(text=str(int(value))) def _change_theme(self, value): ctk.set_appearance_mode(value) # ===== OPERATIONS ===== def _do_encrypt(self): if self.is_busy: return if not self.files_list: messagebox.showwarning("CryptZ", "Добавьте файлы для шифрования!") return pw = self.password_var.get() if not pw: messagebox.showwarning("CryptZ", "Введите пароль!") return output_path = filedialog.asksaveasfilename( title="Сохранить зашифрованный архив", defaultextension=EXTENSION, filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] ) if not output_path: return # 2FA setup use_2fa = self.check_2fa.get() otp_secret = "NONE" if use_2fa: otp_secret = pyotp.random_base32() self._show_2fa_setup(otp_secret) options = EncryptOptions( password=pw, files=self.files_list.copy(), output_path=output_path, use_2fa=use_2fa, otp_secret=otp_secret, max_attempts=int(self.attempts_slider.get()), hardware_key_path=self.hardware_key_path, secure_delete=bool(self.check_secure_del.get()), compress=bool(self.check_compress.get()), progress_callback=self._progress_callback, ) self.is_busy = True self._log("Шифрование начато...") threading.Thread(target=self._encrypt_thread, args=(options,), daemon=True).start() def _encrypt_thread(self, options: EncryptOptions): try: result = encrypt(options) self.after(0, lambda: self._on_encrypt_done(result)) except Exception as e: self.after(0, lambda: self._on_error(str(e))) def _on_encrypt_done(self, path): self.is_busy = False self._log(f"✅ Архив создан: {os.path.basename(path)}") messagebox.showinfo("CryptZ", "Архив успешно создан!\n{}".format(path)) def _do_decrypt(self): if self.is_busy: return pw = self.password_var.get() if not pw: messagebox.showwarning("CryptZ", "Введите пароль!") return file_path = filedialog.askopenfilename( title="Выбрать зашифрованный архив", filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] ) if not file_path: return output_dir = filedialog.askdirectory(title="Папка для извлечения") if not output_dir: return self.is_busy = True self._log("Расшифровка начата...") threading.Thread( target=self._decrypt_thread, args=(file_path, pw, output_dir, None), daemon=True ).start() def _decrypt_thread(self, file_path, password, output_dir, otp_code): result = decrypt( file_path=file_path, password=password, output_dir=output_dir, hardware_key_path=self.hardware_key_path, otp_code=otp_code, progress_callback=self._progress_callback, ) self.after(0, lambda: self._on_decrypt_done(result, file_path, password, output_dir)) def _on_decrypt_done(self, result: DecryptResult, file_path, password, output_dir): self.is_busy = False if result.needs_2fa: self._ask_2fa_code(file_path, password, output_dir) return if result.success: self._log(f"✅ {result.message}") messagebox.showinfo("CryptZ", result.message) else: self._log(f"❌ {result.message}") messagebox.showerror("CryptZ", result.message) def _ask_2fa_code(self, file_path, password, output_dir): """Show 2FA input dialog.""" dialog = ctk.CTkInputDialog(text="Введите код 2FA (6 цифр):", title="Двухфакторная аутентификация") code = dialog.get_input() if code: self.is_busy = True threading.Thread( target=self._decrypt_thread, args=(file_path, password, output_dir, code), daemon=True ).start() def _do_verify(self): if self.is_busy: return pw = self.password_var.get() if not pw: messagebox.showwarning("CryptZ", "Введите пароль!") return file_path = filedialog.askopenfilename( title="Выбрать архив для проверки", filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] ) if not file_path: return ok, msg = verify_integrity(file_path, pw, self.hardware_key_path) if ok: self._log(f"✅ {msg}") messagebox.showinfo("CryptZ", msg) else: self._log(f"❌ {msg}") messagebox.showerror("CryptZ", msg) # ===== 2FA SETUP ===== def _show_2fa_setup(self, otp_secret: str): """Show QR code window for 2FA setup.""" win = ctk.CTkToplevel(self) win.title("Настройка 2FA") win.geometry("400x450") win.transient(self) win.grab_set() ctk.CTkLabel(win, text="Сканируйте QR-код в\nGoogle Authenticator / Authy", font=("Segoe UI", 14)).pack(pady=15) # Generate QR uri = pyotp.TOTP(otp_secret).provisioning_uri(name="CryptZ", issuer_name="CryptZ Ultimate") qr_img = qrcode.make(uri).resize((250, 250)) photo = ImageTk.PhotoImage(qr_img) qr_label = ctk.CTkLabel(win, image=photo, text="") qr_label.image = photo qr_label.pack(pady=10) ctk.CTkLabel(win, text=f"Секрет: {otp_secret}", font=("Consolas", 11)).pack(pady=5) ctk.CTkButton(win, text="Готово", command=win.destroy, width=120).pack(pady=15) # ===== PROGRESS & LOG ===== def _progress_callback(self, progress: float, message: str): self.after(0, lambda: self._update_progress(progress, message)) def _update_progress(self, progress: float, message: str): self.progress_bar.set(progress) self.progress_label.configure(text=message) self.status_op.configure(text=message) def _log(self, message: str): timestamp = time.strftime("%H:%M:%S") self.log_text.configure(state="normal") self.log_text.insert("end", "[{}] {}\n".format(timestamp, message)) self.log_text.see("end") self.log_text.configure(state="disabled") def _on_error(self, error_msg: str): self.is_busy = False self._log(f"❌ Ошибка: {error_msg}") messagebox.showerror("CryptZ", f"Ошибка: {error_msg}")