add checkboxes to keyfileframe
This commit is contained in:
@@ -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."""
|
||||
|
@@ -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="")
|
||||
|
Reference in New Issue
Block a user