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:
@@ -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
|
||||
|
||||
|
||||
47
main_app.py
47
main_app.py
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user