From 6ae9f65fe81bd1686dbfffecd598b2d9e9416339 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:17:01 -0400 Subject: [PATCH] Add key-value entry type support --- src/password_manager/entry_management.py | 77 +++++++++++++++++++++--- src/tests/test_key_value_entry.py | 43 +++++++++++++ 2 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 src/tests/test_key_value_entry.py diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index a3479f2..5a2149b 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -367,6 +367,36 @@ class EntryManager: self.backup_manager.create_backup() return index + def add_key_value( + self, + label: str, + value: str, + *, + notes: str = "", + custom_fields=None, + archived: bool = False, + ) -> int: + """Add a new generic key/value entry.""" + + index = self.get_next_index() + + data = self.vault.load_index() + data.setdefault("entries", {}) + data["entries"][str(index)] = { + "type": EntryType.KEY_VALUE.value, + "kind": EntryType.KEY_VALUE.value, + "label": label, + "value": value, + "notes": notes, + "archived": archived, + "custom_fields": custom_fields or [], + } + + self._save_index(data) + self.update_checksum() + self.backup_manager.create_backup() + return index + def get_nostr_key_pair(self, index: int, parent_seed: str) -> tuple[str, str]: """Return the npub and nsec for the specified entry.""" @@ -502,7 +532,8 @@ class EntryManager: entry = data.get("entries", {}).get(str(index)) if entry: - if entry.get("type", entry.get("kind")) == EntryType.PASSWORD.value: + etype = entry.get("type", entry.get("kind")) + if etype in (EntryType.PASSWORD.value, EntryType.KEY_VALUE.value): entry.setdefault("custom_fields", []) logger.debug(f"Retrieved entry at index {index}: {entry}") return entry @@ -531,6 +562,7 @@ class EntryManager: label: Optional[str] = None, period: Optional[int] = None, digits: Optional[int] = None, + value: Optional[str] = None, custom_fields: List[Dict[str, Any]] | None = None, **legacy, ) -> None: @@ -545,6 +577,7 @@ class EntryManager: :param label: (Optional) The new label for the entry. :param period: (Optional) The new TOTP period in seconds. :param digits: (Optional) The new number of digits for TOTP codes. + :param value: (Optional) New value for key/value entries. """ try: data = self.vault.load_index() @@ -578,12 +611,19 @@ class EntryManager: if label is not None: entry["label"] = label logger.debug(f"Updated label to '{label}' for index {index}.") - if username is not None: - entry["username"] = username - logger.debug(f"Updated username to '{username}' for index {index}.") - if url is not None: - entry["url"] = url - logger.debug(f"Updated URL to '{url}' for index {index}.") + if entry_type == EntryType.PASSWORD.value: + if username is not None: + entry["username"] = username + logger.debug( + f"Updated username to '{username}' for index {index}." + ) + if url is not None: + entry["url"] = url + logger.debug(f"Updated URL to '{url}' for index {index}.") + elif entry_type == EntryType.KEY_VALUE.value: + if value is not None: + entry["value"] = value + logger.debug(f"Updated value for index {index}.") if archived is None and "blacklisted" in legacy: archived = legacy["blacklisted"] @@ -797,6 +837,29 @@ class EntryManager: entry.get("archived", entry.get("blacklisted", False)), ) ) + elif etype == EntryType.KEY_VALUE.value: + value_field = str(entry.get("value", "")) + custom_fields = entry.get("custom_fields", []) + custom_match = any( + query_lower in str(cf.get("label", "")).lower() + or query_lower in str(cf.get("value", "")).lower() + for cf in custom_fields + ) + if ( + label_match + or query_lower in value_field.lower() + or notes_match + or custom_match + ): + results.append( + ( + int(idx), + label, + None, + None, + entry.get("archived", entry.get("blacklisted", False)), + ) + ) else: if label_match or notes_match: results.append( diff --git a/src/tests/test_key_value_entry.py b/src/tests/test_key_value_entry.py new file mode 100644 index 0000000..9322d64 --- /dev/null +++ b/src/tests/test_key_value_entry.py @@ -0,0 +1,43 @@ +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.config_manager import ConfigManager + + +def setup_entry_mgr(tmp_path: Path) -> EntryManager: + vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + return EntryManager(vault, backup_mgr) + + +def test_add_and_modify_key_value(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + em = setup_entry_mgr(tmp_path) + + idx = em.add_key_value("API", "abc123", notes="token") + entry = em.retrieve_entry(idx) + assert entry == { + "type": "key_value", + "kind": "key_value", + "label": "API", + "value": "abc123", + "notes": "token", + "archived": False, + "custom_fields": [], + } + + em.modify_entry(idx, value="def456") + updated = em.retrieve_entry(idx) + assert updated["value"] == "def456" + + results = em.search_entries("def456") + assert results == [(idx, "API", None, None, False)]