mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-10 08:19:23 +00:00
feat: add configurable prompt backoff
This commit is contained in:
@@ -13,7 +13,9 @@ Ensure that all dependencies are installed and properly configured in your envir
|
||||
|
||||
from utils.seed_prompt import masked_input
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
|
||||
from termcolor import colored
|
||||
@@ -28,18 +30,49 @@ colorama_init()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_MAX_ATTEMPTS = 5
|
||||
|
||||
|
||||
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() -> str:
|
||||
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.
|
||||
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.
|
||||
@@ -47,10 +80,10 @@ def prompt_new_password() -> str:
|
||||
Raises:
|
||||
PasswordPromptError: If the user fails to provide a valid password after multiple attempts.
|
||||
"""
|
||||
max_retries = 5
|
||||
max_retries = _get_max_attempts(max_retries)
|
||||
attempts = 0
|
||||
|
||||
while attempts < max_retries:
|
||||
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()
|
||||
@@ -61,6 +94,7 @@ def prompt_new_password() -> str:
|
||||
)
|
||||
logging.warning("User attempted to enter an empty password.")
|
||||
attempts += 1
|
||||
_apply_backoff(attempts, max_retries)
|
||||
continue
|
||||
|
||||
if len(password) < MIN_PASSWORD_LENGTH:
|
||||
@@ -74,6 +108,7 @@ def prompt_new_password() -> str:
|
||||
f"User entered a password shorter than {MIN_PASSWORD_LENGTH} characters."
|
||||
)
|
||||
attempts += 1
|
||||
_apply_backoff(attempts, max_retries)
|
||||
continue
|
||||
|
||||
if password != confirm_password:
|
||||
@@ -82,6 +117,7 @@ def prompt_new_password() -> str:
|
||||
)
|
||||
logging.warning("User entered mismatching passwords.")
|
||||
attempts += 1
|
||||
_apply_backoff(attempts, max_retries)
|
||||
continue
|
||||
|
||||
# Normalize the password to NFKD form
|
||||
@@ -99,6 +135,7 @@ def prompt_new_password() -> str:
|
||||
)
|
||||
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.")
|
||||
@@ -106,16 +143,19 @@ def prompt_new_password() -> str:
|
||||
|
||||
|
||||
def prompt_existing_password(
|
||||
prompt_message: str = "Enter your password: ", max_retries: int = 5
|
||||
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.
|
||||
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): Number of attempts allowed before aborting.
|
||||
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.
|
||||
@@ -124,8 +164,9 @@ def prompt_existing_password(
|
||||
PasswordPromptError: If the user interrupts the operation or exceeds
|
||||
``max_retries`` attempts.
|
||||
"""
|
||||
max_retries = _get_max_attempts(max_retries)
|
||||
attempts = 0
|
||||
while attempts < max_retries:
|
||||
while max_retries == 0 or attempts < max_retries:
|
||||
try:
|
||||
password = masked_input(prompt_message).strip()
|
||||
|
||||
@@ -135,6 +176,7 @@ def prompt_existing_password(
|
||||
)
|
||||
logging.warning("User attempted to enter an empty password.")
|
||||
attempts += 1
|
||||
_apply_backoff(attempts, max_retries)
|
||||
continue
|
||||
|
||||
normalized_password = unicodedata.normalize("NFKD", password)
|
||||
@@ -152,6 +194,7 @@ def prompt_existing_password(
|
||||
)
|
||||
print(colored(f"Error: {e}", "red"))
|
||||
attempts += 1
|
||||
_apply_backoff(attempts, max_retries)
|
||||
|
||||
raise PasswordPromptError("Maximum password attempts exceeded")
|
||||
|
||||
|
Reference in New Issue
Block a user