Remove deprecated website field

This commit is contained in:
thePR0M3TH3AN
2025-07-05 10:00:46 -04:00
parent 8a8bd0ada5
commit bacf32b46e
16 changed files with 123 additions and 68 deletions

View File

@@ -169,7 +169,7 @@ seedpass totp "email"
# The code is printed and copied to your clipboard
# Sort or filter the list view
seedpass list --sort website
seedpass list --sort label
seedpass list --filter totp
# Use the **Settings** menu to configure an extra backup directory
@@ -185,7 +185,7 @@ The encrypted index file `seedpass_entries_db.json.enc` begins with `schema_vers
"schema_version": 2,
"entries": {
"0": {
"website": "example.com",
"label": "example.com",
"length": 8,
"type": "password",
"notes": ""

View File

@@ -53,7 +53,7 @@ The following table provides a quick reference to all available advanced CLI com
| Retrieve a password entry | `retrieve` | `-R` | `--retrieve` | `seedpass retrieve --index 3` or `seedpass retrieve --title "GitHub"` |
| Modify an existing entry | `modify` | `-M` | `--modify` | `seedpass modify --index 3 --title "GitHub Pro" --notes "Updated to pro account" --tags "work,development,pro" --length 22` |
| Delete an entry | `delete` | `-D` | `--delete` | `seedpass delete --index 3` |
| List all entries | `list` | `-L` | `--list` | `seedpass list --sort website` |
| List all entries | `list` | `-L` | `--list` | `seedpass list --sort label` |
| Search for a password entry | `search` | `-S` | `--search` | `seedpass search "GitHub"` |
| Get password from query | `get` | | | `seedpass get "GitHub"`
| Display a TOTP code | `totp` | | | `seedpass totp "email"`
@@ -179,11 +179,11 @@ seedpass delete --index 3
**Long Flag:** `--list`
**Description:**
Lists all password entries stored in the password manager. You can sort the output by index, website, or username and filter by entry type.
Lists all password entries stored in the password manager. You can sort the output by index, label, or username and filter by entry type.
**Usage Example:**
```bash
seedpass list --sort website
seedpass list --sort label
seedpass list --filter totp
```

View File

@@ -42,7 +42,7 @@ All entries belonging to a seed profile are summarized in an encrypted file name
"schema_version": 2,
"entries": {
"0": {
"website": "example.com",
"label": "example.com",
"length": 8,
"type": "password",
"notes": ""

View File

@@ -262,7 +262,7 @@ def print_matches(
print(colored(" Type: Nostr Key", "cyan"))
else:
if website:
print(colored(f" Website: {website}", "cyan"))
print(colored(f" Label: {website}", "cyan"))
if username:
print(colored(f" Username: {username}", "cyan"))
if url:

View File

@@ -65,6 +65,13 @@ class EntryManager:
if "kind" not in entry:
entry["kind"] = entry.get("type", EntryType.PASSWORD.value)
entry.setdefault("type", entry["kind"])
if "label" not in entry and "website" in entry:
entry["label"] = entry["website"]
if (
"website" in entry
and entry.get("type") == EntryType.PASSWORD.value
):
entry.pop("website", None)
logger.debug("Index loaded successfully.")
return data
except Exception as e:
@@ -106,7 +113,7 @@ class EntryManager:
def add_entry(
self,
website_name: str,
label: str,
length: int,
username: Optional[str] = None,
url: Optional[str] = None,
@@ -117,7 +124,7 @@ class EntryManager:
"""
Adds a new entry to the encrypted JSON index file.
:param website_name: The name of the website.
:param label: A label describing the entry (e.g. website name).
:param length: The desired length of the password.
:param username: (Optional) The username associated with the website.
:param url: (Optional) The URL of the website.
@@ -131,7 +138,7 @@ class EntryManager:
data.setdefault("entries", {})
data["entries"][str(index)] = {
"website": website_name,
"label": label,
"length": length,
"username": username if username else "",
"url": url if url else "",
@@ -222,7 +229,11 @@ class EntryManager:
raise
def add_ssh_key(
self, parent_seed: str, index: int | None = None, notes: str = ""
self,
label: str,
parent_seed: str,
index: int | None = None,
notes: str = "",
) -> int:
"""Add a new SSH key pair entry.
@@ -240,6 +251,7 @@ class EntryManager:
"type": EntryType.SSH.value,
"kind": EntryType.SSH.value,
"index": index,
"label": label,
"notes": notes,
}
self._save_index(data)
@@ -263,6 +275,7 @@ class EntryManager:
def add_pgp_key(
self,
label: str,
parent_seed: str,
index: int | None = None,
key_type: str = "ed25519",
@@ -280,6 +293,7 @@ class EntryManager:
"type": EntryType.PGP.value,
"kind": EntryType.PGP.value,
"index": index,
"label": label,
"key_type": key_type,
"user_id": user_id,
"notes": notes,
@@ -362,6 +376,7 @@ class EntryManager:
def add_seed(
self,
label: str,
parent_seed: str,
index: int | None = None,
words_num: int = 24,
@@ -378,6 +393,7 @@ class EntryManager:
"type": EntryType.SEED.value,
"kind": EntryType.SEED.value,
"index": index,
"label": label,
"words": words_num,
"notes": notes,
}
@@ -596,11 +612,11 @@ class EntryManager:
idx_str, entry = item
if sort_by == "index":
return int(idx_str)
if sort_by == "website":
return entry.get("website", "").lower()
if sort_by in {"website", "label"}:
return entry.get("label", entry.get("website", "")).lower()
if sort_by == "username":
return entry.get("username", "").lower()
raise ValueError("sort_by must be 'index', 'website', or 'username'")
raise ValueError("sort_by must be 'index', 'label', or 'username'")
sorted_items = sorted(entries_data.items(), key=sort_key)
@@ -616,19 +632,20 @@ class EntryManager:
entries: List[Tuple[int, str, Optional[str], Optional[str], bool]] = []
for idx, entry in filtered_items:
label = entry.get("label", entry.get("website", ""))
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
if etype == EntryType.TOTP.value:
entries.append((idx, entry.get("label", ""), None, None, False))
else:
if etype == EntryType.PASSWORD.value:
entries.append(
(
idx,
entry.get("website", ""),
label,
entry.get("username", ""),
entry.get("url", ""),
entry.get("blacklisted", False),
)
)
else:
entries.append((idx, label, None, None, False))
logger.debug(f"Total entries found: {len(entries)}")
for idx, entry in filtered_items:
@@ -644,8 +661,13 @@ class EntryManager:
"cyan",
)
)
else:
print(colored(f" Website: {entry.get('website', '')}", "cyan"))
elif etype == EntryType.PASSWORD.value:
print(
colored(
f" Label: {entry.get('label', entry.get('website', ''))}",
"cyan",
)
)
print(
colored(f" Username: {entry.get('username') or 'N/A'}", "cyan")
)
@@ -656,6 +678,13 @@ class EntryManager:
"cyan",
)
)
else:
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
print(
colored(
f" Derivation Index: {entry.get('index', index)}", "cyan"
)
)
print("-" * 40)
return entries
@@ -680,16 +709,14 @@ class EntryManager:
for idx, entry in sorted(entries_data.items(), key=lambda x: int(x[0])):
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
if etype == EntryType.TOTP.value:
label = entry.get("label", "")
notes = entry.get("notes", "")
if query_lower in label.lower() or query_lower in notes.lower():
results.append((int(idx), label, None, None, False))
else:
website = entry.get("website", "")
label = entry.get("label", entry.get("website", ""))
notes = entry.get("notes", "")
label_match = query_lower in label.lower()
notes_match = query_lower in notes.lower()
if etype == EntryType.PASSWORD.value:
username = entry.get("username", "")
url = entry.get("url", "")
notes = entry.get("notes", "")
custom_fields = entry.get("custom_fields", [])
custom_match = any(
query_lower in str(cf.get("label", "")).lower()
@@ -697,21 +724,24 @@ class EntryManager:
for cf in custom_fields
)
if (
query_lower in website.lower()
label_match
or query_lower in username.lower()
or query_lower in url.lower()
or query_lower in notes.lower()
or notes_match
or custom_match
):
results.append(
(
int(idx),
website,
label,
username,
url,
entry.get("blacklisted", False),
)
)
else:
if label_match or notes_match:
results.append((int(idx), label, None, None, False))
return results
@@ -829,7 +859,7 @@ class EntryManager:
for entry in entries:
index, website, username, url, blacklisted = entry
print(colored(f"Index: {index}", "cyan"))
print(colored(f" Website: {website}", "cyan"))
print(colored(f" Label: {website}", "cyan"))
print(colored(f" Username: {username or 'N/A'}", "cyan"))
print(colored(f" URL: {url or 'N/A'}", "cyan"))
print(
@@ -856,19 +886,9 @@ class EntryManager:
if filter_kind and etype != filter_kind:
continue
if etype == EntryType.PASSWORD.value:
label = entry.get("website", "")
elif etype == EntryType.TOTP.value:
label = entry.get("label", "")
elif etype == EntryType.SSH.value:
label = "SSH Key"
elif etype == EntryType.SEED.value:
label = "Seed Phrase"
elif etype == EntryType.NOSTR.value:
label = entry.get("label", "Nostr Key")
elif etype == EntryType.PGP.value:
label = "PGP Key"
label = entry.get("label", entry.get("website", ""))
else:
label = etype
label = entry.get("label", etype)
summaries.append((int(idx_str), label))
summaries.sort(key=lambda x: x[0])

View File

@@ -859,9 +859,9 @@ class PasswordManager:
def handle_add_password(self) -> None:
try:
website_name = input("Enter the website name: ").strip()
website_name = input("Enter the label or website name: ").strip()
if not website_name:
print(colored("Error: Website name cannot be empty.", "red"))
print(colored("Error: Label cannot be empty.", "red"))
return
username = input("Enter the username (optional): ").strip()
@@ -1040,8 +1040,12 @@ class PasswordManager:
def handle_add_ssh_key(self) -> None:
"""Add an SSH key pair entry and display the derived keys."""
try:
label = input("Label: ").strip()
if not label:
print(colored("Error: Label cannot be empty.", "red"))
return
notes = input("Notes (optional): ").strip()
index = self.entry_manager.add_ssh_key(self.parent_seed, notes=notes)
index = self.entry_manager.add_ssh_key(label, self.parent_seed, notes=notes)
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
index, self.parent_seed
)
@@ -1075,6 +1079,10 @@ class PasswordManager:
def handle_add_seed(self) -> None:
"""Add a derived BIP-39 seed phrase entry."""
try:
label = input("Label: ").strip()
if not label:
print(colored("Error: Label cannot be empty.", "red"))
return
words_input = input("Word count (12 or 24, default 24): ").strip()
notes = input("Notes (optional): ").strip()
if words_input and words_input not in {"12", "24"}:
@@ -1082,7 +1090,7 @@ class PasswordManager:
return
words = int(words_input) if words_input else 24
index = self.entry_manager.add_seed(
self.parent_seed, words_num=words, notes=notes
label, self.parent_seed, words_num=words, notes=notes
)
phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed)
self.is_dirty = True
@@ -1117,6 +1125,10 @@ class PasswordManager:
def handle_add_pgp(self) -> None:
"""Add a PGP key entry and display the generated key."""
try:
label = input("Label: ").strip()
if not label:
print(colored("Error: Label cannot be empty.", "red"))
return
key_type = (
input("Key type (ed25519 or rsa, default ed25519): ").strip().lower()
or "ed25519"
@@ -1124,6 +1136,7 @@ class PasswordManager:
user_id = input("User ID (optional): ").strip()
notes = input("Notes (optional): ").strip()
index = self.entry_manager.add_pgp_key(
label,
self.parent_seed,
key_type=key_type,
user_id=user_id,
@@ -1162,7 +1175,10 @@ class PasswordManager:
def handle_add_nostr_key(self) -> None:
"""Add a Nostr key entry and display the derived keys."""
try:
label = input("Label (optional): ").strip()
label = input("Label: ").strip()
if not label:
print(colored("Error: Label cannot be empty.", "red"))
return
notes = input("Notes (optional): ").strip()
index = self.entry_manager.add_nostr_key(label, notes=notes)
npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
@@ -1779,6 +1795,7 @@ class PasswordManager:
print(colored(f" Notes: {notes}", "cyan"))
elif etype == EntryType.SEED.value:
print(colored(" Type: Seed Phrase", "cyan"))
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
print(colored(f" Words: {entry.get('words', 24)}", "cyan"))
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
notes = entry.get("notes", "")
@@ -1786,12 +1803,14 @@ class PasswordManager:
print(colored(f" Notes: {notes}", "cyan"))
elif etype == EntryType.SSH.value:
print(colored(" Type: SSH Key", "cyan"))
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
notes = entry.get("notes", "")
if notes:
print(colored(f" Notes: {notes}", "cyan"))
elif etype == EntryType.PGP.value:
print(colored(" Type: PGP Key", "cyan"))
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
print(colored(f" Key Type: {entry.get('key_type', 'ed25519')}", "cyan"))
uid = entry.get("user_id", "")
if uid:
@@ -1808,11 +1827,11 @@ class PasswordManager:
if notes:
print(colored(f" Notes: {notes}", "cyan"))
else:
website = entry.get("website", "")
website = entry.get("label", entry.get("website", ""))
username = entry.get("username", "")
url = entry.get("url", "")
blacklisted = entry.get("blacklisted", False)
print(colored(f" Website: {website}", "cyan"))
print(colored(f" Label: {website}", "cyan"))
print(colored(f" Username: {username or 'N/A'}", "cyan"))
print(colored(f" URL: {url or 'N/A'}", "cyan"))
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))

View File

@@ -33,6 +33,10 @@ def _v1_to_v2(data: dict) -> dict:
for k, v in passwords.items():
v.setdefault("type", "password")
v.setdefault("notes", "")
if "label" not in v and "website" in v:
v["label"] = v["website"]
if v.get("type") == "password" and "website" in v:
v.pop("website", None)
entries[k] = v
data["entries"] = entries
data["schema_version"] = 2
@@ -46,6 +50,10 @@ def _v2_to_v3(data: dict) -> dict:
for entry in entries.values():
entry.setdefault("custom_fields", [])
entry.setdefault("origin", "")
if entry.get("type", "password") == "password":
if "label" not in entry and "website" in entry:
entry["label"] = entry["website"]
entry.pop("website", None)
data["schema_version"] = 3
return data

View File

@@ -25,7 +25,7 @@ def test_backup_restore_workflow(monkeypatch):
"schema_version": 3,
"entries": {
"0": {
"website": "a",
"label": "a",
"length": 10,
"type": "password",
"kind": "password",
@@ -48,7 +48,7 @@ def test_backup_restore_workflow(monkeypatch):
"schema_version": 3,
"entries": {
"0": {
"website": "b",
"label": "b",
"length": 12,
"type": "password",
"kind": "password",

View File

@@ -30,7 +30,7 @@ def test_add_and_retrieve_entry():
entry = entry_mgr.retrieve_entry(index)
assert entry == {
"website": "example.com",
"label": "example.com",
"length": 12,
"username": "user",
"url": "",
@@ -69,9 +69,9 @@ def test_round_trip_entry_types(method, expected_type):
index = 0
else:
if method == "add_ssh_key":
index = entry_mgr.add_ssh_key(TEST_SEED)
index = entry_mgr.add_ssh_key("ssh", TEST_SEED)
elif method == "add_seed":
index = entry_mgr.add_seed(TEST_SEED)
index = entry_mgr.add_seed("seed", TEST_SEED)
else:
index = getattr(entry_mgr, method)()

View File

@@ -34,7 +34,7 @@ def test_index_export_import_round_trip():
"schema_version": 3,
"entries": {
"0": {
"website": "example",
"label": "example",
"type": "password",
"notes": "",
"custom_fields": [],
@@ -52,7 +52,7 @@ def test_index_export_import_round_trip():
"schema_version": 3,
"entries": {
"0": {
"website": "changed",
"label": "changed",
"type": "password",
"notes": "",
"custom_fields": [],

View File

@@ -20,7 +20,7 @@ def test_migrate_v0_to_v3(tmp_path: Path):
data = vault.load_index()
assert data["schema_version"] == LATEST_VERSION
expected_entry = {
"website": "a",
"label": "a",
"length": 8,
"type": "password",
"notes": "",
@@ -37,7 +37,7 @@ def test_migrate_v1_to_v3(tmp_path: Path):
data = vault.load_index()
assert data["schema_version"] == LATEST_VERSION
expected_entry = {
"website": "b",
"label": "b",
"length": 10,
"type": "password",
"notes": "",
@@ -59,7 +59,7 @@ def test_migrate_v2_to_v3(tmp_path: Path):
data = vault.load_index()
assert data["schema_version"] == LATEST_VERSION
expected_entry = {
"website": "c",
"label": "c",
"length": 5,
"type": "password",
"notes": "",

View File

@@ -58,7 +58,7 @@ def test_nostr_index_size_limits(pytestconfig: pytest.Config):
if max_entries is not None and entry_count >= max_entries:
break
entry_mgr.add_entry(
website_name=f"site-{entry_count + 1}",
label=f"site-{entry_count + 1}",
length=12,
username="u" * size,
url="https://example.com/" + "a" * size,

View File

@@ -19,7 +19,9 @@ def test_pgp_key_determinism():
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
idx = entry_mgr.add_pgp_key(TEST_SEED, key_type="ed25519", user_id="Test")
idx = entry_mgr.add_pgp_key(
"pgp", TEST_SEED, key_type="ed25519", user_id="Test"
)
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)

View File

@@ -23,8 +23,8 @@ def test_seed_phrase_determinism():
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
idx_12 = entry_mgr.add_seed(TEST_SEED, words_num=12)
idx_24 = entry_mgr.add_seed(TEST_SEED, words_num=24)
idx_12 = entry_mgr.add_seed("seed12", TEST_SEED, words_num=12)
idx_24 = entry_mgr.add_seed("seed24", TEST_SEED, words_num=24)
phrase12_a = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)
phrase12_b = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)

View File

@@ -20,9 +20,15 @@ def test_add_and_retrieve_ssh_key_pair():
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
index = entry_mgr.add_ssh_key(TEST_SEED)
index = entry_mgr.add_ssh_key("ssh", TEST_SEED)
entry = entry_mgr.retrieve_entry(index)
assert entry == {"type": "ssh", "kind": "ssh", "index": index, "notes": ""}
assert entry == {
"type": "ssh",
"kind": "ssh",
"index": index,
"label": "ssh",
"notes": "",
}
priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
priv2, pub2 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)

View File

@@ -21,7 +21,7 @@ def test_ssh_private_key_corresponds_to_public():
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
idx = entry_mgr.add_ssh_key(TEST_SEED)
idx = entry_mgr.add_ssh_key("ssh", TEST_SEED)
priv_pem, pub_pem = entry_mgr.get_ssh_key_pair(idx, TEST_SEED)
priv_key = serialization.load_pem_private_key(