mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
# utils/password_prompt.py
|
|
|
|
"""
|
|
Password Prompt Module
|
|
|
|
This module provides functions to securely prompt users for passwords, ensuring that passwords
|
|
are entered and confirmed correctly. It handles both the creation of new passwords and the
|
|
input of existing passwords for decryption purposes. By centralizing password prompting logic,
|
|
this module enhances code reuse, security, and maintainability across the application.
|
|
|
|
Ensure that all dependencies are installed and properly configured in your environment.
|
|
"""
|
|
|
|
from utils.seed_prompt import masked_input
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
import unicodedata
|
|
|
|
from termcolor import colored
|
|
from colorama import init as colorama_init
|
|
|
|
from constants import MIN_PASSWORD_LENGTH
|
|
|
|
# Initialize colorama for colored terminal text
|
|
colorama_init()
|
|
|
|
# Instantiate the logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
DEFAULT_MAX_ATTEMPTS = 5
|
|
|
|
|
|
def _env_password() -> str | None:
|
|
"""Return a password supplied via environment for non-interactive use."""
|
|
|
|
return os.getenv("SEEDPASS_TEST_PASSWORD") or os.getenv("SEEDPASS_PASSWORD")
|
|
|
|
|
|
def _get_max_attempts(override: int | None = None) -> int:
|
|
"""Return the configured maximum number of prompt attempts."""
|
|
|
|
if override is not None:
|
|
return override
|
|
env = os.getenv("SEEDPASS_MAX_PROMPT_ATTEMPTS")
|
|
if env is not None:
|
|
try:
|
|
return int(env)
|
|
except ValueError:
|
|
pass
|
|
return DEFAULT_MAX_ATTEMPTS
|
|
|
|
|
|
def _apply_backoff(attempts: int, max_attempts: int) -> None:
|
|
"""Sleep using exponential backoff unless disabled."""
|
|
|
|
if max_attempts == 0:
|
|
return
|
|
delay = 2 ** (attempts - 1)
|
|
time.sleep(delay)
|
|
|
|
|
|
class PasswordPromptError(Exception):
|
|
"""Exception raised for password prompt errors."""
|
|
|
|
pass
|
|
|
|
|
|
def prompt_new_password(max_retries: int | None = None) -> str:
|
|
"""
|
|
Prompts the user to enter and confirm a new password for encrypting the parent seed.
|
|
|
|
This function ensures that the password meets the minimum length requirement and that the
|
|
password and confirmation match. It provides user-friendly messages and handles retries with
|
|
an exponential backoff between attempts.
|
|
|
|
Parameters:
|
|
max_retries (int | None): Maximum number of attempts before aborting. ``0`` disables the
|
|
limit. Defaults to the ``SEEDPASS_MAX_PROMPT_ATTEMPTS`` environment variable or ``5``.
|
|
|
|
Returns:
|
|
str: The confirmed password entered by the user.
|
|
|
|
Raises:
|
|
PasswordPromptError: If the user fails to provide a valid password after multiple attempts.
|
|
"""
|
|
env_pw = _env_password()
|
|
if env_pw:
|
|
normalized = unicodedata.normalize("NFKD", env_pw)
|
|
if len(normalized) < MIN_PASSWORD_LENGTH:
|
|
raise PasswordPromptError("Environment password too short")
|
|
return normalized
|
|
|
|
max_retries = _get_max_attempts(max_retries)
|
|
attempts = 0
|
|
|
|
while max_retries == 0 or attempts < max_retries:
|
|
try:
|
|
password = masked_input("Enter a new password: ").strip()
|
|
confirm_password = masked_input("Confirm your password: ").strip()
|
|
|
|
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
|
|
_apply_backoff(attempts, max_retries)
|
|
continue
|
|
|
|
if len(password) < MIN_PASSWORD_LENGTH:
|
|
print(
|
|
colored(
|
|
f"Error: Password must be at least {MIN_PASSWORD_LENGTH} characters long.",
|
|
"red",
|
|
)
|
|
)
|
|
logging.warning(
|
|
f"User entered a password shorter than {MIN_PASSWORD_LENGTH} characters."
|
|
)
|
|
attempts += 1
|
|
_apply_backoff(attempts, max_retries)
|
|
continue
|
|
|
|
if password != confirm_password:
|
|
print(
|
|
colored("Error: Passwords do not match. Please try again.", "red")
|
|
)
|
|
logging.warning("User entered mismatching passwords.")
|
|
attempts += 1
|
|
_apply_backoff(attempts, max_retries)
|
|
continue
|
|
|
|
# Normalize the password to NFKD form
|
|
normalized_password = unicodedata.normalize("NFKD", password)
|
|
logging.debug("User entered a valid and confirmed password.")
|
|
return normalized_password
|
|
|
|
except KeyboardInterrupt:
|
|
print(colored("\nOperation cancelled by user.", "yellow"))
|
|
logging.info("Password prompt interrupted by user.")
|
|
raise PasswordPromptError("Operation cancelled by user")
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Unexpected error during password prompt: {e}", exc_info=True
|
|
)
|
|
print(colored(f"Error: {e}", "red"))
|
|
attempts += 1
|
|
_apply_backoff(attempts, max_retries)
|
|
|
|
print(colored("Maximum password attempts exceeded. Exiting.", "red"))
|
|
logging.error("User failed to provide a valid password after multiple attempts.")
|
|
raise PasswordPromptError("Maximum password attempts exceeded")
|
|
|
|
|
|
def prompt_existing_password(
|
|
prompt_message: str = "Enter your password: ", max_retries: int | None = None
|
|
) -> str:
|
|
"""
|
|
Prompt the user for an existing password.
|
|
|
|
The user will be reprompted on empty input up to ``max_retries`` times with
|
|
an exponential backoff between attempts.
|
|
|
|
Parameters:
|
|
prompt_message (str): Message displayed when prompting for the password.
|
|
max_retries (int | None): Number of attempts allowed before aborting. ``0``
|
|
disables the limit. Defaults to the ``SEEDPASS_MAX_PROMPT_ATTEMPTS``
|
|
environment variable or ``5``.
|
|
|
|
Returns:
|
|
str: The password provided by the user.
|
|
|
|
Raises:
|
|
PasswordPromptError: If the user interrupts the operation or exceeds
|
|
``max_retries`` attempts.
|
|
"""
|
|
env_pw = _env_password()
|
|
if env_pw:
|
|
return unicodedata.normalize("NFKD", env_pw)
|
|
|
|
max_retries = _get_max_attempts(max_retries)
|
|
attempts = 0
|
|
while max_retries == 0 or attempts < max_retries:
|
|
try:
|
|
password = masked_input(prompt_message).strip()
|
|
|
|
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
|
|
_apply_backoff(attempts, max_retries)
|
|
continue
|
|
|
|
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"))
|
|
attempts += 1
|
|
_apply_backoff(attempts, max_retries)
|
|
|
|
raise PasswordPromptError("Maximum password attempts exceeded")
|
|
|
|
|
|
def confirm_action(
|
|
prompt_message: str = "Are you sure you want to proceed? (Y/N): ",
|
|
) -> bool:
|
|
"""
|
|
Prompts the user to confirm an action, typically used before performing critical operations.
|
|
|
|
Parameters:
|
|
prompt_message (str): The confirmation message displayed to the user. Defaults to
|
|
"Are you sure you want to proceed? (Y/N): ".
|
|
|
|
Returns:
|
|
bool: True if the user confirms the action, False otherwise.
|
|
|
|
Raises:
|
|
PasswordPromptError: If the user interrupts the operation.
|
|
"""
|
|
try:
|
|
while True:
|
|
response = input(colored(prompt_message, "cyan")).strip().lower()
|
|
if response in ["y", "yes"]:
|
|
logging.debug("User confirmed the action.")
|
|
return True
|
|
elif response in ["n", "no"]:
|
|
logging.debug("User declined the action.")
|
|
return False
|
|
else:
|
|
print(colored("Please respond with 'Y' or 'N'.", "yellow"))
|
|
|
|
except KeyboardInterrupt:
|
|
print(colored("\nOperation cancelled by user.", "yellow"))
|
|
logging.info("Action confirmation interrupted by user.")
|
|
raise PasswordPromptError("Operation cancelled by user")
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Unexpected error during action confirmation: {e}", exc_info=True
|
|
)
|
|
print(colored(f"Error: {e}", "red"))
|
|
raise PasswordPromptError(str(e))
|
|
|
|
|
|
def prompt_for_password() -> str:
|
|
"""
|
|
Prompts the user to enter a new password by invoking the prompt_new_password function.
|
|
|
|
This function serves as an alias to maintain consistency with import statements in other modules.
|
|
|
|
Returns:
|
|
str: The confirmed password entered by the user.
|
|
"""
|
|
return prompt_new_password()
|