mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
@@ -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:
|
||||
|
@@ -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.
|
||||
|
@@ -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
@@ -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]
|
||||
|
@@ -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
|
||||
|
@@ -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}"]
|
||||
|
58
src/tests/test_retrieve_pause_sensitive_entries.py
Normal file
58
src/tests/test_retrieve_pause_sensitive_entries.py
Normal 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
|
Reference in New Issue
Block a user