add checkboxes to keyfileframe

This commit is contained in:
2025-09-15 11:27:17 +02:00
parent 9406d3f0e2
commit 3a59bccddc
2 changed files with 162 additions and 74 deletions

View File

@@ -94,33 +94,62 @@ class EncryptionManager:
return os.path.join(pybackup_dir, container_filename)
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
pybackup_dir = os.path.join(base_dest_path, "pybackup")
# base_dest_path is ignored for security. Keyfiles are stored centrally.
secure_key_dir = "/usr/local/etc/luks"
key_filename = f"luks_{profile_name}.keyfile"
return os.path.join(pybackup_dir, key_filename)
return os.path.join(secure_key_dir, key_filename)
def create_and_add_key_file(self, base_dest_path: str, profile_name: str, password: str) -> Optional[str]:
# Get the new secure path for the keyfile
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
secure_key_dir = os.path.dirname(key_file_path)
# Container path remains the same, on the destination drive
container_path = self.get_container_path(base_dest_path, profile_name)
# This script will be executed as root. It creates the secure directory,
# generates the keyfile, sets its permissions, and adds it to the LUKS container.
# Using shlex.quote to be safe with paths
script = f"""
mkdir -p {shlex.quote(secure_key_dir)} && \
dd if=/dev/urandom of={shlex.quote(key_file_path)} bs=1 count=4096 && \
chmod 0400 {shlex.quote(key_file_path)} && \
cryptsetup luksAddKey {shlex.quote(container_path)} {shlex.quote(key_file_path)}
"""
if self._execute_as_root(script, password):
self.logger.log(
f"Successfully created keyfile at {key_file_path} and added to LUKS container.")
return key_file_path
else:
self.logger.log(
"Failed to create and add key file. The script might have failed midway. Attempting cleanup.")
# Attempt to clean up the keyfile if the script failed, as we can't be sure at which stage it failed.
cleanup_script = f"rm -f {shlex.quote(key_file_path)}"
self._execute_as_root(cleanup_script)
return None
def delete_key_file(self, base_dest_path: str, profile_name: str, password: str) -> bool:
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
container_path = self.get_container_path(base_dest_path, profile_name)
try:
with open(key_file_path, 'wb') as f:
f.write(os.urandom(4096))
os.chmod(key_file_path, 0o400)
self.logger.log(f"Generated new key file at {key_file_path}")
script = f'cryptsetup luksAddKey "{container_path}" "{key_file_path}"'
if self._execute_as_root(script, password):
self.logger.log(
"Successfully added key file to LUKS container.")
return key_file_path
else:
self.logger.log("Failed to add key file to LUKS container.")
os.remove(key_file_path)
return None
except Exception as e:
self.logger.log(f"Error creating key file: {e}")
if os.path.exists(key_file_path):
os.remove(key_file_path)
return None
if not os.path.exists(key_file_path):
self.logger.log(f"Keyfile for profile {profile_name} does not exist. Nothing to delete.")
return True # Considered a success as the desired state is "deleted"
# This script removes the key from the LUKS container and then deletes the keyfile.
# It requires a valid password for the container to authorize the key removal.
script = f"""
cryptsetup luksRemoveKey {shlex.quote(container_path)} {shlex.quote(key_file_path)} && \
rm -f {shlex.quote(key_file_path)}
"""
if self._execute_as_root(script, password):
self.logger.log(f"Successfully removed keyfile for profile {profile_name}.")
return True
else:
self.logger.log(f"Failed to remove keyfile for profile {profile_name}.")
return False
def _get_password_and_keyfile(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[Optional[str], Optional[str]]:
"""Gets the password (from cache, keyring, or user) or the keyfile path."""

View File

@@ -1,6 +1,7 @@
import tkinter as tk
from tkinter import ttk
import os
import glob
from pathlib import Path
from core.pbp_app_config import AppConfig, Msg
@@ -17,6 +18,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager = config_manager
self.app_instance = app_instance
self.current_view_index = 0
self.keyfile_profile_widgets = {}
nav_frame = ttk.Frame(self)
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
@@ -169,39 +171,16 @@ class AdvancedSettingsFrame(ttk.Frame):
# --- Keyfile Settings Frame ---
self.keyfile_settings_frame = ttk.LabelFrame(
view_container, text=Msg.STR["automation_settings_title"], padding=10)
keyfile_info_label = ttk.Label(
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
keyfile_info_label.grid(
row=0, column=0, columnspan=3, sticky="w", padx=5, pady=(5, 15))
keyfile_info_label.pack(fill=tk.X, pady=(5, 15))
# Display for current destination
dest_frame = ttk.Frame(self.keyfile_settings_frame)
dest_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
dest_frame.columnconfigure(1, weight=1)
ttk.Label(dest_frame, text=f"{Msg.STR['current_destination']}:", font=("Ubuntu", 11, "bold")).grid(row=0, column=0, sticky="w")
self.keyfile_dest_path_label = ttk.Label(dest_frame, text=Msg.STR["no_destination_selected"], foreground="gray")
self.keyfile_dest_path_label.grid(row=0, column=1, sticky="w", padx=5)
scan_button = ttk.Button(self.keyfile_settings_frame, text="Scan Destination for Encrypted Profiles", command=self._populate_keyfile_profiles_view)
scan_button.pack(pady=5)
# Action Button
key_file_button = ttk.Button(
self.keyfile_settings_frame, text=Msg.STR["create_add_key_file"], command=self._create_key_file)
key_file_button.grid(row=2, column=0, padx=5, pady=10)
# Status Label
self.key_file_status_var = tk.StringVar(value="")
key_file_status_label = ttk.Label(
self.keyfile_settings_frame, textvariable=self.key_file_status_var, foreground="gray")
key_file_status_label.grid(
row=2, column=1, padx=5, pady=10, sticky="w")
# Sudoers Info
sudoers_info_label = ttk.Label(
self.keyfile_settings_frame, text=Msg.STR["sudoers_info_text"], justify="left")
sudoers_info_label.grid(
row=3, column=0, columnspan=3, sticky="w", padx=5, pady=10)
self.keyfile_settings_frame.columnconfigure(1, weight=1)
self.keyfile_profiles_frame = ttk.Frame(self.keyfile_settings_frame)
self.keyfile_profiles_frame.pack(fill=tk.BOTH, expand=True, pady=10)
# --- Bottom Buttons (for most views) ---
self.bottom_button_frame = ttk.Frame(self)
@@ -222,7 +201,6 @@ class AdvancedSettingsFrame(ttk.Frame):
self._load_animation_settings()
self._load_backup_defaults()
self._load_manual_excludes()
self._update_key_file_status()
self._switch_view(self.current_view_index)
@@ -259,58 +237,140 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager.set_setting("use_trash_bin", use_trash)
self.config_manager.set_setting("no_trash_bin", no_trash)
def _create_key_file(self):
header = self.app_instance.header_frame
destination = self.app_instance.destination_path
def _populate_keyfile_profiles_view(self):
# Clear existing widgets
for widget in self.keyfile_profiles_frame.winfo_children():
widget.destroy()
self.keyfile_profile_widgets.clear()
destination = self.app_instance.destination_path
if not destination:
MessageDialog(message_type="error", title=Msg.STR["error"],
text=Msg.STR["select_destination_first"]).show()
return
pybackup_dir = os.path.join(destination, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
if not os.path.exists(container_path):
MessageDialog(message_type="error", title=Msg.STR["error"],
text=Msg.STR["err_no_encrypted_container"]).show()
if not os.path.isdir(pybackup_dir):
ttk.Label(self.keyfile_profiles_frame, text="No 'pybackup' directory found at destination.").pack()
return
search_pattern = os.path.join(pybackup_dir, "pybackup_encrypted_*.luks")
found_containers = glob.glob(search_pattern)
if not found_containers:
ttk.Label(self.keyfile_profiles_frame, text="No encrypted profiles found at destination.").pack()
return
profiles = []
for container in found_containers:
filename = os.path.basename(container)
profile_name = filename.replace("pybackup_encrypted_", "").replace(".luks", "")
if profile_name:
profiles.append(profile_name)
# Master "All" checkbox
all_var = tk.BooleanVar()
all_check = ttk.Checkbutton(self.keyfile_profiles_frame, text="All Profiles", variable=all_var, style="Switch.TCheckbutton",
command=lambda v=all_var: self._on_master_keyfile_toggle(v, profiles))
all_check.pack(anchor="w", padx=5, pady=5)
ttk.Separator(self.keyfile_profiles_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
# Individual profile checkboxes
for profile_name in sorted(profiles):
profile_frame = ttk.Frame(self.keyfile_profiles_frame)
profile_frame.pack(fill=tk.X, padx=5, pady=2)
var = tk.BooleanVar()
check = ttk.Checkbutton(profile_frame, text=profile_name, variable=var, style="Switch.TCheckbutton")
check.pack(side=tk.LEFT)
# Use a lambda with default arguments to capture the current values
check.config(command=lambda p=profile_name, v=var: self._on_keyfile_checkbox_toggle(p, v))
status_label = ttk.Label(profile_frame, text="", foreground="gray")
status_label.pack(side=tk.LEFT, padx=10)
self.keyfile_profile_widgets[profile_name] = {"var": var, "status_label": status_label}
self._update_single_keyfile_status(profile_name)
def _on_master_keyfile_toggle(self, var, profiles):
if not var.get():
return # Do nothing on uncheck for safety
password_dialog = PasswordDialog(
self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
password, _ = password_dialog.get_password()
if not password:
var.set(False) # Uncheck the master switch
return
for profile_name in profiles:
self._create_key_file_for_profile(profile_name, password)
def _on_keyfile_checkbox_toggle(self, profile_name, var):
if var.get(): # If checked, create key
self._create_key_file_for_profile(profile_name)
else: # If unchecked, delete key
self._delete_key_file_for_profile(profile_name)
def _create_key_file_for_profile(self, profile_name, password=None):
header = self.app_instance.header_frame
destination = self.app_instance.destination_path
if not password:
password_dialog = PasswordDialog(
self, title=f"{Msg.STR['enter_existing_password_title']} for {profile_name}", confirm=False, translations=Msg.STR)
password, _ = password_dialog.get_password()
if not password:
self._update_single_keyfile_status(profile_name) # Revert checkbox state
return
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
destination, password)
destination, profile_name, password)
if key_file_path:
header.show_temporary_message(Msg.STR["keyfile_creation_success"])
else:
header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
self._update_single_keyfile_status(profile_name)
self._update_key_file_status()
def _update_key_file_status(self):
def _delete_key_file_for_profile(self, profile_name):
header = self.app_instance.header_frame
destination = self.app_instance.destination_path
if not destination:
self.keyfile_dest_path_label.config(text=Msg.STR["no_destination_selected"], foreground="gray")
self.key_file_status_var.set(Msg.STR["keyfile_status_unknown"])
password_dialog = PasswordDialog(
self, title=f"Password to delete key for {profile_name}", confirm=False, translations=Msg.STR)
password, _ = password_dialog.get_password()
if not password:
self._update_single_keyfile_status(profile_name) # Revert checkbox state
return
self.keyfile_dest_path_label.config(text=destination, foreground="#FFFFFF") # Adapt color to theme
success = self.app_instance.backup_manager.encryption_manager.delete_key_file(
destination, profile_name, password)
source_name = self.app_instance.left_canvas_data.get('folder') or "system"
profile_name = "system" if source_name == "Computer" else source_name
if success:
header.show_temporary_message("Keyfile successfully deleted.")
else:
header.show_temporary_message("Failed to delete keyfile.")
self._update_single_keyfile_status(profile_name)
def _update_single_keyfile_status(self, profile_name):
widgets = self.keyfile_profile_widgets.get(profile_name)
if not widgets:
return
destination = self.app_instance.destination_path
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
destination, profile_name)
if os.path.exists(key_file_path):
self.key_file_status_var.set(Msg.STR["keyfile_exists"].format(key_file_path=key_file_path))
widgets["var"].set(True)
widgets["status_label"].config(text="Keyfile exists")
else:
self.key_file_status_var.set(Msg.STR["keyfile_not_found_for_dest"])
widgets["var"].set(False)
widgets["status_label"].config(text="")
def _switch_view(self, index):
self.current_view_index = index
@@ -322,8 +382,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.animation_settings_frame.pack_forget()
self.backup_defaults_frame.pack_forget()
# Show/hide the main action buttons based on the view
if index in [1, 2]: # Manual Excludes and Keyfile Settings views
if index in [1, 2]:
self.bottom_button_frame.pack_forget()
else:
self.bottom_button_frame.pack(pady=10)
@@ -339,7 +398,7 @@ class AdvancedSettingsFrame(ttk.Frame):
elif index == 2:
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text="")
self._update_key_file_status()
self._populate_keyfile_profiles_view()
elif index == 3:
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text="")