feat: Improve encrypted backup management and UI feedback

This commit introduces significant improvements to how encrypted backups are handled,
focusing on user experience and system integration.

- Persistent Mounts: Encrypted backup containers now remain mounted across UI view changes,
  eliminating repeated password prompts when navigating the application. The container is
  automatically unmounted when the destination changes or the application closes.
- Key Management Fallback: The mounting process now intelligently falls back from
  keyring to keyfile, and finally to a user password prompt if previous methods fail.
- Enhanced UI Status: The header now provides detailed feedback on the encryption key
  status, indicating whether a key is available (via keyring or keyfile) and if the
  container is currently in use.
- Reduced pkexec Prompts: By keeping containers mounted, the number of system-level
  pkexec authentication prompts is drastically reduced, improving workflow.
- Bug Fixes:
    - Corrected a SyntaxError in encryption_manager.py related to string escaping.
    - Fixed an AttributeError in header_frame.py by restoring the is_key_in_keyring method.
    - Addressed a TclError on application shutdown by safely destroying Tkinter widgets.
This commit is contained in:
2025-09-06 15:39:59 +02:00
parent 739c18f2a9
commit e1b12227d0
3 changed files with 53 additions and 59 deletions

View File

@@ -24,6 +24,7 @@ class EncryptionManager:
self.service_id = "py-backup-encryption"
self.session_password = None
self.mounted_destinations = set()
self.auth_method = None
def get_key_file_path(self, base_dest_path: str) -> str:
"""Generates the standard path for the key file for a given destination."""
@@ -95,6 +96,7 @@ class EncryptionManager:
def mount(self, base_dest_path: str, queue=None) -> Optional[str]:
if not self.is_encrypted(base_dest_path):
self.auth_method = None
return None
if self.is_mounted(base_dest_path):
@@ -103,18 +105,43 @@ class EncryptionManager:
return os.path.join(pybackup_dir, "encrypted")
username = os.path.basename(base_dest_path.rstrip('/'))
password = self.get_password(username, confirm=False)
if not password:
self.logger.log("No password provided, cannot mount container.")
return None
# Use a dummy queue if none is provided
if queue is None:
from queue import Queue
queue = Queue()
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password)
# 1. Try keyring
password = self.get_password_from_keyring(username)
if password:
self.logger.log("Found password in keyring. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password=password)
if mount_point:
self.auth_method = "keyring"
self.mounted_destinations.add(base_dest_path)
return mount_point
# 2. Try key file
key_file_path = self.get_key_file_path(base_dest_path)
if os.path.exists(key_file_path):
self.logger.log(f"Found key file at {key_file_path}. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, key_file=key_file_path)
if mount_point:
self.auth_method = "keyfile"
self.mounted_destinations.add(base_dest_path)
return mount_point
# 3. Prompt for password
self.logger.log("No password in keyring or key file found. Prompting user.")
password = self.get_password(username, confirm=False)
if not password:
self.logger.log("No password provided, cannot mount container.")
self.auth_method = None
return None
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password=password)
if mount_point:
self.auth_method = "password"
self.mounted_destinations.add(base_dest_path)
return mount_point

View File

@@ -552,49 +552,10 @@ class MainApplication(tk.Tk):
self.animated_icon = None
app_logger.log(Msg.STR["app_quit"])
self.destroy()
self.config_manager.set_setting("last_mode", self.mode)
if self.backup_left_canvas_data.get('path_display'):
self.config_manager.set_setting(
"backup_source_path", self.backup_left_canvas_data['path_display'])
else:
self.config_manager.set_setting("backup_source_path", None)
if self.backup_right_canvas_data.get('path_display'):
self.config_manager.set_setting(
"backup_destination_path", self.backup_right_canvas_data['path_display'])
else:
self.config_manager.set_setting("backup_destination_path", None)
if self.restore_left_canvas_data.get('path_display'):
self.config_manager.set_setting(
"restore_destination_path", self.restore_left_canvas_data['path_display'])
else:
self.config_manager.set_setting("restore_destination_path", None)
if self.restore_right_canvas_data.get('path_display'):
self.config_manager.set_setting(
"restore_source_path", self.restore_right_canvas_data['path_display'])
else:
self.config_manager.set_setting("restore_source_path", None)
if self.left_canvas_animation:
self.left_canvas_animation.stop()
self.left_canvas_animation.destroy()
self.left_canvas_animation = None
if self.right_canvas_animation:
self.right_canvas_animation.stop()
self.right_canvas_animation.destroy()
self.right_canvas_animation = None
if self.animated_icon:
self.animated_icon.stop()
self.animated_icon.destroy()
self.animated_icon = None
app_logger.log(Msg.STR["app_quit"])
self.destroy()
try:
self.destroy()
except tk.TclError:
pass # App is already destroyed
def _process_queue(self):
try:

View File

@@ -69,29 +69,35 @@ class HeaderFrame(tk.Frame):
def refresh_status(self):
"""Checks the keyring status based on the current destination and updates the label."""
dest_path = self.app.destination_path
if not dest_path:
self.keyring_status_label.config(
text="Keyring: N/A",
fg="#A9A9A9" # DarkGray
)
if not dest_path or not self.encryption_manager.is_encrypted(dest_path):
self.keyring_status_label.config(text="") # Clear status if not encrypted
return
username = os.path.basename(dest_path.rstrip('/'))
mapper_name = f"pybackup_{username}"
mount_point = f"/mnt/{mapper_name}"
if os.path.ismount(mount_point):
if self.encryption_manager.is_mounted(dest_path):
status_text = "Key: In Use"
auth_method = getattr(self.encryption_manager, 'auth_method', None)
if auth_method == 'keyring':
status_text += " (Keyring)"
elif auth_method == 'keyfile':
status_text += " (Keyfile)"
self.keyring_status_label.config(
text="Keyring: In Use",
text=status_text,
fg="#2E8B57" # SeaGreen
)
elif self.encryption_manager.is_key_in_keyring(username):
self.keyring_status_label.config(
text="Keyring: Available",
text="Key: Available (Keyring)",
fg="#FFD700" # Gold
)
elif os.path.exists(self.encryption_manager.get_key_file_path(dest_path)):
self.keyring_status_label.config(
text="Key: Available (Keyfile)",
fg="#FFD700" # Gold
)
else:
self.keyring_status_label.config(
text="Keyring: Not in Use",
text="Key: Not Available",
fg="#A9A9A9" # DarkGray
)