diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index dec5831..18fb6e8 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -377,51 +377,61 @@ class PasswordManager: ) -> bool: """Set up encryption for the current fingerprint and load the seed.""" - try: - if password is None: - password = prompt_existing_password("Enter your master password: ") - - iterations = ( - self.config_manager.get_kdf_iterations() - if getattr(self, "config_manager", None) - else 100_000 - ) - print("Deriving key...") - seed_key = derive_key_from_password(password, iterations=iterations) - seed_mgr = EncryptionManager(seed_key, fingerprint_dir) - print("Decrypting seed...") + attempts = 0 + max_attempts = 5 + while attempts < max_attempts: try: - self.parent_seed = seed_mgr.decrypt_parent_seed() - except Exception: - msg = "Invalid password for selected seed profile." - print(colored(msg, "red")) + if password is None: + password = prompt_existing_password("Enter your master password: ") + + iterations = ( + self.config_manager.get_kdf_iterations() + if getattr(self, "config_manager", None) + else 100_000 + ) + print("Deriving key...") + seed_key = derive_key_from_password(password, iterations=iterations) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) + print("Decrypting seed...") + try: + self.parent_seed = seed_mgr.decrypt_parent_seed() + except Exception: + msg = ( + "Invalid password for selected seed profile. Please try again." + ) + print(colored(msg, "red")) + attempts += 1 + password = None + continue + + key = derive_index_key(self.parent_seed) + + self.encryption_manager = EncryptionManager(key, fingerprint_dir) + self.vault = Vault(self.encryption_manager, fingerprint_dir) + + self.config_manager = ConfigManager( + vault=self.vault, + fingerprint_dir=fingerprint_dir, + ) + + self.fingerprint_dir = fingerprint_dir + if not self.verify_password(password): + print(colored("Invalid password. Please try again.", "red")) + attempts += 1 + password = None + continue + return True + except KeyboardInterrupt: + raise + except Exception as e: + logger.error(f"Failed to set up EncryptionManager: {e}", exc_info=True) + print(colored(f"Error: Failed to set up encryption: {e}", "red")) if exit_on_fail: sys.exit(1) return False - - key = derive_index_key(self.parent_seed) - - self.encryption_manager = EncryptionManager(key, fingerprint_dir) - self.vault = Vault(self.encryption_manager, fingerprint_dir) - - self.config_manager = ConfigManager( - vault=self.vault, - fingerprint_dir=fingerprint_dir, - ) - - self.fingerprint_dir = fingerprint_dir - if not self.verify_password(password): - print(colored("Invalid password.", "red")) - if exit_on_fail: - sys.exit(1) - return False - return True - except Exception as e: - logger.error(f"Failed to set up EncryptionManager: {e}", exc_info=True) - print(colored(f"Error: Failed to set up encryption: {e}", "red")) - if exit_on_fail: - sys.exit(1) - return False + if exit_on_fail: + sys.exit(1) + return False def load_parent_seed( self, fingerprint_dir: Path, password: Optional[str] = None diff --git a/src/utils/password_prompt.py b/src/utils/password_prompt.py index de380d3..065ea0a 100644 --- a/src/utils/password_prompt.py +++ b/src/utils/password_prompt.py @@ -106,44 +106,55 @@ def prompt_new_password() -> str: raise PasswordPromptError("Maximum password attempts exceeded") -def prompt_existing_password(prompt_message: str = "Enter your password: ") -> str: +def prompt_existing_password( + prompt_message: str = "Enter your password: ", max_retries: int = 5 +) -> str: """ - Prompts the user to enter an existing password, typically used for decryption purposes. + Prompt the user for an existing password. - This function ensures that the password is entered securely without echoing it to the terminal. + The user will be reprompted on empty input up to ``max_retries`` times. Parameters: - prompt_message (str): The message displayed to prompt the user. Defaults to "Enter your password: ". + prompt_message (str): Message displayed when prompting for the password. + max_retries (int): Number of attempts allowed before aborting. Returns: - str: The password entered by the user. + str: The password provided by the user. Raises: - PasswordPromptError: If the user interrupts the operation. + PasswordPromptError: If the user interrupts the operation or exceeds + ``max_retries`` attempts. """ - try: - password = getpass.getpass(prompt=prompt_message).strip() + attempts = 0 + while attempts < max_retries: + try: + password = getpass.getpass(prompt=prompt_message).strip() - if not password: - print(colored("Error: Password cannot be empty.", "red")) - logging.warning("User attempted to enter an empty password.") - raise PasswordPromptError("Password cannot be empty") + if not password: + print( + colored("Error: Password cannot be empty. Please try again.", "red") + ) + logging.warning("User attempted to enter an empty password.") + attempts += 1 + continue - # Normalize the password to NFKD form - normalized_password = unicodedata.normalize("NFKD", password) - logging.debug("User entered an existing password for decryption.") - return normalized_password + normalized_password = unicodedata.normalize("NFKD", password) + logging.debug("User entered an existing password for decryption.") + return normalized_password - except KeyboardInterrupt: - print(colored("\nOperation cancelled by user.", "yellow")) - logging.info("Existing password prompt interrupted by user.") - raise PasswordPromptError("Operation cancelled by user") - except Exception as e: - logging.error( - f"Unexpected error during existing password prompt: {e}", exc_info=True - ) - print(colored(f"Error: {e}", "red")) - raise PasswordPromptError(str(e)) + except KeyboardInterrupt: + print(colored("\nOperation cancelled by user.", "yellow")) + logging.info("Existing password prompt interrupted by user.") + raise PasswordPromptError("Operation cancelled by user") + except Exception as e: + logging.error( + f"Unexpected error during existing password prompt: {e}", + exc_info=True, + ) + print(colored(f"Error: {e}", "red")) + attempts += 1 + + raise PasswordPromptError("Maximum password attempts exceeded") def confirm_action(