feat: add quick password entry mode

This commit is contained in:
thePR0M3TH3AN
2025-08-02 16:43:43 -04:00
parent 7a8c0aef86
commit 7f503f0787
5 changed files with 173 additions and 55 deletions

View File

@@ -409,6 +409,15 @@ When choosing **Add Entry**, you can now select from:
- **Key/Value**
- **Managed Account**
### Adding a Password Entry
After selecting **Password**, SeedPass asks you to pick a mode:
1. **Quick** prompts only for a label, username, URL, desired length, and whether to include special characters. Default values are used for notes, tags, and policy settings.
2. **Advanced** walks through the full set of prompts for notes, tags, custom fields, and detailed password policy options.
Both modes generate the password, display it (or copy it to the clipboard in Secret Mode), and save the entry to your encrypted vault.
### Adding a 2FA Entry
1. From the main menu choose **Add Entry** and select **2FA (TOTP)**.

View File

@@ -316,6 +316,15 @@ When choosing **Add Entry**, you can now select from:
- **Key/Value**
- **Managed Account**
### Adding a Password Entry
After selecting **Password**, SeedPass asks you to choose a mode:
1. **Quick** enter only a label, username, URL, desired length, and whether to include special characters. All other fields use defaults.
2. **Advanced** continue through prompts for notes, tags, custom fields, and detailed password policy settings.
Both modes generate the password, display it (or copy it to the clipboard in Secret Mode), and save the entry to your encrypted vault.
### Adding a 2FA Entry
1. From the main menu choose **Add Entry** and select **2FA (TOTP)**.

View File

@@ -1464,6 +1464,72 @@ class PasswordManager:
parent_fingerprint=parent_fp,
child_fingerprint=child_fp,
)
def prompt_length() -> int | None:
length_input = input(
f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): "
).strip()
length = DEFAULT_PASSWORD_LENGTH
if length_input:
if not length_input.isdigit():
print(
colored("Error: Password length must be a number.", "red")
)
return None
length = int(length_input)
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
print(
colored(
f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.",
"red",
)
)
return None
return length
def finalize_entry(index: int, label: str, length: int) -> None:
# Mark database as dirty for background sync
self.is_dirty = True
self.last_update = time.time()
# Generate the password using the assigned index
entry = self.entry_manager.retrieve_entry(index)
password = self._generate_password_for_entry(entry, index, length)
# Provide user feedback
print(
colored(
f"\n[+] Password generated and indexed with ID {index}.\n",
"green",
)
)
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
else:
print(colored(f"Password for {label}: {password}\n", "yellow"))
# Automatically push the updated encrypted index to Nostr so the
# latest changes are backed up remotely.
try:
self.start_background_vault_sync()
logging.info(
"Encrypted index posted to Nostr after entry addition."
)
except Exception as nostr_error:
logging.error(
f"Failed to post updated index to Nostr: {nostr_error}",
exc_info=True,
)
pause()
mode = input("Choose mode: [Q]uick or [A]dvanced? ").strip().lower()
website_name = input("Enter the label or website name: ").strip()
if not website_name:
print(colored("Error: Label cannot be empty.", "red"))
@@ -1471,6 +1537,29 @@ class PasswordManager:
username = input("Enter the username (optional): ").strip()
url = input("Enter the URL (optional): ").strip()
if mode.startswith("q"):
length = prompt_length()
if length is None:
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"
index = self.entry_manager.add_entry(
website_name,
length,
username,
url,
include_special_chars=include_special_chars,
)
finalize_entry(index, website_name, length)
return
notes = input("Enter notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
@@ -1491,23 +1580,9 @@ class PasswordManager:
{"label": label, "value": value, "is_hidden": hidden}
)
length_input = input(
f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): "
).strip()
length = DEFAULT_PASSWORD_LENGTH
if length_input:
if not length_input.isdigit():
print(colored("Error: Password length must be a number.", "red"))
return
length = int(length_input)
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
print(
colored(
f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.",
"red",
)
)
return
length = prompt_length()
if length is None:
return
include_special_input = (
input("Include special characters? (Y/n): ").strip().lower()
@@ -1563,7 +1638,6 @@ class PasswordManager:
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,
length,
@@ -1583,43 +1657,7 @@ class PasswordManager:
min_special=min_special,
)
# Mark database as dirty for background sync
self.is_dirty = True
self.last_update = time.time()
# Generate the password using the assigned index
entry = self.entry_manager.retrieve_entry(index)
password = self._generate_password_for_entry(entry, index, length)
# Provide user feedback
print(
colored(
f"\n[+] Password generated and indexed with ID {index}.\n",
"green",
)
)
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
else:
print(colored(f"Password for {website_name}: {password}\n", "yellow"))
# Automatically push the updated encrypted index to Nostr so the
# latest changes are backed up remotely.
try:
self.start_background_vault_sync()
logging.info("Encrypted index posted to Nostr after entry addition.")
except Exception as nostr_error:
logging.error(
f"Failed to post updated index to Nostr: {nostr_error}",
exc_info=True,
)
pause()
finalize_entry(index, website_name, length)
except Exception as e:
logging.error(f"Error during password generation: {e}", exc_info=True)

View File

@@ -45,6 +45,7 @@ def test_handle_add_password(monkeypatch, dummy_nostr_client, capsys):
inputs = iter(
[
"a", # advanced mode
"Example", # label
"", # username
"", # url
@@ -114,6 +115,7 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys
inputs = iter(
[
"a", # advanced mode
"Example", # label
"", # username
"", # url
@@ -147,3 +149,62 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys
assert f"pw-0-{DEFAULT_PASSWORD_LENGTH}" not in out
assert "copied to clipboard" in out
assert called == [(f"pw-0-{DEFAULT_PASSWORD_LENGTH}", 5)]
def test_handle_add_password_quick_mode(monkeypatch, dummy_nostr_client, capsys):
client, _relay = dummy_nostr_client
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.encryption_manager = enc_mgr
pm.vault = vault
pm.entry_manager = entry_mgr
pm.backup_manager = backup_mgr
pm.password_generator = FakePasswordGenerator()
pm.parent_seed = TEST_SEED
pm.nostr_client = client
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
pm.is_dirty = False
inputs = iter(
[
"q", # quick mode
"Example", # label
"", # username
"", # url
"", # length (default)
"", # include special default
]
)
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
monkeypatch.setattr("seedpass.core.manager.pause", lambda *a, **k: None)
monkeypatch.setattr(pm, "start_background_vault_sync", lambda *a, **k: None)
pm.handle_add_password()
out = capsys.readouterr().out
entries = entry_mgr.list_entries(verbose=False)
assert entries == [(0, "Example", "", "", False)]
entry = entry_mgr.retrieve_entry(0)
assert entry == {
"label": "Example",
"length": DEFAULT_PASSWORD_LENGTH,
"username": "",
"url": "",
"archived": False,
"type": "password",
"kind": "password",
"notes": "",
"custom_fields": [],
"tags": [],
}
assert f"pw-0-{DEFAULT_PASSWORD_LENGTH}" in out

View File

@@ -50,6 +50,7 @@ def test_manager_workflow(monkeypatch):
inputs = iter(
[
"a", # advanced mode
"example.com",
"", # username
"", # url