add checkboxes to keyfileframe
This commit is contained in:
@@ -94,33 +94,62 @@ class EncryptionManager:
|
|||||||
return os.path.join(pybackup_dir, container_filename)
|
return os.path.join(pybackup_dir, container_filename)
|
||||||
|
|
||||||
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
|
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"
|
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]:
|
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)
|
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||||
container_path = self.get_container_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 not os.path.exists(key_file_path):
|
||||||
if self._execute_as_root(script, password):
|
self.logger.log(f"Keyfile for profile {profile_name} does not exist. Nothing to delete.")
|
||||||
self.logger.log(
|
return True # Considered a success as the desired state is "deleted"
|
||||||
"Successfully added key file to LUKS container.")
|
|
||||||
return key_file_path
|
# This script removes the key from the LUKS container and then deletes the keyfile.
|
||||||
else:
|
# It requires a valid password for the container to authorize the key removal.
|
||||||
self.logger.log("Failed to add key file to LUKS container.")
|
script = f"""
|
||||||
os.remove(key_file_path)
|
cryptsetup luksRemoveKey {shlex.quote(container_path)} {shlex.quote(key_file_path)} && \
|
||||||
return None
|
rm -f {shlex.quote(key_file_path)}
|
||||||
except Exception as e:
|
"""
|
||||||
self.logger.log(f"Error creating key file: {e}")
|
|
||||||
if os.path.exists(key_file_path):
|
if self._execute_as_root(script, password):
|
||||||
os.remove(key_file_path)
|
self.logger.log(f"Successfully removed keyfile for profile {profile_name}.")
|
||||||
return None
|
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]]:
|
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."""
|
"""Gets the password (from cache, keyring, or user) or the keyfile path."""
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import os
|
import os
|
||||||
|
import glob
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from core.pbp_app_config import AppConfig, Msg
|
from core.pbp_app_config import AppConfig, Msg
|
||||||
@@ -17,6 +18,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
self.app_instance = app_instance
|
self.app_instance = app_instance
|
||||||
self.current_view_index = 0
|
self.current_view_index = 0
|
||||||
|
self.keyfile_profile_widgets = {}
|
||||||
|
|
||||||
nav_frame = ttk.Frame(self)
|
nav_frame = ttk.Frame(self)
|
||||||
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
|
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
|
||||||
@@ -169,39 +171,16 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
# --- Keyfile Settings Frame ---
|
# --- Keyfile Settings Frame ---
|
||||||
self.keyfile_settings_frame = ttk.LabelFrame(
|
self.keyfile_settings_frame = ttk.LabelFrame(
|
||||||
view_container, text=Msg.STR["automation_settings_title"], padding=10)
|
view_container, text=Msg.STR["automation_settings_title"], padding=10)
|
||||||
|
|
||||||
keyfile_info_label = ttk.Label(
|
keyfile_info_label = ttk.Label(
|
||||||
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
|
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
|
||||||
keyfile_info_label.grid(
|
keyfile_info_label.pack(fill=tk.X, pady=(5, 15))
|
||||||
row=0, column=0, columnspan=3, sticky="w", padx=5, pady=(5, 15))
|
|
||||||
|
|
||||||
# Display for current destination
|
scan_button = ttk.Button(self.keyfile_settings_frame, text="Scan Destination for Encrypted Profiles", command=self._populate_keyfile_profiles_view)
|
||||||
dest_frame = ttk.Frame(self.keyfile_settings_frame)
|
scan_button.pack(pady=5)
|
||||||
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)
|
|
||||||
|
|
||||||
# Action Button
|
self.keyfile_profiles_frame = ttk.Frame(self.keyfile_settings_frame)
|
||||||
key_file_button = ttk.Button(
|
self.keyfile_profiles_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
||||||
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)
|
|
||||||
|
|
||||||
# --- Bottom Buttons (for most views) ---
|
# --- Bottom Buttons (for most views) ---
|
||||||
self.bottom_button_frame = ttk.Frame(self)
|
self.bottom_button_frame = ttk.Frame(self)
|
||||||
@@ -222,7 +201,6 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self._load_animation_settings()
|
self._load_animation_settings()
|
||||||
self._load_backup_defaults()
|
self._load_backup_defaults()
|
||||||
self._load_manual_excludes()
|
self._load_manual_excludes()
|
||||||
self._update_key_file_status()
|
|
||||||
|
|
||||||
self._switch_view(self.current_view_index)
|
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("use_trash_bin", use_trash)
|
||||||
self.config_manager.set_setting("no_trash_bin", no_trash)
|
self.config_manager.set_setting("no_trash_bin", no_trash)
|
||||||
|
|
||||||
def _create_key_file(self):
|
def _populate_keyfile_profiles_view(self):
|
||||||
header = self.app_instance.header_frame
|
# Clear existing widgets
|
||||||
destination = self.app_instance.destination_path
|
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:
|
if not destination:
|
||||||
MessageDialog(message_type="error", title=Msg.STR["error"],
|
MessageDialog(message_type="error", title=Msg.STR["error"],
|
||||||
text=Msg.STR["select_destination_first"]).show()
|
text=Msg.STR["select_destination_first"]).show()
|
||||||
return
|
return
|
||||||
|
|
||||||
pybackup_dir = os.path.join(destination, "pybackup")
|
pybackup_dir = os.path.join(destination, "pybackup")
|
||||||
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
|
if not os.path.isdir(pybackup_dir):
|
||||||
if not os.path.exists(container_path):
|
ttk.Label(self.keyfile_profiles_frame, text="No 'pybackup' directory found at destination.").pack()
|
||||||
MessageDialog(message_type="error", title=Msg.STR["error"],
|
|
||||||
text=Msg.STR["err_no_encrypted_container"]).show()
|
|
||||||
return
|
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(
|
password_dialog = PasswordDialog(
|
||||||
self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
|
self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
|
||||||
password, _ = password_dialog.get_password()
|
password, _ = password_dialog.get_password()
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
|
var.set(False) # Uncheck the master switch
|
||||||
return
|
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(
|
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:
|
if key_file_path:
|
||||||
header.show_temporary_message(Msg.STR["keyfile_creation_success"])
|
header.show_temporary_message(Msg.STR["keyfile_creation_success"])
|
||||||
else:
|
else:
|
||||||
header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
|
header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
|
||||||
|
|
||||||
|
self._update_single_keyfile_status(profile_name)
|
||||||
|
|
||||||
self._update_key_file_status()
|
def _delete_key_file_for_profile(self, profile_name):
|
||||||
|
header = self.app_instance.header_frame
|
||||||
def _update_key_file_status(self):
|
|
||||||
destination = self.app_instance.destination_path
|
destination = self.app_instance.destination_path
|
||||||
if not destination:
|
|
||||||
self.keyfile_dest_path_label.config(text=Msg.STR["no_destination_selected"], foreground="gray")
|
password_dialog = PasswordDialog(
|
||||||
self.key_file_status_var.set(Msg.STR["keyfile_status_unknown"])
|
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
|
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"
|
if success:
|
||||||
profile_name = "system" if source_name == "Computer" else source_name
|
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(
|
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
||||||
destination, profile_name)
|
destination, profile_name)
|
||||||
|
|
||||||
if os.path.exists(key_file_path):
|
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:
|
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):
|
def _switch_view(self, index):
|
||||||
self.current_view_index = index
|
self.current_view_index = index
|
||||||
@@ -322,8 +382,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.animation_settings_frame.pack_forget()
|
self.animation_settings_frame.pack_forget()
|
||||||
self.backup_defaults_frame.pack_forget()
|
self.backup_defaults_frame.pack_forget()
|
||||||
|
|
||||||
# Show/hide the main action buttons based on the view
|
if index in [1, 2]:
|
||||||
if index in [1, 2]: # Manual Excludes and Keyfile Settings views
|
|
||||||
self.bottom_button_frame.pack_forget()
|
self.bottom_button_frame.pack_forget()
|
||||||
else:
|
else:
|
||||||
self.bottom_button_frame.pack(pady=10)
|
self.bottom_button_frame.pack(pady=10)
|
||||||
@@ -339,7 +398,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
elif index == 2:
|
elif index == 2:
|
||||||
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
|
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.info_label.config(text="")
|
self.info_label.config(text="")
|
||||||
self._update_key_file_status()
|
self._populate_keyfile_profiles_view()
|
||||||
elif index == 3:
|
elif index == 3:
|
||||||
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
|
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.info_label.config(text="")
|
self.info_label.config(text="")
|
||||||
|
|||||||
Reference in New Issue
Block a user