From b74c0993ca76c435c391bd2061855339bca184cd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:55:56 -0400 Subject: [PATCH] Prompt for per-entry password policy --- .../01-getting-started/01-advanced_cli.md | 2 +- src/seedpass/core/manager.py | 62 +++++++++++++++++++ src/tests/test_manager_add_password.py | 16 +++++ src/tests/test_manager_workflow.py | 8 +++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/docs/docs/content/01-getting-started/01-advanced_cli.md b/docs/docs/content/01-getting-started/01-advanced_cli.md index 0b5279c..ce3fb7a 100644 --- a/docs/docs/content/01-getting-started/01-advanced_cli.md +++ b/docs/docs/content/01-getting-started/01-advanced_cli.md @@ -223,5 +223,5 @@ Shut down the server with `seedpass api stop`. - Use the `--help` flag for details on any command. - Set a strong master password and regularly export encrypted backups. - Adjust configuration values like `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `nostr_max_retries`, `nostr_retry_delay`, or `quick_unlock` through the `config` commands. -- Customize password complexity with `config set min_uppercase 3`, `config set min_digits 4`, and similar commands. +- Customize the global password policy with commands like `config set min_uppercase 3`. When adding a password interactively you can override these values, choose a safe special-character set, and exclude ambiguous characters. - `entry get` is script‑friendly and can be piped into other commands. diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index dc61107..b139966 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -1441,6 +1441,60 @@ class PasswordManager: ) return + include_special_input = ( + input("Include special characters? (Y/n): ").strip().lower() + ) + include_special_chars: bool | None = None + if include_special_input: + include_special_chars = include_special_input != "n" + + allowed_special_chars = input( + "Allowed special characters (leave blank for default): " + ).strip() + if not allowed_special_chars: + allowed_special_chars = None + + special_mode = input("Special character mode (safe/leave blank): ").strip() + if not special_mode: + special_mode = None + + exclude_ambiguous_input = ( + input("Exclude ambiguous characters? (y/N): ").strip().lower() + ) + exclude_ambiguous: bool | None = None + if exclude_ambiguous_input: + exclude_ambiguous = exclude_ambiguous_input == "y" + + min_uppercase_input = input( + "Minimum uppercase letters (blank for default): " + ).strip() + if min_uppercase_input and not min_uppercase_input.isdigit(): + print(colored("Error: Minimum uppercase must be a number.", "red")) + return + min_uppercase = int(min_uppercase_input) if min_uppercase_input else None + + min_lowercase_input = input( + "Minimum lowercase letters (blank for default): " + ).strip() + if min_lowercase_input and not min_lowercase_input.isdigit(): + print(colored("Error: Minimum lowercase must be a number.", "red")) + return + min_lowercase = int(min_lowercase_input) if min_lowercase_input else None + + min_digits_input = input("Minimum digits (blank for default): ").strip() + if min_digits_input and not min_digits_input.isdigit(): + print(colored("Error: Minimum digits must be a number.", "red")) + return + min_digits = int(min_digits_input) if min_digits_input else None + + min_special_input = input( + "Minimum special characters (blank for default): " + ).strip() + if min_special_input and not min_special_input.isdigit(): + print(colored("Error: Minimum special must be a number.", "red")) + return + min_special = int(min_special_input) if min_special_input else None + # Add the entry to the index and get the assigned index index = self.entry_manager.add_entry( website_name, @@ -1451,6 +1505,14 @@ class PasswordManager: notes=notes, custom_fields=custom_fields, tags=tags, + include_special_chars=include_special_chars, + allowed_special_chars=allowed_special_chars, + special_mode=special_mode, + exclude_ambiguous=exclude_ambiguous, + min_uppercase=min_uppercase, + min_lowercase=min_lowercase, + min_digits=min_digits, + min_special=min_special, ) # Mark database as dirty for background sync diff --git a/src/tests/test_manager_add_password.py b/src/tests/test_manager_add_password.py index 3579f48..7e9f8f1 100644 --- a/src/tests/test_manager_add_password.py +++ b/src/tests/test_manager_add_password.py @@ -52,6 +52,14 @@ def test_handle_add_password(monkeypatch, dummy_nostr_client, capsys): "", # tags "n", # add custom field "", # length (default) + "", # include special default + "", # allowed special default + "", # special mode default + "", # exclude ambiguous default + "", # min uppercase + "", # min lowercase + "", # min digits + "", # min special ] ) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) @@ -113,6 +121,14 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys "", # tags "n", # add custom field "", # length (default) + "", # include special default + "", # allowed special default + "", # special mode default + "", # exclude ambiguous default + "", # min uppercase + "", # min lowercase + "", # min digits + "", # min special ] ) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) diff --git a/src/tests/test_manager_workflow.py b/src/tests/test_manager_workflow.py index 99bea0a..b2a7a54 100644 --- a/src/tests/test_manager_workflow.py +++ b/src/tests/test_manager_workflow.py @@ -57,6 +57,14 @@ def test_manager_workflow(monkeypatch): "", # tags "n", # add custom field "", # length (default) + "", # include special default + "", # allowed special default + "", # special mode default + "", # exclude ambiguous default + "", # min uppercase + "", # min lowercase + "", # min digits + "", # min special "0", # retrieve index "", # no action in entry menu "0", # modify index