diff --git a/backup_manager.py b/backup_manager.py index fcb5c71..22d1ed1 100644 --- a/backup_manager.py +++ b/backup_manager.py @@ -9,8 +9,11 @@ from pathlib import Path from crontab import CronTab import tempfile import stat +import shutil from pbp_app_config import AppConfig +from pyimage_ui.password_dialog import PasswordDialog +from core.encryption_manager import EncryptionManager class BackupManager: @@ -18,52 +21,15 @@ class BackupManager: Handles the logic for creating and managing backups using rsync. """ - def __init__(self, logger): + def __init__(self, logger, app=None): self.logger = logger self.process = None self.app_tag = "# Py-Backup Job" self.is_system_process = False + self.app = app + self.encryption_manager = EncryptionManager(logger, app) - def _execute_as_root(self, script_content: str) -> bool: - """Executes a shell script with root privileges using pkexec.""" - script_path = '' - try: - # Use tempfile for secure temporary file creation - with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="pybackup_script_", suffix=".sh", dir="/tmp") as tmp_script: - tmp_script.write("#!/bin/bash\n\n") - tmp_script.write("set -e\n\n") # Exit on error - tmp_script.write(script_content) - script_path = tmp_script.name - - # Make the script executable - os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP | - stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - - command = ['pkexec', script_path] - - self.logger.log( - f"Executing privileged command via script: {script_path}") - self.logger.log( - f"Script content:\n---\n{script_content}\n---") - - result = subprocess.run( - command, capture_output=True, text=True, check=False) - - if result.returncode == 0: - self.logger.log( - f"Privileged script executed successfully. Output:\n{result.stdout}") - return True - else: - self.logger.log( - f"Privileged script failed. Return code: {result.returncode}\nStderr: {result.stderr}\nStdout: {result.stdout}") - return False - except Exception as e: - self.logger.log( - f"Failed to set up or execute privileged command: {e}") - return False - finally: - if script_path and os.path.exists(script_path): - os.remove(script_path) + def cancel_and_delete_privileged_backup(self, delete_path: str): """Cancels a running system backup and deletes the target directory in one atomic pkexec call.""" @@ -162,10 +128,10 @@ rm -f '{info_file}' else: self.logger.log("No active backup process to cancel.") - def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, mode: str = "incremental"): + def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental"): """Starts a generic backup process for a specific path, reporting to a queue.""" thread = threading.Thread(target=self._run_backup_path, args=( - queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, mode)) + queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode)) thread.daemon = True thread.start() @@ -241,9 +207,23 @@ set -e self.logger.log(f"An unexpected error occurred during local compression/cleanup: {e}") return False - def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, mode: str): + + + def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str): try: - self.is_system_process = is_system + if is_encrypted: + # For encrypted backups, the dest_path is the container file. + container_path = dest_path + ".luks" + # Estimate container size to be 110% of source size + size_gb = int(source_size / (1024**3) * 1.1) + 1 + mount_point = self.encryption_manager.setup_encrypted_backup(queue, container_path, size_gb) + if not mount_point: + return # Error or cancellation already handled in setup method + + # The actual destination for rsync is the mount point + rsync_dest = mount_point + else: + rsync_dest = dest_path self.logger.log( f"Starting backup from '{source_path}' to '{dest_path}'...") @@ -280,7 +260,7 @@ set -e if is_dry_run: command.append('--dry-run') - command.extend([source_path, dest_path]) + command.extend([source_path, rsync_dest]) self.logger.log(f"Rsync command: {' '.join(command)}") transferred_size, total_size = self._execute_rsync(queue, command) @@ -341,6 +321,8 @@ set -e self.logger.log( f"Backup to '{dest_path}' completed.") finally: + if is_encrypted and 'mount_point' in locals() and mount_point: + self.encryption_manager.cleanup_encrypted_backup(f"pybackup_{os.path.basename(dest_path + '.luks')}", mount_point) self.process = None def _create_info_file(self, dest_path: str, filename: str, source_size: int): @@ -633,7 +615,7 @@ set -e # Regex to parse folder names like '6-März-2024_143000_system_full' or '6-März-2024_143000_system_full.tar.gz' name_regex = re.compile( - r"^(\d{1,2}-\w+-\d{4})_(\d{6})_system_(full|incremental)(\.tar\.gz)?$", re.IGNORECASE) + r"^(\d{1,2}-\w+-\d{4})_(\d{6})_system_(full|incremental)(\.tar\.gz|\.luks)?$", re.IGNORECASE) for item in os.listdir(pybackup_path): # Skip info files @@ -648,11 +630,15 @@ set -e date_str = match.group(1) # time_str = match.group(2) # Not currently used in UI, but available backup_type_base = match.group(3).capitalize() - is_compressed = match.group(4) is not None + extension = match.group(4) + is_compressed = (extension == ".tar.gz") + is_encrypted = (extension == ".luks") backup_type = backup_type_base if is_compressed: backup_type += " (Compressed)" + elif is_encrypted: + backup_type += " (Encrypted)" backup_size = "N/A" comment = "" @@ -682,7 +668,8 @@ set -e "folder_name": item, "full_path": full_path, "comment": comment, - "is_compressed": is_compressed + "is_compressed": is_compressed, + "is_encrypted": is_encrypted }) # Sort by parsing the date from the folder name diff --git a/core/encryption_manager.py b/core/encryption_manager.py new file mode 100644 index 0000000..98e98e1 --- /dev/null +++ b/core/encryption_manager.py @@ -0,0 +1,179 @@ + +import keyring +import keyring.errors +from keyring.backends import SecretService +import os +import shutil +import subprocess +import tempfile +import stat +from typing import Optional + +from pyimage_ui.password_dialog import PasswordDialog + +class EncryptionManager: + def __init__(self, logger, app=None): + self.logger = logger + self.app = app + self.service_id = "py-backup-encryption" + self.session_password = None + self.save_to_keyring = False + + def get_password_from_keyring(self, username: str) -> Optional[str]: + try: + return keyring.get_password(self.service_id, username) + except keyring.errors.InitError as e: + self.logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}") + return None + except Exception as e: + self.logger.log(f"Could not get password from keyring: {e}") + return None + + def set_password_in_keyring(self, username: str, password: str): + try: + keyring.set_password(self.service_id, username, password) + self.logger.log(f"Password for {username} stored in keyring.") + except Exception as e: + self.logger.log(f"Could not set password in keyring: {e}") + + def delete_password_from_keyring(self, username: str): + try: + keyring.delete_password(self.service_id, username) + self.logger.log(f"Password for {username} deleted from keyring.") + except Exception as e: + self.logger.log(f"Could not delete password from keyring: {e}") + + def set_session_password(self, password: str, save_to_keyring: bool): + self.session_password = password + self.save_to_keyring = save_to_keyring + + def clear_session_password(self): + self.session_password = None + self.save_to_keyring = False + + def get_password(self, username: str, confirm: bool = True) -> Optional[str]: + if self.session_password: + if self.save_to_keyring: + self.set_password_in_keyring(username, self.session_password) + return self.session_password + + password = self.get_password_from_keyring(username) + if password: + return password + + # If not in keyring, prompt the user + dialog = PasswordDialog(self.app, title=f"Enter password for {username}", confirm=confirm) + password = dialog.get_password() + if password: + # Ask to save the password + # For now, we don't save it automatically. This will be a UI option. + pass + return password + + def setup_encrypted_backup(self, queue, container_path: str, size_gb: int) -> Optional[str]: + """Sets up a LUKS encrypted container for the backup.""" + self.logger.log(f"Setting up encrypted container at {container_path}") + + if not shutil.which("cryptsetup"): + self.logger.log("Error: cryptsetup is not installed.") + queue.put(('error', "cryptsetup is not installed.")) + return None + + mapper_name = f"pybackup_{os.path.basename(container_path)}" + mount_point = f"/mnt/{mapper_name}" + + if os.path.exists(container_path): + # Container exists, try to open it + self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.") + password = self.get_password(os.path.basename(container_path), confirm=False) + if not password: + self.logger.log("User cancelled password entry.") + queue.put(('completion', {'status': 'cancelled', 'returncode': -1})) + return None + + script = f""" + echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} - + mkdir -p {mount_point} + mount /dev/mapper/{mapper_name} {mount_point} + """ + if not self._execute_as_root(script): + self.logger.log("Failed to unlock existing encrypted container.") + queue.put(('error', "Failed to unlock existing encrypted container.")) + return None + else: + # Container does not exist, create it + password = self.get_password(os.path.basename(container_path), confirm=True) + if not password: + self.logger.log("User cancelled password entry.") + queue.put(('completion', {'status': 'cancelled', 'returncode': -1})) + return None + + script = f""" + fallocate -l {size_gb}G {container_path} + echo -n '{password}' | cryptsetup luksFormat {container_path} - + echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} - + mkfs.ext4 /dev/mapper/{mapper_name} + mkdir -p {mount_point} + mount /dev/mapper/{mapper_name} {mount_point} + """ + + if not self._execute_as_root(script): + self.logger.log("Failed to setup encrypted container.") + self._cleanup_encrypted_backup(mapper_name, mount_point) + queue.put(('error', "Failed to setup encrypted container.")) + return None + + self.logger.log(f"Encrypted container is ready and mounted at {mount_point}") + return mount_point + + def cleanup_encrypted_backup(self, mapper_name: str, mount_point: str): + """Unmounts and closes the LUKS container.""" + self.logger.log(f"Cleaning up encrypted backup: {mapper_name}") + script = f""" + umount {mount_point} || echo "Mount point not found or already unmounted." + cryptsetup luksClose {mapper_name} || echo "Mapper not found or already closed." + rmdir {mount_point} || echo "Mount point directory not found." + """ + if not self._execute_as_root(script): + self.logger.log("Encrypted backup cleanup script failed.") + + def _execute_as_root(self, script_content: str) -> bool: + """Executes a shell script with root privileges using pkexec.""" + script_path = '' + try: + # Use tempfile for secure temporary file creation + with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="pybackup_script_", suffix=".sh", dir="/tmp") as tmp_script: + tmp_script.write("#!/bin/bash\n\n") + tmp_script.write("set -e\n\n") # Exit on error + tmp_script.write(script_content) + script_path = tmp_script.name + + # Make the script executable + os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP | + stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + + command = ['pkexec', script_path] + + self.logger.log( + f"Executing privileged command via script: {script_path}") + self.logger.log( + f"Script content:\n---\n{script_content}\n---") + + result = subprocess.run( + command, capture_output=True, text=True, check=False) + + if result.returncode == 0: + self.logger.log( + f"Privileged script executed successfully. Output:\n{result.stdout}") + return True + else: + self.logger.log( + f"Privileged script failed. Return code: {result.returncode}\nStderr: {result.stderr}\nStdout: {result.stdout}") + return False + except Exception as e: + self.logger.log( + f"Failed to set up or execute privileged command: {e}") + return False + finally: + if script_path and os.path.exists(script_path): + os.remove(script_path) diff --git a/main_app.py b/main_app.py index 0ab4484..78bb424 100644 --- a/main_app.py +++ b/main_app.py @@ -16,6 +16,7 @@ from pyimage_ui.scheduler_frame import SchedulerFrame from pyimage_ui.backup_content_frame import BackupContentFrame from pyimage_ui.header_frame import HeaderFrame from pyimage_ui.settings_frame import SettingsFrame +from pyimage_ui.encryption_frame import EncryptionFrame from core.data_processing import DataProcessing from pyimage_ui.drawing import Drawing from pyimage_ui.navigation import Navigation @@ -69,7 +70,7 @@ class MainApplication(tk.Tk): self.content_frame.grid_rowconfigure(6, weight=0) self.content_frame.grid_columnconfigure(0, weight=1) - self.backup_manager = BackupManager(app_logger) + self.backup_manager = BackupManager(app_logger, self) self.queue = Queue() self.image_manager = IconManager() @@ -79,6 +80,7 @@ class MainApplication(tk.Tk): self.navigation = Navigation(self) self.actions = Actions(self) + self.mode = "backup" # Default mode self.backup_is_running = False self.start_time = None @@ -142,6 +144,10 @@ class MainApplication(tk.Tk): self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton") self.settings_button.pack(fill=tk.X, pady=10) + self.encryption_button = ttk.Button( + self.sidebar_buttons_frame, text="Encryption", command=lambda: self.navigation.toggle_encryption_frame(5), style="Sidebar.TButton") + self.encryption_button.pack(fill=tk.X, pady=10) + self.header_frame = HeaderFrame(self.content_frame, self.image_manager) self.header_frame.grid(row=0, column=0, sticky="nsew") @@ -216,6 +222,7 @@ class MainApplication(tk.Tk): self._setup_scheduler_frame() self._setup_settings_frame() self._setup_backup_content_frame() + self._setup_encryption_frame() self._setup_task_bar() @@ -400,6 +407,12 @@ class MainApplication(tk.Tk): self.backup_content_frame.grid(row=2, column=0, sticky="nsew") self.backup_content_frame.grid_remove() + def _setup_encryption_frame(self): + self.encryption_frame = EncryptionFrame( + self.content_frame, self.backup_manager.encryption_manager, padding=10) + self.encryption_frame.grid(row=2, column=0, sticky="nsew") + self.encryption_frame.grid_remove() + def _setup_task_bar(self): # Define all boolean vars at the top to ensure they exist before use. self.vollbackup_var = tk.BooleanVar() @@ -459,7 +472,7 @@ class MainApplication(tk.Tk): variable=self.compressed_var, command=self.actions.handle_compression_change) self.compressed_cb.pack(side=tk.LEFT, padx=5) self.encrypted_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["encrypted"], - variable=self.encrypted_var) + variable=self.encrypted_var, command=self.actions.handle_encryption_change) self.encrypted_cb.pack(side=tk.LEFT, padx=5) self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"], variable=self.testlauf_var) diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index 69c19ef..75deece 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -88,6 +88,23 @@ class Actions: # The state of the incremental checkbox depends on whether a full backup exists self._update_backup_type_controls() + def handle_encryption_change(self): + if self.app.encrypted_var.get(): + # Encryption is enabled, force full backup and disable compression + self.app.vollbackup_var.set(True) + self.app.inkrementell_var.set(False) + self.app.full_backup_cb.config(state="disabled") + self.app.incremental_cb.config(state="disabled") + self.app.compressed_var.set(False) + self.app.compressed_cb.config(state="disabled") + self.app.accurate_size_cb.config(state="disabled") + self.app.genaue_berechnung_var.set(False) + else: + # Encryption is disabled, restore normal logic + self.app.full_backup_cb.config(state="normal") + self.app.compressed_cb.config(state="normal") + self._update_backup_type_controls() + def on_toggle_accurate_size_calc(self): # This method is called when the user clicks the "Genaue inkrem. Größe" checkbox. if not self.app.genaue_berechnung_var.get(): @@ -673,6 +690,7 @@ class Actions: is_dry_run = self.app.testlauf_var.get() is_compressed = self.app.compressed_var.get() + is_encrypted = self.app.encrypted_var.get() self.app.backup_manager.start_backup( queue=self.app.queue, @@ -683,6 +701,7 @@ class Actions: exclude_files=exclude_file_paths, source_size=source_size_bytes, is_compressed=is_compressed, + is_encrypted=is_encrypted, mode=mode) def _start_user_backup(self, sources): diff --git a/pyimage_ui/encryption_frame.py b/pyimage_ui/encryption_frame.py new file mode 100644 index 0000000..a76b500 --- /dev/null +++ b/pyimage_ui/encryption_frame.py @@ -0,0 +1,75 @@ + +import tkinter as tk +from tkinter import ttk +from shared_libs.message import MessageDialog +import keyring + + +class EncryptionFrame(ttk.Frame): + def __init__(self, parent, encryption_manager, **kwargs): + super().__init__(parent, **kwargs) + self.encryption_manager = encryption_manager + + self.columnconfigure(0, weight=1) + + ttk.Label(self, text="Encryption Settings", font=("Ubuntu", 16, "bold")).grid( + row=0, column=0, pady=10, sticky="w") + + # Keyring status + self.keyring_status_label = ttk.Label(self, text="") + self.keyring_status_label.grid( + row=1, column=0, sticky="ew", padx=10, pady=5) + self.check_keyring_availability() + + # Password section + password_frame = ttk.LabelFrame(self, text="Password Management") + password_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=10) + password_frame.columnconfigure(1, weight=1) + + ttk.Label(password_frame, text="Password:").grid( + row=0, column=0, padx=5, pady=5, sticky="w") + self.password_entry = ttk.Entry(password_frame, show="*") + self.password_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + + self.save_to_keyring_var = tk.BooleanVar() + self.save_to_keyring_cb = ttk.Checkbutton( + password_frame, text="Save password to system keyring", variable=self.save_to_keyring_var) + self.save_to_keyring_cb.grid( + row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w") + + set_password_button = ttk.Button( + password_frame, text="Set Session Password", command=self.set_session_password) + set_password_button.grid(row=2, column=0, padx=5, pady=5) + + clear_password_button = ttk.Button( + password_frame, text="Clear Session Password", command=self.clear_session_password) + clear_password_button.grid(row=2, column=1, padx=5, pady=5, sticky="w") + + self.status_message_label = ttk.Label(self, text="", foreground="blue") + self.status_message_label.grid(row=3, column=0, sticky="ew", padx=10, pady=5) + + def check_keyring_availability(self): + try: + kr = keyring.get_keyring() + if kr is None: + self.keyring_status_label.config( + text="No system keyring found. Passwords will not be saved.", foreground="orange") + self.save_to_keyring_cb.config(state="disabled") + except keyring.errors.NoKeyringError: + self.keyring_status_label.config( + text="No system keyring found. Passwords will not be saved.", foreground="orange") + self.save_to_keyring_cb.config(state="disabled") + + def set_session_password(self): + password = self.password_entry.get() + if not password: + self.status_message_label.config(text="Password cannot be empty.", foreground="red") + return + + self.encryption_manager.set_session_password(password, self.save_to_keyring_var.get()) + self.status_message_label.config(text="Password set for this session.", foreground="green") + + def clear_session_password(self): + self.encryption_manager.clear_session_password() + self.password_entry.delete(0, tk.END) + self.status_message_label.config(text="Session password cleared.", foreground="green") diff --git a/pyimage_ui/navigation.py b/pyimage_ui/navigation.py index bd5a79a..767a822 100644 --- a/pyimage_ui/navigation.py +++ b/pyimage_ui/navigation.py @@ -142,6 +142,7 @@ class Navigation: self.app.scheduler_frame.hide() self.app.settings_frame.hide() self.app.backup_content_frame.hide() + self.app.encryption_frame.grid_remove() # Show the main content frames self.app.canvas_frame.grid() @@ -186,6 +187,7 @@ class Navigation: self.app.scheduler_frame.hide() self.app.settings_frame.hide() self.app.backup_content_frame.hide() + self.app.encryption_frame.grid_remove() self.app.canvas_frame.grid() self.app.source_size_frame.grid() self.app.target_size_frame.grid() @@ -240,6 +242,7 @@ class Navigation: self.app.log_frame.grid_remove() self.app.settings_frame.hide() self.app.backup_content_frame.hide() + self.app.encryption_frame.grid_remove() self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() self.app.restore_size_frame_before.grid_remove() @@ -257,6 +260,7 @@ class Navigation: self.app.log_frame.grid_remove() self.app.backup_content_frame.hide() self.app.scheduler_frame.hide() + self.app.encryption_frame.grid_remove() self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() self.app.restore_size_frame_before.grid_remove() @@ -280,6 +284,7 @@ class Navigation: self.app.log_frame.grid_remove() self.app.scheduler_frame.hide() self.app.settings_frame.hide() + self.app.encryption_frame.grid_remove() self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() self.app.restore_size_frame_before.grid_remove() @@ -287,3 +292,20 @@ class Navigation: self.app.backup_content_frame.show(self.app.destination_path) self.app.top_bar.grid() self._update_task_bar_visibility("scheduler") + + def toggle_encryption_frame(self, active_index=None): + self._cancel_calculation() + self.app.drawing.update_nav_buttons(-1) + + self.app.canvas_frame.grid_remove() + self.app.log_frame.grid_remove() + self.app.scheduler_frame.hide() + self.app.settings_frame.hide() + self.app.backup_content_frame.hide() + self.app.source_size_frame.grid_remove() + self.app.target_size_frame.grid_remove() + self.app.restore_size_frame_before.grid_remove() + self.app.restore_size_frame_after.grid_remove() + self.app.encryption_frame.grid() + self.app.top_bar.grid() + self._update_task_bar_visibility("settings") diff --git a/pyimage_ui/password_dialog.py b/pyimage_ui/password_dialog.py new file mode 100644 index 0000000..9c2ce3b --- /dev/null +++ b/pyimage_ui/password_dialog.py @@ -0,0 +1,60 @@ + +import tkinter as tk +from tkinter import ttk, messagebox + +class PasswordDialog(tk.Toplevel): + def __init__(self, parent, title="Password Required", confirm=True): + super().__init__(parent) + self.title(title) + self.parent = parent + self.password = None + self.confirm = confirm + + self.transient(parent) + self.grab_set() + + ttk.Label(self, text="Please enter the password for the encrypted backup:").pack(padx=20, pady=10) + self.password_entry = ttk.Entry(self, show="*") + self.password_entry.pack(padx=20, pady=5, fill="x", expand=True) + self.password_entry.focus_set() + + if self.confirm: + ttk.Label(self, text="Confirm password:").pack(padx=20, pady=10) + self.confirm_entry = ttk.Entry(self, show="*") + self.confirm_entry.pack(padx=20, pady=5, fill="x", expand=True) + + button_frame = ttk.Frame(self) + button_frame.pack(pady=10) + + ok_button = ttk.Button(button_frame, text="OK", command=self.on_ok) + ok_button.pack(side="left", padx=5) + cancel_button = ttk.Button(button_frame, text="Cancel", command=self.on_cancel) + cancel_button.pack(side="left", padx=5) + + self.bind("", lambda event: self.on_ok()) + self.bind("", lambda event: self.on_cancel()) + + self.wait_window(self) + + def on_ok(self): + password = self.password_entry.get() + + if not password: + messagebox.showerror("Error", "Password cannot be empty.", parent=self) + return + + if self.confirm: + confirm = self.confirm_entry.get() + if password != confirm: + messagebox.showerror("Error", "Passwords do not match.", parent=self) + return + + self.password = password + self.destroy() + + def on_cancel(self): + self.password = None + self.destroy() + + def get_password(self): + return self.password