mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 14:58:56 +00:00
Merge pull request #683 from PR0M3TH3AN/codex/add-key-field-to-key/value-pair-entry
Add key field to key/value entries
This commit is contained in:
@@ -458,7 +458,7 @@ The table below summarizes the extra fields stored for each entry type. Every en
|
||||
| Seed Phrase | `index`, `word_count` *(mnemonic regenerated; never stored)*, `archived`, optional `notes`, optional `tags` |
|
||||
| PGP Key | `index`, `key_type`, `archived`, optional `user_id`, optional `notes`, optional `tags` |
|
||||
| Nostr Key Pair | `index`, `archived`, optional `notes`, optional `tags` |
|
||||
| Key/Value | `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` |
|
||||
| Key/Value | `key`, `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` |
|
||||
| Managed Account | `index`, `word_count`, `fingerprint`, `archived`, optional `notes`, optional `tags` |
|
||||
|
||||
### Managing Multiple Seeds
|
||||
|
@@ -55,7 +55,7 @@ Manage individual entries within a vault.
|
||||
| Add a PGP key entry | `entry add-pgp` | `seedpass entry add-pgp Personal --user-id me@example.com` |
|
||||
| Add a Nostr key entry | `entry add-nostr` | `seedpass entry add-nostr Chat` |
|
||||
| Add a seed phrase entry | `entry add-seed` | `seedpass entry add-seed Backup --words 24` |
|
||||
| Add a key/value entry | `entry add-key-value` | `seedpass entry add-key-value "API Token" --value abc123` |
|
||||
| Add a key/value entry | `entry add-key-value` | `seedpass entry add-key-value "API Token" --key api --value abc123` |
|
||||
| Add a managed account entry | `entry add-managed-account` | `seedpass entry add-managed-account Trading` |
|
||||
| Modify an entry | `entry modify` | `seedpass entry modify 1 --username alice` |
|
||||
| Archive an entry | `entry archive` | `seedpass entry archive 1` |
|
||||
@@ -144,7 +144,7 @@ Run or stop the local HTTP API.
|
||||
- **`seedpass entry add-pgp <label>`** – Create a PGP key entry. Provide `--user-id` and `--key-type` as needed.
|
||||
- **`seedpass entry add-nostr <label>`** – Create a Nostr key entry for decentralised chat.
|
||||
- **`seedpass entry add-seed <label>`** – Store a derived seed phrase. Use `--words` to set the word count.
|
||||
- **`seedpass entry add-key-value <label>`** – Store arbitrary data with `--value`.
|
||||
- **`seedpass entry add-key-value <label>`** – Store arbitrary data with `--key` and `--value`.
|
||||
- **`seedpass entry add-managed-account <label>`** – Store a BIP‑85 derived account seed.
|
||||
- **`seedpass entry modify <id>`** – Update an entry's label, username, URL or notes.
|
||||
- **`seedpass entry archive <id>`** – Mark an entry as archived so it is hidden from normal lists.
|
||||
|
@@ -95,6 +95,7 @@ Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` d
|
||||
- **custom_fields** (`array`, optional): Additional user-defined fields.
|
||||
- **origin** (`string`, optional): Source identifier for imported data.
|
||||
- **value** (`string`, optional): For `key_value` entries, stores the secret value.
|
||||
- **key** (`string`, optional): Name of the key for `key_value` entries.
|
||||
- **index** (`integer`, optional): BIP-85 derivation index for entries that derive material from a seed.
|
||||
- **word_count** (`integer`, managed_account only): Number of words in the child seed. Managed accounts always use `12`.
|
||||
- **fingerprint** (`string`, managed_account only): Identifier of the child profile, used for its directory name.
|
||||
|
@@ -363,7 +363,7 @@ entry includes a `label`, while only password entries track a `url`.
|
||||
| Seed Phrase | `index`, `word_count` *(mnemonic regenerated; never stored)*, `archived`, optional `notes`, optional `tags` |
|
||||
| PGP Key | `index`, `key_type`, `archived`, optional `user_id`, optional `notes`, optional `tags` |
|
||||
| Nostr Key Pair| `index`, `archived`, optional `notes`, optional `tags` |
|
||||
| Key/Value | `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` |
|
||||
| Key/Value | `key`, `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` |
|
||||
| Managed Account | `index`, `word_count`, `fingerprint`, `archived`, optional `notes`, optional `tags` |
|
||||
|
||||
|
||||
|
@@ -173,6 +173,7 @@ def create_entry(
|
||||
if etype == "key_value":
|
||||
index = _pm.entry_manager.add_key_value(
|
||||
entry.get("label"),
|
||||
entry.get("key"),
|
||||
entry.get("value"),
|
||||
notes=entry.get("notes", ""),
|
||||
)
|
||||
|
@@ -315,12 +315,13 @@ def entry_add_seed(
|
||||
def entry_add_key_value(
|
||||
ctx: typer.Context,
|
||||
label: str,
|
||||
key: str = typer.Option(..., "--key", help="Key name"),
|
||||
value: str = typer.Option(..., "--value", help="Stored value"),
|
||||
notes: str = typer.Option("", "--notes", help="Entry notes"),
|
||||
) -> None:
|
||||
"""Add a key/value entry and output its index."""
|
||||
service = _get_entry_service(ctx)
|
||||
idx = service.add_key_value(label, value, notes=notes)
|
||||
idx = service.add_key_value(label, key, value, notes=notes)
|
||||
typer.echo(str(idx))
|
||||
|
||||
|
||||
|
@@ -379,9 +379,13 @@ class EntryService:
|
||||
self._manager.start_background_vault_sync()
|
||||
return idx
|
||||
|
||||
def add_key_value(self, label: str, value: str, *, notes: str = "") -> int:
|
||||
def add_key_value(
|
||||
self, label: str, key: str, value: str, *, notes: str = ""
|
||||
) -> int:
|
||||
with self._lock:
|
||||
idx = self._manager.entry_manager.add_key_value(label, value, notes=notes)
|
||||
idx = self._manager.entry_manager.add_key_value(
|
||||
label, key, value, notes=notes
|
||||
)
|
||||
self._manager.start_background_vault_sync()
|
||||
return idx
|
||||
|
||||
|
@@ -412,6 +412,7 @@ class EntryManager:
|
||||
def add_key_value(
|
||||
self,
|
||||
label: str,
|
||||
key: str,
|
||||
value: str,
|
||||
*,
|
||||
notes: str = "",
|
||||
@@ -429,6 +430,7 @@ class EntryManager:
|
||||
"type": EntryType.KEY_VALUE.value,
|
||||
"kind": EntryType.KEY_VALUE.value,
|
||||
"label": label,
|
||||
"key": key,
|
||||
"modified_ts": int(time.time()),
|
||||
"value": value,
|
||||
"notes": notes,
|
||||
@@ -720,6 +722,7 @@ class EntryManager:
|
||||
label: Optional[str] = None,
|
||||
period: Optional[int] = None,
|
||||
digits: Optional[int] = None,
|
||||
key: Optional[str] = None,
|
||||
value: Optional[str] = None,
|
||||
custom_fields: List[Dict[str, Any]] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
@@ -736,6 +739,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 key: (Optional) New key for key/value entries.
|
||||
:param value: (Optional) New value for key/value entries.
|
||||
"""
|
||||
try:
|
||||
@@ -764,6 +768,7 @@ class EntryManager:
|
||||
"label": label,
|
||||
"period": period,
|
||||
"digits": digits,
|
||||
"key": key,
|
||||
"value": value,
|
||||
"custom_fields": custom_fields,
|
||||
"tags": tags,
|
||||
@@ -790,6 +795,7 @@ class EntryManager:
|
||||
},
|
||||
EntryType.KEY_VALUE.value: {
|
||||
"label",
|
||||
"key",
|
||||
"value",
|
||||
"archived",
|
||||
"notes",
|
||||
@@ -870,6 +876,9 @@ class EntryManager:
|
||||
EntryType.KEY_VALUE.value,
|
||||
EntryType.MANAGED_ACCOUNT.value,
|
||||
):
|
||||
if key is not None and entry_type == EntryType.KEY_VALUE.value:
|
||||
entry["key"] = key
|
||||
logger.debug(f"Updated key for index {index}.")
|
||||
if value is not None:
|
||||
entry["value"] = value
|
||||
logger.debug(f"Updated value for index {index}.")
|
||||
|
@@ -1888,6 +1888,10 @@ class PasswordManager:
|
||||
if not label:
|
||||
print(colored("Error: Label cannot be empty.", "red"))
|
||||
return
|
||||
key_field = input("Key: ").strip()
|
||||
if not key_field:
|
||||
print(colored("Error: Key cannot be empty.", "red"))
|
||||
return
|
||||
value = input("Value: ").strip()
|
||||
notes = input("Notes (optional): ").strip()
|
||||
tags_input = input("Enter tags (comma-separated, optional): ").strip()
|
||||
@@ -1915,6 +1919,7 @@ class PasswordManager:
|
||||
|
||||
index = self.entry_manager.add_key_value(
|
||||
label,
|
||||
key_field,
|
||||
value,
|
||||
notes=notes,
|
||||
custom_fields=custom_fields,
|
||||
@@ -2886,6 +2891,9 @@ class PasswordManager:
|
||||
input(f'Enter new label (leave blank to keep "{label}"): ').strip()
|
||||
or label
|
||||
)
|
||||
new_key = input(
|
||||
f'Enter new key (leave blank to keep "{entry.get("key", "")}"): '
|
||||
).strip() or entry.get("key", "")
|
||||
new_value = (
|
||||
input("Enter new value (leave blank to keep current): ").strip()
|
||||
or value
|
||||
@@ -2947,6 +2955,7 @@ class PasswordManager:
|
||||
archived=new_blacklisted,
|
||||
notes=new_notes,
|
||||
label=new_label,
|
||||
key=new_key,
|
||||
value=new_value,
|
||||
custom_fields=custom_fields,
|
||||
tags=tags,
|
||||
@@ -3237,7 +3246,8 @@ class PasswordManager:
|
||||
print(color_text(f" Tags: {', '.join(tags)}", "index"))
|
||||
elif etype == EntryType.KEY_VALUE.value:
|
||||
print(color_text(" Type: Key/Value", "index"))
|
||||
print(color_text(f" Label (key): {entry.get('label', '')}", "index"))
|
||||
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
||||
print(color_text(f" Key: {entry.get('key', '')}", "index"))
|
||||
print(color_text(f" Value: {entry.get('value', '')}", "index"))
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
|
@@ -217,6 +217,7 @@ class EntryDialog(toga.Window):
|
||||
self.length_input = toga.NumberInput(
|
||||
min=8, max=128, style=Pack(width=80), value=16
|
||||
)
|
||||
self.key_input = toga.TextInput(style=Pack(flex=1))
|
||||
self.value_input = toga.TextInput(style=Pack(flex=1))
|
||||
|
||||
save_button = toga.Button(
|
||||
@@ -234,6 +235,8 @@ class EntryDialog(toga.Window):
|
||||
box.add(self.url_input)
|
||||
box.add(toga.Label("Length"))
|
||||
box.add(self.length_input)
|
||||
box.add(toga.Label("Key"))
|
||||
box.add(self.key_input)
|
||||
box.add(toga.Label("Value"))
|
||||
box.add(self.value_input)
|
||||
box.add(save_button)
|
||||
@@ -249,6 +252,7 @@ class EntryDialog(toga.Window):
|
||||
self.username_input.value = entry.get("username", "") or ""
|
||||
self.url_input.value = entry.get("url", "") or ""
|
||||
self.length_input.value = entry.get("length", 16)
|
||||
self.key_input.value = entry.get("key", "")
|
||||
self.value_input.value = entry.get("value", "")
|
||||
|
||||
def save(self, widget: toga.Widget) -> None:
|
||||
@@ -257,6 +261,7 @@ class EntryDialog(toga.Window):
|
||||
url = self.url_input.value or None
|
||||
length = int(self.length_input.value or 16)
|
||||
kind = self.kind_input.value
|
||||
key = self.key_input.value or None
|
||||
value = self.value_input.value or None
|
||||
|
||||
if self.entry_id is None:
|
||||
@@ -275,7 +280,9 @@ class EntryDialog(toga.Window):
|
||||
elif kind == EntryType.NOSTR.value:
|
||||
entry_id = self.main.entries.add_nostr_key(label)
|
||||
elif kind == EntryType.KEY_VALUE.value:
|
||||
entry_id = self.main.entries.add_key_value(label, value or "")
|
||||
entry_id = self.main.entries.add_key_value(
|
||||
label, key or "", value or ""
|
||||
)
|
||||
elif kind == EntryType.MANAGED_ACCOUNT.value:
|
||||
entry_id = self.main.entries.add_managed_account(label)
|
||||
else:
|
||||
@@ -284,7 +291,7 @@ class EntryDialog(toga.Window):
|
||||
if kind == EntryType.PASSWORD.value:
|
||||
kwargs.update({"username": username, "url": url})
|
||||
elif kind == EntryType.KEY_VALUE.value:
|
||||
kwargs.update({"value": value})
|
||||
kwargs.update({"key": key, "value": value})
|
||||
self.main.entries.modify_entry(entry_id, **kwargs)
|
||||
|
||||
entry = self.main.entries.retrieve_entry(entry_id) or {}
|
||||
|
@@ -26,7 +26,7 @@ class DummyPM:
|
||||
add_pgp_key=lambda label, seed, index=None, key_type="ed25519", user_id="", notes="": 3,
|
||||
add_nostr_key=lambda label, index=None, notes="": 4,
|
||||
add_seed=lambda label, seed, index=None, words_num=24, notes="": 5,
|
||||
add_key_value=lambda label, value, notes="": 6,
|
||||
add_key_value=lambda label, key, value, notes="": 6,
|
||||
add_managed_account=lambda label, seed, index=None, notes="": 7,
|
||||
modify_entry=lambda *a, **kw: None,
|
||||
archive_entry=lambda i: None,
|
||||
|
@@ -90,8 +90,8 @@ runner = CliRunner()
|
||||
(
|
||||
"add-key-value",
|
||||
"add_key_value",
|
||||
["Label", "--value", "val", "--notes", "note"],
|
||||
("Label", "val"),
|
||||
["Label", "--key", "k1", "--value", "val", "--notes", "note"],
|
||||
("Label", "k1", "val"),
|
||||
{"notes": "note"},
|
||||
"7",
|
||||
),
|
||||
|
@@ -73,7 +73,7 @@ def test_round_trip_entry_types(method, expected_type):
|
||||
entry_mgr.add_totp("example", TEST_SEED)
|
||||
index = 0
|
||||
elif method == "add_key_value":
|
||||
index = entry_mgr.add_key_value("label", "val")
|
||||
index = entry_mgr.add_key_value("label", "k1", "val")
|
||||
else:
|
||||
if method == "add_ssh_key":
|
||||
index = entry_mgr.add_ssh_key("ssh", TEST_SEED)
|
||||
@@ -118,7 +118,7 @@ def test_legacy_entry_defaults_to_password():
|
||||
("add_pgp_key", ("pgp", TEST_SEED)),
|
||||
("add_nostr_key", ("nostr",)),
|
||||
("add_seed", ("seed", TEST_SEED)),
|
||||
("add_key_value", ("label", "val")),
|
||||
("add_key_value", ("label", "k1", "val")),
|
||||
("add_managed_account", ("acct", TEST_SEED)),
|
||||
],
|
||||
)
|
||||
|
@@ -53,16 +53,18 @@ class FakeEntries:
|
||||
self.added.append(("nostr", label))
|
||||
return 1
|
||||
|
||||
def add_key_value(self, label, value):
|
||||
self.added.append(("key_value", label, value))
|
||||
def add_key_value(self, label, key, value):
|
||||
self.added.append(("key_value", label, key, value))
|
||||
return 1
|
||||
|
||||
def add_managed_account(self, label):
|
||||
self.added.append(("managed_account", label))
|
||||
return 1
|
||||
|
||||
def modify_entry(self, entry_id, username=None, url=None, label=None, value=None):
|
||||
self.modified.append((entry_id, username, url, label, value))
|
||||
def modify_entry(
|
||||
self, entry_id, username=None, url=None, label=None, key=None, value=None
|
||||
):
|
||||
self.modified.append((entry_id, username, url, label, key, value))
|
||||
|
||||
|
||||
def setup_module(module):
|
||||
@@ -106,7 +108,7 @@ def test_unlock_creates_main_window():
|
||||
(EntryType.SEED.value, ("seed", "L")),
|
||||
(EntryType.PGP.value, ("pgp", "L")),
|
||||
(EntryType.NOSTR.value, ("nostr", "L")),
|
||||
(EntryType.KEY_VALUE.value, ("key_value", "L", "val")),
|
||||
(EntryType.KEY_VALUE.value, ("key_value", "L", "k1", "val")),
|
||||
(EntryType.MANAGED_ACCOUNT.value, ("managed_account", "L")),
|
||||
],
|
||||
)
|
||||
@@ -123,6 +125,7 @@ def test_entrydialog_add_calls_service(kind, expect):
|
||||
dlg.username_input.value = "u"
|
||||
dlg.url_input.value = "x"
|
||||
dlg.length_input.value = 12
|
||||
dlg.key_input.value = "k1"
|
||||
dlg.value_input.value = "val"
|
||||
dlg.save(None)
|
||||
|
||||
@@ -136,9 +139,9 @@ def test_entrydialog_add_calls_service(kind, expect):
|
||||
@pytest.mark.parametrize(
|
||||
"kind,expected",
|
||||
[
|
||||
(EntryType.PASSWORD.value, (1, "newu", "newx", "New", None)),
|
||||
(EntryType.KEY_VALUE.value, (1, None, None, "New", "val2")),
|
||||
(EntryType.TOTP.value, (1, None, None, "New", None)),
|
||||
(EntryType.PASSWORD.value, (1, "newu", "newx", "New", None, None)),
|
||||
(EntryType.KEY_VALUE.value, (1, None, None, "New", "k2", "val2")),
|
||||
(EntryType.TOTP.value, (1, None, None, "New", None, None)),
|
||||
],
|
||||
)
|
||||
def test_entrydialog_edit_calls_service(kind, expected):
|
||||
@@ -157,6 +160,7 @@ def test_entrydialog_edit_calls_service(kind, expected):
|
||||
dlg.kind_input.value = kind
|
||||
dlg.username_input.value = "newu"
|
||||
dlg.url_input.value = "newx"
|
||||
dlg.key_input.value = "k2"
|
||||
dlg.value_input.value = "val2"
|
||||
dlg.save(None)
|
||||
|
||||
|
@@ -23,12 +23,13 @@ def test_add_and_modify_key_value():
|
||||
tmp_path = Path(tmpdir)
|
||||
em = setup_entry_mgr(tmp_path)
|
||||
|
||||
idx = em.add_key_value("API", "abc123", notes="token")
|
||||
idx = em.add_key_value("API entry", "api_key", "abc123", notes="token")
|
||||
entry = em.retrieve_entry(idx)
|
||||
assert entry == {
|
||||
"type": "key_value",
|
||||
"kind": "key_value",
|
||||
"label": "API",
|
||||
"label": "API entry",
|
||||
"key": "api_key",
|
||||
"value": "abc123",
|
||||
"notes": "token",
|
||||
"archived": False,
|
||||
@@ -36,8 +37,9 @@ def test_add_and_modify_key_value():
|
||||
"tags": [],
|
||||
}
|
||||
|
||||
em.modify_entry(idx, value="def456")
|
||||
em.modify_entry(idx, key="api_key2", value="def456")
|
||||
updated = em.retrieve_entry(idx)
|
||||
assert updated["key"] == "api_key2"
|
||||
assert updated["value"] == "def456"
|
||||
|
||||
results = em.search_entries("def456")
|
||||
|
@@ -38,7 +38,7 @@ def test_handle_list_entries(monkeypatch, capsys):
|
||||
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
entry_mgr.add_entry("example.com", 12)
|
||||
entry_mgr.add_key_value("API", "abc123")
|
||||
entry_mgr.add_key_value("API entry", "api", "abc123")
|
||||
entry_mgr.add_managed_account("acct", TEST_SEED)
|
||||
|
||||
inputs = iter(["1", ""]) # list all, then exit
|
||||
@@ -72,7 +72,7 @@ def test_list_entries_show_details(monkeypatch, capsys):
|
||||
pm.secret_mode_enabled = False
|
||||
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
entry_mgr.add_key_value("API", "val")
|
||||
entry_mgr.add_key_value("API entry", "api", "val")
|
||||
entry_mgr.add_managed_account("acct", TEST_SEED)
|
||||
|
||||
monkeypatch.setattr(pm.entry_manager, "get_totp_code", lambda *a, **k: "123456")
|
||||
@@ -353,7 +353,7 @@ def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
|
||||
)
|
||||
expected = "123456"
|
||||
elif entry_type == "key_value":
|
||||
idx = entry_mgr.add_key_value("API", "abc")
|
||||
idx = entry_mgr.add_key_value("API entry", "api", "abc")
|
||||
expected = "abc"
|
||||
else: # managed_account
|
||||
idx = entry_mgr.add_managed_account("acct", TEST_SEED)
|
||||
@@ -390,8 +390,8 @@ def test_show_entry_details_with_enum_type(monkeypatch, capsys, entry_type):
|
||||
)
|
||||
expect = "Label: Example"
|
||||
else: # KEY_VALUE
|
||||
idx = entry_mgr.add_key_value("API", "abc")
|
||||
expect = "API"
|
||||
idx = entry_mgr.add_key_value("API entry", "api", "abc")
|
||||
expect = "API entry"
|
||||
|
||||
data = entry_mgr._load_index(force_reload=True)
|
||||
data["entries"][str(idx)]["type"] = entry_type
|
||||
|
@@ -93,7 +93,7 @@ def test_search_key_value_value():
|
||||
tmp_path = Path(tmpdir)
|
||||
entry_mgr = setup_entry_manager(tmp_path)
|
||||
|
||||
idx = entry_mgr.add_key_value("API", "token123")
|
||||
idx = entry_mgr.add_key_value("API entry", "api", "token123")
|
||||
|
||||
result = entry_mgr.search_entries("token123")
|
||||
assert result == []
|
||||
|
Reference in New Issue
Block a user