encrypted backups part one
This commit is contained in:
@@ -9,8 +9,11 @@ from pathlib import Path
|
||||
from crontab import CronTab
|
||||
import tempfile
|
||||
import stat
|
||||
import shutil
|
||||
|
||||
from pbp_app_config import AppConfig
|
||||
from pyimage_ui.password_dialog import PasswordDialog
|
||||
from core.encryption_manager import EncryptionManager
|
||||
|
||||
|
||||
class BackupManager:
|
||||
@@ -18,52 +21,15 @@ class BackupManager:
|
||||
Handles the logic for creating and managing backups using rsync.
|
||||
"""
|
||||
|
||||
def __init__(self, logger):
|
||||
def __init__(self, logger, app=None):
|
||||
self.logger = logger
|
||||
self.process = None
|
||||
self.app_tag = "# Py-Backup Job"
|
||||
self.is_system_process = False
|
||||
self.app = app
|
||||
self.encryption_manager = EncryptionManager(logger, app)
|
||||
|
||||
def _execute_as_root(self, script_content: str) -> bool:
|
||||
"""Executes a shell script with root privileges using pkexec."""
|
||||
script_path = ''
|
||||
try:
|
||||
# Use tempfile for secure temporary file creation
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="pybackup_script_", suffix=".sh", dir="/tmp") as tmp_script:
|
||||
tmp_script.write("#!/bin/bash\n\n")
|
||||
tmp_script.write("set -e\n\n") # Exit on error
|
||||
tmp_script.write(script_content)
|
||||
script_path = tmp_script.name
|
||||
|
||||
# Make the script executable
|
||||
os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP |
|
||||
stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
command = ['pkexec', script_path]
|
||||
|
||||
self.logger.log(
|
||||
f"Executing privileged command via script: {script_path}")
|
||||
self.logger.log(
|
||||
f"Script content:\n---\n{script_content}\n---")
|
||||
|
||||
result = subprocess.run(
|
||||
command, capture_output=True, text=True, check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.log(
|
||||
f"Privileged script executed successfully. Output:\n{result.stdout}")
|
||||
return True
|
||||
else:
|
||||
self.logger.log(
|
||||
f"Privileged script failed. Return code: {result.returncode}\nStderr: {result.stderr}\nStdout: {result.stdout}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Failed to set up or execute privileged command: {e}")
|
||||
return False
|
||||
finally:
|
||||
if script_path and os.path.exists(script_path):
|
||||
os.remove(script_path)
|
||||
|
||||
|
||||
def cancel_and_delete_privileged_backup(self, delete_path: str):
|
||||
"""Cancels a running system backup and deletes the target directory in one atomic pkexec call."""
|
||||
@@ -162,10 +128,10 @@ rm -f '{info_file}'
|
||||
else:
|
||||
self.logger.log("No active backup process to cancel.")
|
||||
|
||||
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, mode: str = "incremental"):
|
||||
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental"):
|
||||
"""Starts a generic backup process for a specific path, reporting to a queue."""
|
||||
thread = threading.Thread(target=self._run_backup_path, args=(
|
||||
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, mode))
|
||||
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
@@ -241,9 +207,23 @@ set -e
|
||||
self.logger.log(f"An unexpected error occurred during local compression/cleanup: {e}")
|
||||
return False
|
||||
|
||||
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, mode: str):
|
||||
|
||||
|
||||
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str):
|
||||
try:
|
||||
self.is_system_process = is_system
|
||||
if is_encrypted:
|
||||
# For encrypted backups, the dest_path is the container file.
|
||||
container_path = dest_path + ".luks"
|
||||
# Estimate container size to be 110% of source size
|
||||
size_gb = int(source_size / (1024**3) * 1.1) + 1
|
||||
mount_point = self.encryption_manager.setup_encrypted_backup(queue, container_path, size_gb)
|
||||
if not mount_point:
|
||||
return # Error or cancellation already handled in setup method
|
||||
|
||||
# The actual destination for rsync is the mount point
|
||||
rsync_dest = mount_point
|
||||
else:
|
||||
rsync_dest = dest_path
|
||||
self.logger.log(
|
||||
f"Starting backup from '{source_path}' to '{dest_path}'...")
|
||||
|
||||
@@ -280,7 +260,7 @@ set -e
|
||||
if is_dry_run:
|
||||
command.append('--dry-run')
|
||||
|
||||
command.extend([source_path, dest_path])
|
||||
command.extend([source_path, rsync_dest])
|
||||
self.logger.log(f"Rsync command: {' '.join(command)}")
|
||||
|
||||
transferred_size, total_size = self._execute_rsync(queue, command)
|
||||
@@ -341,6 +321,8 @@ set -e
|
||||
self.logger.log(
|
||||
f"Backup to '{dest_path}' completed.")
|
||||
finally:
|
||||
if is_encrypted and 'mount_point' in locals() and mount_point:
|
||||
self.encryption_manager.cleanup_encrypted_backup(f"pybackup_{os.path.basename(dest_path + '.luks')}", mount_point)
|
||||
self.process = None
|
||||
|
||||
def _create_info_file(self, dest_path: str, filename: str, source_size: int):
|
||||
@@ -633,7 +615,7 @@ set -e
|
||||
|
||||
# Regex to parse folder names like '6-März-2024_143000_system_full' or '6-März-2024_143000_system_full.tar.gz'
|
||||
name_regex = re.compile(
|
||||
r"^(\d{1,2}-\w+-\d{4})_(\d{6})_system_(full|incremental)(\.tar\.gz)?$", re.IGNORECASE)
|
||||
r"^(\d{1,2}-\w+-\d{4})_(\d{6})_system_(full|incremental)(\.tar\.gz|\.luks)?$", re.IGNORECASE)
|
||||
|
||||
for item in os.listdir(pybackup_path):
|
||||
# Skip info files
|
||||
@@ -648,11 +630,15 @@ set -e
|
||||
date_str = match.group(1)
|
||||
# time_str = match.group(2) # Not currently used in UI, but available
|
||||
backup_type_base = match.group(3).capitalize()
|
||||
is_compressed = match.group(4) is not None
|
||||
extension = match.group(4)
|
||||
is_compressed = (extension == ".tar.gz")
|
||||
is_encrypted = (extension == ".luks")
|
||||
|
||||
backup_type = backup_type_base
|
||||
if is_compressed:
|
||||
backup_type += " (Compressed)"
|
||||
elif is_encrypted:
|
||||
backup_type += " (Encrypted)"
|
||||
|
||||
backup_size = "N/A"
|
||||
comment = ""
|
||||
@@ -682,7 +668,8 @@ set -e
|
||||
"folder_name": item,
|
||||
"full_path": full_path,
|
||||
"comment": comment,
|
||||
"is_compressed": is_compressed
|
||||
"is_compressed": is_compressed,
|
||||
"is_encrypted": is_encrypted
|
||||
})
|
||||
|
||||
# Sort by parsing the date from the folder name
|
||||
|
||||
179
core/encryption_manager.py
Normal file
179
core/encryption_manager.py
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
import keyring
|
||||
import keyring.errors
|
||||
from keyring.backends import SecretService
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import stat
|
||||
from typing import Optional
|
||||
|
||||
from pyimage_ui.password_dialog import PasswordDialog
|
||||
|
||||
class EncryptionManager:
|
||||
def __init__(self, logger, app=None):
|
||||
self.logger = logger
|
||||
self.app = app
|
||||
self.service_id = "py-backup-encryption"
|
||||
self.session_password = None
|
||||
self.save_to_keyring = False
|
||||
|
||||
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||
try:
|
||||
return keyring.get_password(self.service_id, username)
|
||||
except keyring.errors.InitError as e:
|
||||
self.logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.log(f"Could not get password from keyring: {e}")
|
||||
return None
|
||||
|
||||
def set_password_in_keyring(self, username: str, password: str):
|
||||
try:
|
||||
keyring.set_password(self.service_id, username, password)
|
||||
self.logger.log(f"Password for {username} stored in keyring.")
|
||||
except Exception as e:
|
||||
self.logger.log(f"Could not set password in keyring: {e}")
|
||||
|
||||
def delete_password_from_keyring(self, username: str):
|
||||
try:
|
||||
keyring.delete_password(self.service_id, username)
|
||||
self.logger.log(f"Password for {username} deleted from keyring.")
|
||||
except Exception as e:
|
||||
self.logger.log(f"Could not delete password from keyring: {e}")
|
||||
|
||||
def set_session_password(self, password: str, save_to_keyring: bool):
|
||||
self.session_password = password
|
||||
self.save_to_keyring = save_to_keyring
|
||||
|
||||
def clear_session_password(self):
|
||||
self.session_password = None
|
||||
self.save_to_keyring = False
|
||||
|
||||
def get_password(self, username: str, confirm: bool = True) -> Optional[str]:
|
||||
if self.session_password:
|
||||
if self.save_to_keyring:
|
||||
self.set_password_in_keyring(username, self.session_password)
|
||||
return self.session_password
|
||||
|
||||
password = self.get_password_from_keyring(username)
|
||||
if password:
|
||||
return password
|
||||
|
||||
# If not in keyring, prompt the user
|
||||
dialog = PasswordDialog(self.app, title=f"Enter password for {username}", confirm=confirm)
|
||||
password = dialog.get_password()
|
||||
if password:
|
||||
# Ask to save the password
|
||||
# For now, we don't save it automatically. This will be a UI option.
|
||||
pass
|
||||
return password
|
||||
|
||||
def setup_encrypted_backup(self, queue, container_path: str, size_gb: int) -> Optional[str]:
|
||||
"""Sets up a LUKS encrypted container for the backup."""
|
||||
self.logger.log(f"Setting up encrypted container at {container_path}")
|
||||
|
||||
if not shutil.which("cryptsetup"):
|
||||
self.logger.log("Error: cryptsetup is not installed.")
|
||||
queue.put(('error', "cryptsetup is not installed."))
|
||||
return None
|
||||
|
||||
mapper_name = f"pybackup_{os.path.basename(container_path)}"
|
||||
mount_point = f"/mnt/{mapper_name}"
|
||||
|
||||
if os.path.exists(container_path):
|
||||
# Container exists, try to open it
|
||||
self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.")
|
||||
password = self.get_password(os.path.basename(container_path), confirm=False)
|
||||
if not password:
|
||||
self.logger.log("User cancelled password entry.")
|
||||
queue.put(('completion', {'status': 'cancelled', 'returncode': -1}))
|
||||
return None
|
||||
|
||||
script = f"""
|
||||
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
|
||||
mkdir -p {mount_point}
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Failed to unlock existing encrypted container.")
|
||||
queue.put(('error', "Failed to unlock existing encrypted container."))
|
||||
return None
|
||||
else:
|
||||
# Container does not exist, create it
|
||||
password = self.get_password(os.path.basename(container_path), confirm=True)
|
||||
if not password:
|
||||
self.logger.log("User cancelled password entry.")
|
||||
queue.put(('completion', {'status': 'cancelled', 'returncode': -1}))
|
||||
return None
|
||||
|
||||
script = f"""
|
||||
fallocate -l {size_gb}G {container_path}
|
||||
echo -n '{password}' | cryptsetup luksFormat {container_path} -
|
||||
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
|
||||
mkfs.ext4 /dev/mapper/{mapper_name}
|
||||
mkdir -p {mount_point}
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
"""
|
||||
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Failed to setup encrypted container.")
|
||||
self._cleanup_encrypted_backup(mapper_name, mount_point)
|
||||
queue.put(('error', "Failed to setup encrypted container."))
|
||||
return None
|
||||
|
||||
self.logger.log(f"Encrypted container is ready and mounted at {mount_point}")
|
||||
return mount_point
|
||||
|
||||
def cleanup_encrypted_backup(self, mapper_name: str, mount_point: str):
|
||||
"""Unmounts and closes the LUKS container."""
|
||||
self.logger.log(f"Cleaning up encrypted backup: {mapper_name}")
|
||||
script = f"""
|
||||
umount {mount_point} || echo "Mount point not found or already unmounted."
|
||||
cryptsetup luksClose {mapper_name} || echo "Mapper not found or already closed."
|
||||
rmdir {mount_point} || echo "Mount point directory not found."
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Encrypted backup cleanup script failed.")
|
||||
|
||||
def _execute_as_root(self, script_content: str) -> bool:
|
||||
"""Executes a shell script with root privileges using pkexec."""
|
||||
script_path = ''
|
||||
try:
|
||||
# Use tempfile for secure temporary file creation
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="pybackup_script_", suffix=".sh", dir="/tmp") as tmp_script:
|
||||
tmp_script.write("#!/bin/bash\n\n")
|
||||
tmp_script.write("set -e\n\n") # Exit on error
|
||||
tmp_script.write(script_content)
|
||||
script_path = tmp_script.name
|
||||
|
||||
# Make the script executable
|
||||
os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP |
|
||||
stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
command = ['pkexec', script_path]
|
||||
|
||||
self.logger.log(
|
||||
f"Executing privileged command via script: {script_path}")
|
||||
self.logger.log(
|
||||
f"Script content:\n---\n{script_content}\n---")
|
||||
|
||||
result = subprocess.run(
|
||||
command, capture_output=True, text=True, check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.log(
|
||||
f"Privileged script executed successfully. Output:\n{result.stdout}")
|
||||
return True
|
||||
else:
|
||||
self.logger.log(
|
||||
f"Privileged script failed. Return code: {result.returncode}\nStderr: {result.stderr}\nStdout: {result.stdout}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Failed to set up or execute privileged command: {e}")
|
||||
return False
|
||||
finally:
|
||||
if script_path and os.path.exists(script_path):
|
||||
os.remove(script_path)
|
||||
17
main_app.py
17
main_app.py
@@ -16,6 +16,7 @@ from pyimage_ui.scheduler_frame import SchedulerFrame
|
||||
from pyimage_ui.backup_content_frame import BackupContentFrame
|
||||
from pyimage_ui.header_frame import HeaderFrame
|
||||
from pyimage_ui.settings_frame import SettingsFrame
|
||||
from pyimage_ui.encryption_frame import EncryptionFrame
|
||||
from core.data_processing import DataProcessing
|
||||
from pyimage_ui.drawing import Drawing
|
||||
from pyimage_ui.navigation import Navigation
|
||||
@@ -69,7 +70,7 @@ class MainApplication(tk.Tk):
|
||||
self.content_frame.grid_rowconfigure(6, weight=0)
|
||||
self.content_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.backup_manager = BackupManager(app_logger)
|
||||
self.backup_manager = BackupManager(app_logger, self)
|
||||
self.queue = Queue()
|
||||
self.image_manager = IconManager()
|
||||
|
||||
@@ -79,6 +80,7 @@ class MainApplication(tk.Tk):
|
||||
self.navigation = Navigation(self)
|
||||
self.actions = Actions(self)
|
||||
|
||||
self.mode = "backup" # Default mode
|
||||
self.backup_is_running = False
|
||||
self.start_time = None
|
||||
|
||||
@@ -142,6 +144,10 @@ class MainApplication(tk.Tk):
|
||||
self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton")
|
||||
self.settings_button.pack(fill=tk.X, pady=10)
|
||||
|
||||
self.encryption_button = ttk.Button(
|
||||
self.sidebar_buttons_frame, text="Encryption", command=lambda: self.navigation.toggle_encryption_frame(5), style="Sidebar.TButton")
|
||||
self.encryption_button.pack(fill=tk.X, pady=10)
|
||||
|
||||
self.header_frame = HeaderFrame(self.content_frame, self.image_manager)
|
||||
self.header_frame.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
@@ -216,6 +222,7 @@ class MainApplication(tk.Tk):
|
||||
self._setup_scheduler_frame()
|
||||
self._setup_settings_frame()
|
||||
self._setup_backup_content_frame()
|
||||
self._setup_encryption_frame()
|
||||
|
||||
self._setup_task_bar()
|
||||
|
||||
@@ -400,6 +407,12 @@ class MainApplication(tk.Tk):
|
||||
self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
|
||||
self.backup_content_frame.grid_remove()
|
||||
|
||||
def _setup_encryption_frame(self):
|
||||
self.encryption_frame = EncryptionFrame(
|
||||
self.content_frame, self.backup_manager.encryption_manager, padding=10)
|
||||
self.encryption_frame.grid(row=2, column=0, sticky="nsew")
|
||||
self.encryption_frame.grid_remove()
|
||||
|
||||
def _setup_task_bar(self):
|
||||
# Define all boolean vars at the top to ensure they exist before use.
|
||||
self.vollbackup_var = tk.BooleanVar()
|
||||
@@ -459,7 +472,7 @@ class MainApplication(tk.Tk):
|
||||
variable=self.compressed_var, command=self.actions.handle_compression_change)
|
||||
self.compressed_cb.pack(side=tk.LEFT, padx=5)
|
||||
self.encrypted_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["encrypted"],
|
||||
variable=self.encrypted_var)
|
||||
variable=self.encrypted_var, command=self.actions.handle_encryption_change)
|
||||
self.encrypted_cb.pack(side=tk.LEFT, padx=5)
|
||||
self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"],
|
||||
variable=self.testlauf_var)
|
||||
|
||||
@@ -88,6 +88,23 @@ class Actions:
|
||||
# The state of the incremental checkbox depends on whether a full backup exists
|
||||
self._update_backup_type_controls()
|
||||
|
||||
def handle_encryption_change(self):
|
||||
if self.app.encrypted_var.get():
|
||||
# Encryption is enabled, force full backup and disable compression
|
||||
self.app.vollbackup_var.set(True)
|
||||
self.app.inkrementell_var.set(False)
|
||||
self.app.full_backup_cb.config(state="disabled")
|
||||
self.app.incremental_cb.config(state="disabled")
|
||||
self.app.compressed_var.set(False)
|
||||
self.app.compressed_cb.config(state="disabled")
|
||||
self.app.accurate_size_cb.config(state="disabled")
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
else:
|
||||
# Encryption is disabled, restore normal logic
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
self.app.compressed_cb.config(state="normal")
|
||||
self._update_backup_type_controls()
|
||||
|
||||
def on_toggle_accurate_size_calc(self):
|
||||
# This method is called when the user clicks the "Genaue inkrem. Größe" checkbox.
|
||||
if not self.app.genaue_berechnung_var.get():
|
||||
@@ -673,6 +690,7 @@ class Actions:
|
||||
|
||||
is_dry_run = self.app.testlauf_var.get()
|
||||
is_compressed = self.app.compressed_var.get()
|
||||
is_encrypted = self.app.encrypted_var.get()
|
||||
|
||||
self.app.backup_manager.start_backup(
|
||||
queue=self.app.queue,
|
||||
@@ -683,6 +701,7 @@ class Actions:
|
||||
exclude_files=exclude_file_paths,
|
||||
source_size=source_size_bytes,
|
||||
is_compressed=is_compressed,
|
||||
is_encrypted=is_encrypted,
|
||||
mode=mode)
|
||||
|
||||
def _start_user_backup(self, sources):
|
||||
|
||||
75
pyimage_ui/encryption_frame.py
Normal file
75
pyimage_ui/encryption_frame.py
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from shared_libs.message import MessageDialog
|
||||
import keyring
|
||||
|
||||
|
||||
class EncryptionFrame(ttk.Frame):
|
||||
def __init__(self, parent, encryption_manager, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
self.encryption_manager = encryption_manager
|
||||
|
||||
self.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(self, text="Encryption Settings", font=("Ubuntu", 16, "bold")).grid(
|
||||
row=0, column=0, pady=10, sticky="w")
|
||||
|
||||
# Keyring status
|
||||
self.keyring_status_label = ttk.Label(self, text="")
|
||||
self.keyring_status_label.grid(
|
||||
row=1, column=0, sticky="ew", padx=10, pady=5)
|
||||
self.check_keyring_availability()
|
||||
|
||||
# Password section
|
||||
password_frame = ttk.LabelFrame(self, text="Password Management")
|
||||
password_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=10)
|
||||
password_frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(password_frame, text="Password:").grid(
|
||||
row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.password_entry = ttk.Entry(password_frame, show="*")
|
||||
self.password_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||
|
||||
self.save_to_keyring_var = tk.BooleanVar()
|
||||
self.save_to_keyring_cb = ttk.Checkbutton(
|
||||
password_frame, text="Save password to system keyring", variable=self.save_to_keyring_var)
|
||||
self.save_to_keyring_cb.grid(
|
||||
row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
|
||||
|
||||
set_password_button = ttk.Button(
|
||||
password_frame, text="Set Session Password", command=self.set_session_password)
|
||||
set_password_button.grid(row=2, column=0, padx=5, pady=5)
|
||||
|
||||
clear_password_button = ttk.Button(
|
||||
password_frame, text="Clear Session Password", command=self.clear_session_password)
|
||||
clear_password_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
self.status_message_label = ttk.Label(self, text="", foreground="blue")
|
||||
self.status_message_label.grid(row=3, column=0, sticky="ew", padx=10, pady=5)
|
||||
|
||||
def check_keyring_availability(self):
|
||||
try:
|
||||
kr = keyring.get_keyring()
|
||||
if kr is None:
|
||||
self.keyring_status_label.config(
|
||||
text="No system keyring found. Passwords will not be saved.", foreground="orange")
|
||||
self.save_to_keyring_cb.config(state="disabled")
|
||||
except keyring.errors.NoKeyringError:
|
||||
self.keyring_status_label.config(
|
||||
text="No system keyring found. Passwords will not be saved.", foreground="orange")
|
||||
self.save_to_keyring_cb.config(state="disabled")
|
||||
|
||||
def set_session_password(self):
|
||||
password = self.password_entry.get()
|
||||
if not password:
|
||||
self.status_message_label.config(text="Password cannot be empty.", foreground="red")
|
||||
return
|
||||
|
||||
self.encryption_manager.set_session_password(password, self.save_to_keyring_var.get())
|
||||
self.status_message_label.config(text="Password set for this session.", foreground="green")
|
||||
|
||||
def clear_session_password(self):
|
||||
self.encryption_manager.clear_session_password()
|
||||
self.password_entry.delete(0, tk.END)
|
||||
self.status_message_label.config(text="Session password cleared.", foreground="green")
|
||||
@@ -142,6 +142,7 @@ class Navigation:
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
|
||||
# Show the main content frames
|
||||
self.app.canvas_frame.grid()
|
||||
@@ -186,6 +187,7 @@ class Navigation:
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
self.app.canvas_frame.grid()
|
||||
self.app.source_size_frame.grid()
|
||||
self.app.target_size_frame.grid()
|
||||
@@ -240,6 +242,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
self.app.source_size_frame.grid_remove()
|
||||
self.app.target_size_frame.grid_remove()
|
||||
self.app.restore_size_frame_before.grid_remove()
|
||||
@@ -257,6 +260,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
self.app.source_size_frame.grid_remove()
|
||||
self.app.target_size_frame.grid_remove()
|
||||
self.app.restore_size_frame_before.grid_remove()
|
||||
@@ -280,6 +284,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
self.app.source_size_frame.grid_remove()
|
||||
self.app.target_size_frame.grid_remove()
|
||||
self.app.restore_size_frame_before.grid_remove()
|
||||
@@ -287,3 +292,20 @@ class Navigation:
|
||||
self.app.backup_content_frame.show(self.app.destination_path)
|
||||
self.app.top_bar.grid()
|
||||
self._update_task_bar_visibility("scheduler")
|
||||
|
||||
def toggle_encryption_frame(self, active_index=None):
|
||||
self._cancel_calculation()
|
||||
self.app.drawing.update_nav_buttons(-1)
|
||||
|
||||
self.app.canvas_frame.grid_remove()
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.source_size_frame.grid_remove()
|
||||
self.app.target_size_frame.grid_remove()
|
||||
self.app.restore_size_frame_before.grid_remove()
|
||||
self.app.restore_size_frame_after.grid_remove()
|
||||
self.app.encryption_frame.grid()
|
||||
self.app.top_bar.grid()
|
||||
self._update_task_bar_visibility("settings")
|
||||
|
||||
60
pyimage_ui/password_dialog.py
Normal file
60
pyimage_ui/password_dialog.py
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
class PasswordDialog(tk.Toplevel):
|
||||
def __init__(self, parent, title="Password Required", confirm=True):
|
||||
super().__init__(parent)
|
||||
self.title(title)
|
||||
self.parent = parent
|
||||
self.password = None
|
||||
self.confirm = confirm
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
ttk.Label(self, text="Please enter the password for the encrypted backup:").pack(padx=20, pady=10)
|
||||
self.password_entry = ttk.Entry(self, show="*")
|
||||
self.password_entry.pack(padx=20, pady=5, fill="x", expand=True)
|
||||
self.password_entry.focus_set()
|
||||
|
||||
if self.confirm:
|
||||
ttk.Label(self, text="Confirm password:").pack(padx=20, pady=10)
|
||||
self.confirm_entry = ttk.Entry(self, show="*")
|
||||
self.confirm_entry.pack(padx=20, pady=5, fill="x", expand=True)
|
||||
|
||||
button_frame = ttk.Frame(self)
|
||||
button_frame.pack(pady=10)
|
||||
|
||||
ok_button = ttk.Button(button_frame, text="OK", command=self.on_ok)
|
||||
ok_button.pack(side="left", padx=5)
|
||||
cancel_button = ttk.Button(button_frame, text="Cancel", command=self.on_cancel)
|
||||
cancel_button.pack(side="left", padx=5)
|
||||
|
||||
self.bind("<Return>", lambda event: self.on_ok())
|
||||
self.bind("<Escape>", lambda event: self.on_cancel())
|
||||
|
||||
self.wait_window(self)
|
||||
|
||||
def on_ok(self):
|
||||
password = self.password_entry.get()
|
||||
|
||||
if not password:
|
||||
messagebox.showerror("Error", "Password cannot be empty.", parent=self)
|
||||
return
|
||||
|
||||
if self.confirm:
|
||||
confirm = self.confirm_entry.get()
|
||||
if password != confirm:
|
||||
messagebox.showerror("Error", "Passwords do not match.", parent=self)
|
||||
return
|
||||
|
||||
self.password = password
|
||||
self.destroy()
|
||||
|
||||
def on_cancel(self):
|
||||
self.password = None
|
||||
self.destroy()
|
||||
|
||||
def get_password(self):
|
||||
return self.password
|
||||
Reference in New Issue
Block a user