626 lines
24 KiB
Python
626 lines
24 KiB
Python
|
|
"""
|
||
|
|
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("<Control-o>", lambda e: self._add_files())
|
||
|
|
self.bind("<Control-e>", lambda e: self._do_encrypt())
|
||
|
|
self.bind("<Control-d>", lambda e: self._do_decrypt())
|
||
|
|
self.bind("<Control-g>", lambda e: self._generate_password())
|
||
|
|
self.bind("<Delete>", 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("<<Drop>>", 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("<Button-3>", 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}")
|