Merge pull request #474 from PR0M3TH3AN/codex/improve-wrong-password-error-message

Improve password retry experience
This commit is contained in:
thePR0M3TH3AN
2025-07-12 09:38:16 -04:00
committed by GitHub
2 changed files with 88 additions and 67 deletions

View File

@@ -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

View File

@@ -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(