Merge pull request #568 from PR0M3TH3AN/beta

Beta
This commit is contained in:
thePR0M3TH3AN
2025-07-15 13:09:02 -04:00
committed by GitHub
8 changed files with 943 additions and 456 deletions

View File

@@ -331,6 +331,15 @@ When **Secret Mode** is enabled, SeedPass copies retrieved passwords directly to
2. Choose how many seconds to keep passwords on the clipboard.
3. Retrieve an entry and SeedPass will confirm the password was copied.
### Viewing Entry Details
Selecting an item from **List Entries** or **Search Entries** first displays the
entry's metadata such as the label, username, tags and notes. Passwords, seed
phrases and other sensitive fields remain hidden until you choose to reveal
them. When you opt to show the secret, the details view presents the same action
menu as **Retrieve Entry** so you can edit, archive or display QR codes for the
entry.
### Additional Entry Types
SeedPass supports storing more than just passwords and 2FA secrets. You can also create entries for:

View File

@@ -156,6 +156,14 @@ $ seedpass entry get "email"
Code: 123456
```
### Viewing Entry Details
Picking an entry from `entry list` or `entry search` displays its metadata first
so you can review the label, username and notes. Sensitive fields are hidden
until you confirm you want to reveal them. After showing the secret, the details
view offers the same actions as `entry get`—edit the entry, archive it or show
QR codes for supported types.
### `vault` Commands
- **`seedpass vault export`** Export the entire vault to an encrypted JSON file.

View File

@@ -437,8 +437,8 @@ class EntryManager:
"""Return the npub and nsec for the specified entry."""
entry = self.retrieve_entry(index)
etype = entry.get("type") if entry else None
kind = entry.get("kind") if entry else None
etype = entry.get("type", "").lower() if entry else ""
kind = entry.get("kind", "").lower() if entry else ""
if not entry or (
etype != EntryType.NOSTR.value and kind != EntryType.NOSTR.value
):

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
import pytest
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
import sys
@@ -11,6 +13,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.entry_management import EntryManager
from password_manager.backup import BackupManager
from password_manager.manager import PasswordManager, EncryptionMode
from password_manager.entry_types import EntryType
from password_manager.config_manager import ConfigManager
@@ -80,12 +83,314 @@ def test_list_entries_show_details(monkeypatch, capsys):
lambda *a, **k: "b",
)
inputs = iter(["1", "0", "n"])
inputs = iter(["1", "0"])
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
pm.handle_list_entries()
out = capsys.readouterr().out
assert "Retrieved 2FA Code" in out
assert "123456" in out
assert "Label: Example" in out
assert "Period: 30s" in out
assert "API" in out
assert "acct" in out
def test_show_entry_details_by_index(monkeypatch):
"""Ensure entry details screen triggers expected calls."""
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.nostr_client = SimpleNamespace()
pm.fingerprint_dir = tmp_path
index = entry_mgr.add_entry("example.com", 12)
header_calls = []
monkeypatch.setattr(
"password_manager.manager.clear_header_with_notification",
lambda *a, **k: header_calls.append(True),
)
call_order = []
monkeypatch.setattr(
pm,
"display_entry_details",
lambda *a, **k: call_order.append("display"),
)
monkeypatch.setattr(
pm,
"_entry_actions_menu",
lambda *a, **k: call_order.append("actions"),
)
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
monkeypatch.setattr(
"password_manager.manager.confirm_action", lambda *a, **k: False
)
pm.show_entry_details_by_index(index)
assert len(header_calls) == 1
assert call_order == ["display", "actions"]
def _setup_manager(tmp_path):
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.parent_seed = TEST_SEED
pm.nostr_client = SimpleNamespace()
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
return pm, entry_mgr
def _detail_common(monkeypatch, pm):
monkeypatch.setattr(
"password_manager.manager.clear_header_with_notification",
lambda *a, **k: None,
)
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
monkeypatch.setattr(
"password_manager.manager.confirm_action", lambda *a, **k: False
)
called = []
monkeypatch.setattr(pm, "_entry_actions_menu", lambda *a, **k: called.append(True))
return called
def test_show_seed_entry_details(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_seed("seed", TEST_SEED, words_num=12)
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert "Type: Seed Phrase" in out
assert "Label: seed" in out
assert "Words: 12" in out
assert f"Derivation Index: {idx}" in out
assert called == [True]
def test_show_ssh_entry_details(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_ssh_key("ssh", TEST_SEED)
data = entry_mgr._load_index(force_reload=True)
data["entries"][str(idx)]["public_key_label"] = "server"
data["entries"][str(idx)]["fingerprint"] = "abc123"
entry_mgr._save_index(data)
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert "Type: SSH Key" in out
assert "Label: ssh" in out
assert f"Derivation Index: {idx}" in out
assert "server" in out
assert "abc123" in out
assert called == [True]
def test_show_pgp_entry_details(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
_k, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert "Type: PGP Key" in out
assert "Label: pgp" in out
assert "Key Type: ed25519" in out
assert "User ID: test" in out
assert f"Derivation Index: {idx}" in out
assert fp in out
assert called == [True]
def test_show_nostr_entry_details(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_nostr_key("nostr")
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert "Type: Nostr Key" in out
assert "Label: nostr" in out
assert f"Derivation Index: {idx}" in out
assert called == [True]
def test_show_managed_account_entry_details(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_managed_account("acct", TEST_SEED)
fp = entry_mgr.retrieve_entry(idx).get("fingerprint")
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert "Type: Managed Account" in out
assert "Label: acct" in out
assert f"Derivation Index: {idx}" in out
assert "Words: 12" in out
assert fp in out
assert called == [True]
@pytest.mark.parametrize(
"entry_type",
[
"password",
"seed",
"ssh",
"pgp",
"nostr",
"totp",
"key_value",
"managed_account",
],
)
def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
"""Ensure sensitive details are displayed for each entry type."""
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
pm.password_generator = SimpleNamespace(generate_password=lambda l, i: "pw123")
monkeypatch.setattr(
"password_manager.manager.confirm_action", lambda *a, **k: True
)
monkeypatch.setattr(
"password_manager.manager.copy_to_clipboard", lambda *a, **k: None
)
monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b")
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
monkeypatch.setattr(
"password_manager.manager.TotpManager.print_qr_code", lambda *a, **k: None
)
monkeypatch.setattr(
"password_manager.manager.clear_header_with_notification",
lambda *a, **k: None,
)
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
input_val = "r" if entry_type == "managed_account" else ""
monkeypatch.setattr("builtins.input", lambda *a, **k: input_val)
called = []
monkeypatch.setattr(
pm, "_entry_actions_menu", lambda *a, **k: called.append(True)
)
if entry_type == "password":
idx = entry_mgr.add_entry("example", 8)
expected = "pw123"
elif entry_type == "seed":
idx = entry_mgr.add_seed("seed", TEST_SEED, words_num=12)
expected = entry_mgr.get_seed_phrase(idx, TEST_SEED)
elif entry_type == "ssh":
idx = entry_mgr.add_ssh_key("ssh", TEST_SEED)
priv, pub = entry_mgr.get_ssh_key_pair(idx, TEST_SEED)
expected = priv
extra = pub
elif entry_type == "pgp":
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
priv, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
expected = priv
extra = fp
elif entry_type == "nostr":
idx = entry_mgr.add_nostr_key("nostr")
_npub, nsec = entry_mgr.get_nostr_key_pair(idx, TEST_SEED)
expected = nsec
elif entry_type == "totp":
entry_mgr.add_totp("Example", TEST_SEED)
idx = 0
monkeypatch.setattr(
pm.entry_manager, "get_totp_code", lambda *a, **k: "123456"
)
monkeypatch.setattr(
pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1
)
expected = "123456"
elif entry_type == "key_value":
idx = entry_mgr.add_key_value("API", "abc")
expected = "abc"
else: # managed_account
idx = entry_mgr.add_managed_account("acct", TEST_SEED)
expected = entry_mgr.get_managed_account_seed(idx, TEST_SEED)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert expected in out
if entry_type in {"ssh", "pgp"}:
assert extra in out
assert called == [True]
@pytest.mark.parametrize(
"entry_type", [EntryType.PASSWORD, EntryType.TOTP, EntryType.KEY_VALUE]
)
def test_show_entry_details_with_enum_type(monkeypatch, capsys, entry_type):
"""Entries storing an EntryType enum should display correctly."""
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
if entry_type == EntryType.PASSWORD:
idx = entry_mgr.add_entry("example.com", 8)
expect = "example.com"
elif entry_type == EntryType.TOTP:
entry_mgr.add_totp("Example", TEST_SEED)
idx = 0
monkeypatch.setattr(
pm.entry_manager, "get_totp_code", lambda *a, **k: "123456"
)
monkeypatch.setattr(
pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1
)
expect = "Label: Example"
else: # KEY_VALUE
idx = entry_mgr.add_key_value("API", "abc")
expect = "API"
data = entry_mgr._load_index(force_reload=True)
data["entries"][str(idx)]["type"] = entry_type
entry_mgr._save_index(data)
called = _detail_common(monkeypatch, pm)
pm.show_entry_details_by_index(idx)
out = capsys.readouterr().out
assert expect in out
assert called == [True]

View File

@@ -41,11 +41,11 @@ def test_search_entries_prompt_for_details(monkeypatch, capsys):
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b")
inputs = iter(["Example", "0", "n", ""])
inputs = iter(["Example", "0"])
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
pm.handle_search_entries()
out = capsys.readouterr().out
assert "0. Example" in out
assert "Retrieved 2FA Code" in out
assert "123456" in out
assert "Label: Example" in out
assert "Period: 30s" in out

View File

@@ -93,3 +93,46 @@ def test_show_private_key_qr(monkeypatch, capsys):
out = capsys.readouterr().out
assert called == [nsec]
assert color_text(f"nsec: {nsec}", "deterministic") in out
def test_qr_menu_case_insensitive(monkeypatch):
"""QR menu should appear even if entry type is uppercase."""
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.parent_seed = TEST_SEED
pm.nostr_client = FakeNostrClient()
pm.fingerprint_dir = tmp_path
pm.is_dirty = False
pm.secret_mode_enabled = False
idx = entry_mgr.add_nostr_key("main")
npub, _ = entry_mgr.get_nostr_key_pair(idx, TEST_SEED)
# Modify index to use uppercase type/kind
data = enc_mgr.load_json_data(entry_mgr.index_file)
data["entries"][str(idx)]["type"] = "NOSTR"
data["entries"][str(idx)]["kind"] = "NOSTR"
enc_mgr.save_json_data(data, entry_mgr.index_file)
entry_mgr._index_cache = None
inputs = iter([str(idx), "q", "p", ""])
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
called = []
monkeypatch.setattr(
"password_manager.manager.TotpManager.print_qr_code",
lambda data: called.append(data),
)
pm.handle_retrieve_entry()
assert called == [f"nostr:{npub}"]

View File

@@ -0,0 +1,58 @@
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.entry_management import EntryManager
from password_manager.backup import BackupManager
from password_manager.manager import PasswordManager, EncryptionMode
from password_manager.config_manager import ConfigManager
import pytest
@pytest.mark.parametrize(
"adder,needs_confirm",
[
(lambda mgr: mgr.add_seed("seed", TEST_SEED), True),
(lambda mgr: mgr.add_pgp_key("pgp", TEST_SEED, user_id="test"), True),
(lambda mgr: mgr.add_ssh_key("ssh", TEST_SEED), True),
(lambda mgr: mgr.add_nostr_key("nostr"), False),
],
)
def test_pause_before_entry_actions(monkeypatch, adder, needs_confirm):
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.parent_seed = TEST_SEED
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
index = adder(entry_mgr)
pause_calls = []
monkeypatch.setattr(
"password_manager.manager.pause", lambda *a, **k: pause_calls.append(True)
)
monkeypatch.setattr(pm, "_entry_actions_menu", lambda *a, **k: None)
monkeypatch.setattr("builtins.input", lambda *a, **k: str(index))
if needs_confirm:
monkeypatch.setattr(
"password_manager.manager.confirm_action", lambda *a, **k: True
)
pm.handle_retrieve_entry()
assert len(pause_calls) == 1