mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-14 01:59:36 +00:00
@@ -169,7 +169,7 @@ seedpass totp "email"
|
|||||||
# The code is printed and copied to your clipboard
|
# The code is printed and copied to your clipboard
|
||||||
|
|
||||||
# Sort or filter the list view
|
# Sort or filter the list view
|
||||||
seedpass list --sort website
|
seedpass list --sort label
|
||||||
seedpass list --filter totp
|
seedpass list --filter totp
|
||||||
|
|
||||||
# Use the **Settings** menu to configure an extra backup directory
|
# 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,
|
"schema_version": 2,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "example.com",
|
"label": "example.com",
|
||||||
"length": 8,
|
"length": 8,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": ""
|
"notes": ""
|
||||||
@@ -266,6 +266,9 @@ SeedPass supports storing more than just passwords and 2FA secrets. You can also
|
|||||||
- **Seed Phrase** – generate a BIP-39 mnemonic and keep it encrypted until needed.
|
- **Seed Phrase** – generate a BIP-39 mnemonic and keep it encrypted until needed.
|
||||||
- **PGP Key** – derive an OpenPGP key pair from your master seed.
|
- **PGP Key** – derive an OpenPGP key pair from your master seed.
|
||||||
- **Nostr Key Pair** – store the index used to derive an `npub`/`nsec` pair for Nostr clients.
|
- **Nostr Key Pair** – store the index used to derive an `npub`/`nsec` pair for Nostr clients.
|
||||||
|
When you retrieve one of these entries, SeedPass can display QR codes for the
|
||||||
|
keys. The `npub` is wrapped in the `nostr:` URI scheme so any client can scan
|
||||||
|
it, while the `nsec` QR is shown only after a security warning.
|
||||||
|
|
||||||
|
|
||||||
### Managing Multiple Seeds
|
### Managing Multiple Seeds
|
||||||
|
@@ -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"` |
|
| 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` |
|
| 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` |
|
| 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"` |
|
| Search for a password entry | `search` | `-S` | `--search` | `seedpass search "GitHub"` |
|
||||||
| Get password from query | `get` | | | `seedpass get "GitHub"`
|
| Get password from query | `get` | | | `seedpass get "GitHub"`
|
||||||
| Display a TOTP code | `totp` | | | `seedpass totp "email"`
|
| Display a TOTP code | `totp` | | | `seedpass totp "email"`
|
||||||
@@ -179,11 +179,11 @@ seedpass delete --index 3
|
|||||||
**Long Flag:** `--list`
|
**Long Flag:** `--list`
|
||||||
|
|
||||||
**Description:**
|
**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:**
|
**Usage Example:**
|
||||||
```bash
|
```bash
|
||||||
seedpass list --sort website
|
seedpass list --sort label
|
||||||
seedpass list --filter totp
|
seedpass list --filter totp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -42,7 +42,7 @@ All entries belonging to a seed profile are summarized in an encrypted file name
|
|||||||
"schema_version": 2,
|
"schema_version": 2,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "example.com",
|
"label": "example.com",
|
||||||
"length": 8,
|
"length": 8,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": ""
|
"notes": ""
|
||||||
|
65
src/main.py
65
src/main.py
@@ -234,19 +234,40 @@ def handle_display_stats(password_manager: PasswordManager) -> None:
|
|||||||
print(colored(f"Error: Failed to display stats: {e}", "red"))
|
print(colored(f"Error: Failed to display stats: {e}", "red"))
|
||||||
|
|
||||||
|
|
||||||
def print_matches(matches: list[tuple[int, str, str | None, str | None, bool]]) -> None:
|
def print_matches(
|
||||||
|
password_manager: PasswordManager,
|
||||||
|
matches: list[tuple[int, str, str | None, str | None, bool]],
|
||||||
|
) -> None:
|
||||||
"""Print a list of search matches."""
|
"""Print a list of search matches."""
|
||||||
print(colored("\n[+] Matches:\n", "green"))
|
print(colored("\n[+] Matches:\n", "green"))
|
||||||
for entry in matches:
|
for entry in matches:
|
||||||
idx, website, username, url, blacklisted = entry
|
idx, website, username, url, blacklisted = entry
|
||||||
|
data = password_manager.entry_manager.retrieve_entry(idx)
|
||||||
|
etype = (
|
||||||
|
data.get("type", data.get("kind", EntryType.PASSWORD.value))
|
||||||
|
if data
|
||||||
|
else EntryType.PASSWORD.value
|
||||||
|
)
|
||||||
print(colored(f"Index: {idx}", "cyan"))
|
print(colored(f"Index: {idx}", "cyan"))
|
||||||
if website:
|
if etype == EntryType.TOTP.value:
|
||||||
print(colored(f" Website: {website}", "cyan"))
|
print(colored(f" Label: {data.get('label', website)}", "cyan"))
|
||||||
if username:
|
print(colored(f" Derivation Index: {data.get('index', idx)}", "cyan"))
|
||||||
print(colored(f" Username: {username}", "cyan"))
|
elif etype == EntryType.SEED.value:
|
||||||
if url:
|
print(colored(" Type: Seed Phrase", "cyan"))
|
||||||
print(colored(f" URL: {url}", "cyan"))
|
elif etype == EntryType.SSH.value:
|
||||||
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))
|
print(colored(" Type: SSH Key", "cyan"))
|
||||||
|
elif etype == EntryType.PGP.value:
|
||||||
|
print(colored(" Type: PGP Key", "cyan"))
|
||||||
|
elif etype == EntryType.NOSTR.value:
|
||||||
|
print(colored(" Type: Nostr Key", "cyan"))
|
||||||
|
else:
|
||||||
|
if website:
|
||||||
|
print(colored(f" Label: {website}", "cyan"))
|
||||||
|
if username:
|
||||||
|
print(colored(f" Username: {username}", "cyan"))
|
||||||
|
if url:
|
||||||
|
print(colored(f" URL: {url}", "cyan"))
|
||||||
|
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
|
|
||||||
@@ -680,10 +701,11 @@ def display_menu(
|
|||||||
1. Add Entry
|
1. Add Entry
|
||||||
2. Retrieve Entry
|
2. Retrieve Entry
|
||||||
3. Search Entries
|
3. Search Entries
|
||||||
4. Modify an Existing Entry
|
4. List Entries
|
||||||
5. 2FA Codes
|
5. Modify an Existing Entry
|
||||||
6. Settings
|
6. 2FA Codes
|
||||||
7. Exit
|
7. Settings
|
||||||
|
8. Exit
|
||||||
"""
|
"""
|
||||||
display_fn = getattr(password_manager, "display_stats", None)
|
display_fn = getattr(password_manager, "display_stats", None)
|
||||||
if callable(display_fn):
|
if callable(display_fn):
|
||||||
@@ -708,7 +730,7 @@ def display_menu(
|
|||||||
print(colored(menu, "cyan"))
|
print(colored(menu, "cyan"))
|
||||||
try:
|
try:
|
||||||
choice = timed_input(
|
choice = timed_input(
|
||||||
"Enter your choice (1-7): ", inactivity_timeout
|
"Enter your choice (1-8): ", inactivity_timeout
|
||||||
).strip()
|
).strip()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
print(colored("Session timed out. Vault locked.", "yellow"))
|
print(colored("Session timed out. Vault locked.", "yellow"))
|
||||||
@@ -719,7 +741,7 @@ def display_menu(
|
|||||||
if not choice:
|
if not choice:
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
"No input detected. Please enter a number between 1 and 7.",
|
"No input detected. Please enter a number between 1 and 8.",
|
||||||
"yellow",
|
"yellow",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -766,14 +788,17 @@ def display_menu(
|
|||||||
password_manager.handle_search_entries()
|
password_manager.handle_search_entries()
|
||||||
elif choice == "4":
|
elif choice == "4":
|
||||||
password_manager.update_activity()
|
password_manager.update_activity()
|
||||||
password_manager.handle_modify_entry()
|
password_manager.handle_list_entries()
|
||||||
elif choice == "5":
|
elif choice == "5":
|
||||||
password_manager.update_activity()
|
password_manager.update_activity()
|
||||||
password_manager.handle_display_totp_codes()
|
password_manager.handle_modify_entry()
|
||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
password_manager.update_activity()
|
password_manager.update_activity()
|
||||||
handle_settings(password_manager)
|
password_manager.handle_display_totp_codes()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
|
password_manager.update_activity()
|
||||||
|
handle_settings(password_manager)
|
||||||
|
elif choice == "8":
|
||||||
logging.info("Exiting the program.")
|
logging.info("Exiting the program.")
|
||||||
print(colored("Exiting the program.", "green"))
|
print(colored("Exiting the program.", "green"))
|
||||||
password_manager.nostr_client.close_client_pool()
|
password_manager.nostr_client.close_client_pool()
|
||||||
@@ -831,7 +856,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.command == "search":
|
if args.command == "search":
|
||||||
matches = password_manager.entry_manager.search_entries(args.query)
|
matches = password_manager.entry_manager.search_entries(args.query)
|
||||||
if matches:
|
if matches:
|
||||||
print_matches(matches)
|
print_matches(password_manager, matches)
|
||||||
else:
|
else:
|
||||||
print(colored("No matching entries found.", "yellow"))
|
print(colored("No matching entries found.", "yellow"))
|
||||||
return 0
|
return 0
|
||||||
@@ -841,7 +866,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if not matches:
|
if not matches:
|
||||||
print(colored("No matching entries found.", "yellow"))
|
print(colored("No matching entries found.", "yellow"))
|
||||||
else:
|
else:
|
||||||
print_matches(matches)
|
print_matches(password_manager, matches)
|
||||||
return 1
|
return 1
|
||||||
idx = matches[0][0]
|
idx = matches[0][0]
|
||||||
entry = password_manager.entry_manager.retrieve_entry(idx)
|
entry = password_manager.entry_manager.retrieve_entry(idx)
|
||||||
@@ -858,7 +883,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if not matches:
|
if not matches:
|
||||||
print(colored("No matching entries found.", "yellow"))
|
print(colored("No matching entries found.", "yellow"))
|
||||||
else:
|
else:
|
||||||
print_matches(matches)
|
print_matches(password_manager, matches)
|
||||||
return 1
|
return 1
|
||||||
idx = matches[0][0]
|
idx = matches[0][0]
|
||||||
entry = password_manager.entry_manager.retrieve_entry(idx)
|
entry = password_manager.entry_manager.retrieve_entry(idx)
|
||||||
|
@@ -65,6 +65,13 @@ class EntryManager:
|
|||||||
if "kind" not in entry:
|
if "kind" not in entry:
|
||||||
entry["kind"] = entry.get("type", EntryType.PASSWORD.value)
|
entry["kind"] = entry.get("type", EntryType.PASSWORD.value)
|
||||||
entry.setdefault("type", entry["kind"])
|
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.")
|
logger.debug("Index loaded successfully.")
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -106,7 +113,7 @@ class EntryManager:
|
|||||||
|
|
||||||
def add_entry(
|
def add_entry(
|
||||||
self,
|
self,
|
||||||
website_name: str,
|
label: str,
|
||||||
length: int,
|
length: int,
|
||||||
username: Optional[str] = None,
|
username: Optional[str] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
@@ -117,7 +124,7 @@ class EntryManager:
|
|||||||
"""
|
"""
|
||||||
Adds a new entry to the encrypted JSON index file.
|
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 length: The desired length of the password.
|
||||||
:param username: (Optional) The username associated with the website.
|
:param username: (Optional) The username associated with the website.
|
||||||
:param url: (Optional) The URL of the website.
|
:param url: (Optional) The URL of the website.
|
||||||
@@ -131,7 +138,7 @@ class EntryManager:
|
|||||||
|
|
||||||
data.setdefault("entries", {})
|
data.setdefault("entries", {})
|
||||||
data["entries"][str(index)] = {
|
data["entries"][str(index)] = {
|
||||||
"website": website_name,
|
"label": label,
|
||||||
"length": length,
|
"length": length,
|
||||||
"username": username if username else "",
|
"username": username if username else "",
|
||||||
"url": url if url else "",
|
"url": url if url else "",
|
||||||
@@ -222,7 +229,11 @@ class EntryManager:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def add_ssh_key(
|
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:
|
) -> int:
|
||||||
"""Add a new SSH key pair entry.
|
"""Add a new SSH key pair entry.
|
||||||
|
|
||||||
@@ -240,6 +251,7 @@ class EntryManager:
|
|||||||
"type": EntryType.SSH.value,
|
"type": EntryType.SSH.value,
|
||||||
"kind": EntryType.SSH.value,
|
"kind": EntryType.SSH.value,
|
||||||
"index": index,
|
"index": index,
|
||||||
|
"label": label,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
}
|
}
|
||||||
self._save_index(data)
|
self._save_index(data)
|
||||||
@@ -263,6 +275,7 @@ class EntryManager:
|
|||||||
|
|
||||||
def add_pgp_key(
|
def add_pgp_key(
|
||||||
self,
|
self,
|
||||||
|
label: str,
|
||||||
parent_seed: str,
|
parent_seed: str,
|
||||||
index: int | None = None,
|
index: int | None = None,
|
||||||
key_type: str = "ed25519",
|
key_type: str = "ed25519",
|
||||||
@@ -280,6 +293,7 @@ class EntryManager:
|
|||||||
"type": EntryType.PGP.value,
|
"type": EntryType.PGP.value,
|
||||||
"kind": EntryType.PGP.value,
|
"kind": EntryType.PGP.value,
|
||||||
"index": index,
|
"index": index,
|
||||||
|
"label": label,
|
||||||
"key_type": key_type,
|
"key_type": key_type,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
@@ -362,6 +376,7 @@ class EntryManager:
|
|||||||
|
|
||||||
def add_seed(
|
def add_seed(
|
||||||
self,
|
self,
|
||||||
|
label: str,
|
||||||
parent_seed: str,
|
parent_seed: str,
|
||||||
index: int | None = None,
|
index: int | None = None,
|
||||||
words_num: int = 24,
|
words_num: int = 24,
|
||||||
@@ -378,6 +393,7 @@ class EntryManager:
|
|||||||
"type": EntryType.SEED.value,
|
"type": EntryType.SEED.value,
|
||||||
"kind": EntryType.SEED.value,
|
"kind": EntryType.SEED.value,
|
||||||
"index": index,
|
"index": index,
|
||||||
|
"label": label,
|
||||||
"words": words_num,
|
"words": words_num,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
}
|
}
|
||||||
@@ -596,11 +612,11 @@ class EntryManager:
|
|||||||
idx_str, entry = item
|
idx_str, entry = item
|
||||||
if sort_by == "index":
|
if sort_by == "index":
|
||||||
return int(idx_str)
|
return int(idx_str)
|
||||||
if sort_by == "website":
|
if sort_by in {"website", "label"}:
|
||||||
return entry.get("website", "").lower()
|
return entry.get("label", entry.get("website", "")).lower()
|
||||||
if sort_by == "username":
|
if sort_by == "username":
|
||||||
return entry.get("username", "").lower()
|
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)
|
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]] = []
|
entries: List[Tuple[int, str, Optional[str], Optional[str], bool]] = []
|
||||||
for idx, entry in filtered_items:
|
for idx, entry in filtered_items:
|
||||||
|
label = entry.get("label", entry.get("website", ""))
|
||||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||||
if etype == EntryType.TOTP.value:
|
if etype == EntryType.PASSWORD.value:
|
||||||
entries.append((idx, entry.get("label", ""), None, None, False))
|
|
||||||
else:
|
|
||||||
entries.append(
|
entries.append(
|
||||||
(
|
(
|
||||||
idx,
|
idx,
|
||||||
entry.get("website", ""),
|
label,
|
||||||
entry.get("username", ""),
|
entry.get("username", ""),
|
||||||
entry.get("url", ""),
|
entry.get("url", ""),
|
||||||
entry.get("blacklisted", False),
|
entry.get("blacklisted", False),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
entries.append((idx, label, None, None, False))
|
||||||
|
|
||||||
logger.debug(f"Total entries found: {len(entries)}")
|
logger.debug(f"Total entries found: {len(entries)}")
|
||||||
for idx, entry in filtered_items:
|
for idx, entry in filtered_items:
|
||||||
@@ -644,8 +661,13 @@ class EntryManager:
|
|||||||
"cyan",
|
"cyan",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
elif etype == EntryType.PASSWORD.value:
|
||||||
print(colored(f" Website: {entry.get('website', '')}", "cyan"))
|
print(
|
||||||
|
colored(
|
||||||
|
f" Label: {entry.get('label', entry.get('website', ''))}",
|
||||||
|
"cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
colored(f" Username: {entry.get('username') or 'N/A'}", "cyan")
|
colored(f" Username: {entry.get('username') or 'N/A'}", "cyan")
|
||||||
)
|
)
|
||||||
@@ -656,6 +678,13 @@ class EntryManager:
|
|||||||
"cyan",
|
"cyan",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||||
|
)
|
||||||
|
)
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
@@ -680,16 +709,14 @@ class EntryManager:
|
|||||||
|
|
||||||
for idx, entry in sorted(entries_data.items(), key=lambda x: int(x[0])):
|
for idx, entry in sorted(entries_data.items(), key=lambda x: int(x[0])):
|
||||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||||
if etype == EntryType.TOTP.value:
|
label = entry.get("label", entry.get("website", ""))
|
||||||
label = entry.get("label", "")
|
notes = entry.get("notes", "")
|
||||||
notes = entry.get("notes", "")
|
label_match = query_lower in label.lower()
|
||||||
if query_lower in label.lower() or query_lower in notes.lower():
|
notes_match = query_lower in notes.lower()
|
||||||
results.append((int(idx), label, None, None, False))
|
|
||||||
else:
|
if etype == EntryType.PASSWORD.value:
|
||||||
website = entry.get("website", "")
|
|
||||||
username = entry.get("username", "")
|
username = entry.get("username", "")
|
||||||
url = entry.get("url", "")
|
url = entry.get("url", "")
|
||||||
notes = entry.get("notes", "")
|
|
||||||
custom_fields = entry.get("custom_fields", [])
|
custom_fields = entry.get("custom_fields", [])
|
||||||
custom_match = any(
|
custom_match = any(
|
||||||
query_lower in str(cf.get("label", "")).lower()
|
query_lower in str(cf.get("label", "")).lower()
|
||||||
@@ -697,21 +724,24 @@ class EntryManager:
|
|||||||
for cf in custom_fields
|
for cf in custom_fields
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
query_lower in website.lower()
|
label_match
|
||||||
or query_lower in username.lower()
|
or query_lower in username.lower()
|
||||||
or query_lower in url.lower()
|
or query_lower in url.lower()
|
||||||
or query_lower in notes.lower()
|
or notes_match
|
||||||
or custom_match
|
or custom_match
|
||||||
):
|
):
|
||||||
results.append(
|
results.append(
|
||||||
(
|
(
|
||||||
int(idx),
|
int(idx),
|
||||||
website,
|
label,
|
||||||
username,
|
username,
|
||||||
url,
|
url,
|
||||||
entry.get("blacklisted", False),
|
entry.get("blacklisted", False),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
if label_match or notes_match:
|
||||||
|
results.append((int(idx), label, None, None, False))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -829,7 +859,7 @@ class EntryManager:
|
|||||||
for entry in entries:
|
for entry in entries:
|
||||||
index, website, username, url, blacklisted = entry
|
index, website, username, url, blacklisted = entry
|
||||||
print(colored(f"Index: {index}", "cyan"))
|
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" Username: {username or 'N/A'}", "cyan"))
|
||||||
print(colored(f" URL: {url or 'N/A'}", "cyan"))
|
print(colored(f" URL: {url or 'N/A'}", "cyan"))
|
||||||
print(
|
print(
|
||||||
@@ -841,3 +871,29 @@ class EntryManager:
|
|||||||
logger.error(f"Failed to list all entries: {e}", exc_info=True)
|
logger.error(f"Failed to list all entries: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to list all entries: {e}", "red"))
|
print(colored(f"Error: Failed to list all entries: {e}", "red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_entry_summaries(
|
||||||
|
self, filter_kind: str | None = None
|
||||||
|
) -> list[tuple[int, str, str]]:
|
||||||
|
"""Return a list of entry index, type, and display labels."""
|
||||||
|
try:
|
||||||
|
data = self.vault.load_index()
|
||||||
|
entries_data = data.get("entries", {})
|
||||||
|
|
||||||
|
summaries: list[tuple[int, str, str]] = []
|
||||||
|
for idx_str, entry in entries_data.items():
|
||||||
|
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||||
|
if filter_kind and etype != filter_kind:
|
||||||
|
continue
|
||||||
|
if etype == EntryType.PASSWORD.value:
|
||||||
|
label = entry.get("label", entry.get("website", ""))
|
||||||
|
else:
|
||||||
|
label = entry.get("label", etype)
|
||||||
|
summaries.append((int(idx_str), etype, label))
|
||||||
|
|
||||||
|
summaries.sort(key=lambda x: x[0])
|
||||||
|
return summaries
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get entry summaries: {e}", exc_info=True)
|
||||||
|
print(colored(f"Error: Failed to get entry summaries: {e}", "red"))
|
||||||
|
return []
|
||||||
|
@@ -19,6 +19,7 @@ from typing import Optional
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import select
|
import select
|
||||||
|
import builtins
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
|
||||||
from password_manager.encryption import EncryptionManager
|
from password_manager.encryption import EncryptionManager
|
||||||
@@ -859,9 +860,9 @@ class PasswordManager:
|
|||||||
|
|
||||||
def handle_add_password(self) -> None:
|
def handle_add_password(self) -> None:
|
||||||
try:
|
try:
|
||||||
website_name = input("Enter the website name: ").strip()
|
website_name = input("Enter the label or website name: ").strip()
|
||||||
if not website_name:
|
if not website_name:
|
||||||
print(colored("Error: Website name cannot be empty.", "red"))
|
print(colored("Error: Label cannot be empty.", "red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
username = input("Enter the username (optional): ").strip()
|
username = input("Enter the username (optional): ").strip()
|
||||||
@@ -1040,14 +1041,27 @@ class PasswordManager:
|
|||||||
def handle_add_ssh_key(self) -> None:
|
def handle_add_ssh_key(self) -> None:
|
||||||
"""Add an SSH key pair entry and display the derived keys."""
|
"""Add an SSH key pair entry and display the derived keys."""
|
||||||
try:
|
try:
|
||||||
|
label = input("Label: ").strip()
|
||||||
|
if not label:
|
||||||
|
print(colored("Error: Label cannot be empty.", "red"))
|
||||||
|
return
|
||||||
notes = input("Notes (optional): ").strip()
|
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(
|
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
|
||||||
index, self.parent_seed
|
index, self.parent_seed
|
||||||
)
|
)
|
||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
|
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying SSH keys reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("SSH key display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
|
|
||||||
print(colored(f"\n[+] SSH key entry added with ID {index}.\n", "green"))
|
print(colored(f"\n[+] SSH key entry added with ID {index}.\n", "green"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
print(colored("Public Key:", "cyan"))
|
print(colored("Public Key:", "cyan"))
|
||||||
print(pub_pem)
|
print(pub_pem)
|
||||||
print(colored("Private Key:", "cyan"))
|
print(colored("Private Key:", "cyan"))
|
||||||
@@ -1066,6 +1080,10 @@ class PasswordManager:
|
|||||||
def handle_add_seed(self) -> None:
|
def handle_add_seed(self) -> None:
|
||||||
"""Add a derived BIP-39 seed phrase entry."""
|
"""Add a derived BIP-39 seed phrase entry."""
|
||||||
try:
|
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()
|
words_input = input("Word count (12 or 24, default 24): ").strip()
|
||||||
notes = input("Notes (optional): ").strip()
|
notes = input("Notes (optional): ").strip()
|
||||||
if words_input and words_input not in {"12", "24"}:
|
if words_input and words_input not in {"12", "24"}:
|
||||||
@@ -1073,14 +1091,27 @@ class PasswordManager:
|
|||||||
return
|
return
|
||||||
words = int(words_input) if words_input else 24
|
words = int(words_input) if words_input else 24
|
||||||
index = self.entry_manager.add_seed(
|
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)
|
phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed)
|
||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
|
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying the seed phrase reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("Seed phrase display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
|
|
||||||
print(colored(f"\n[+] Seed entry added with ID {index}.\n", "green"))
|
print(colored(f"\n[+] Seed entry added with ID {index}.\n", "green"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
print(colored("Seed Phrase:", "cyan"))
|
print(colored("Seed Phrase:", "cyan"))
|
||||||
print(colored(phrase, "yellow"))
|
print(colored(phrase, "yellow"))
|
||||||
|
if confirm_action("Show Compact Seed QR? (Y/N): "):
|
||||||
|
from password_manager.seedqr import encode_seedqr
|
||||||
|
|
||||||
|
TotpManager.print_qr_code(encode_seedqr(phrase))
|
||||||
try:
|
try:
|
||||||
self.sync_vault()
|
self.sync_vault()
|
||||||
except Exception as nostr_error:
|
except Exception as nostr_error:
|
||||||
@@ -1095,6 +1126,10 @@ class PasswordManager:
|
|||||||
def handle_add_pgp(self) -> None:
|
def handle_add_pgp(self) -> None:
|
||||||
"""Add a PGP key entry and display the generated key."""
|
"""Add a PGP key entry and display the generated key."""
|
||||||
try:
|
try:
|
||||||
|
label = input("Label: ").strip()
|
||||||
|
if not label:
|
||||||
|
print(colored("Error: Label cannot be empty.", "red"))
|
||||||
|
return
|
||||||
key_type = (
|
key_type = (
|
||||||
input("Key type (ed25519 or rsa, default ed25519): ").strip().lower()
|
input("Key type (ed25519 or rsa, default ed25519): ").strip().lower()
|
||||||
or "ed25519"
|
or "ed25519"
|
||||||
@@ -1102,6 +1137,7 @@ class PasswordManager:
|
|||||||
user_id = input("User ID (optional): ").strip()
|
user_id = input("User ID (optional): ").strip()
|
||||||
notes = input("Notes (optional): ").strip()
|
notes = input("Notes (optional): ").strip()
|
||||||
index = self.entry_manager.add_pgp_key(
|
index = self.entry_manager.add_pgp_key(
|
||||||
|
label,
|
||||||
self.parent_seed,
|
self.parent_seed,
|
||||||
key_type=key_type,
|
key_type=key_type,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -1112,7 +1148,18 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
|
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying the PGP key reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("PGP key display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
|
|
||||||
print(colored(f"\n[+] PGP key entry added with ID {index}.\n", "green"))
|
print(colored(f"\n[+] PGP key entry added with ID {index}.\n", "green"))
|
||||||
|
if user_id:
|
||||||
|
print(colored(f"User ID: {user_id}", "cyan"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
|
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
|
||||||
print(priv_key)
|
print(priv_key)
|
||||||
try:
|
try:
|
||||||
@@ -1129,7 +1176,10 @@ class PasswordManager:
|
|||||||
def handle_add_nostr_key(self) -> None:
|
def handle_add_nostr_key(self) -> None:
|
||||||
"""Add a Nostr key entry and display the derived keys."""
|
"""Add a Nostr key entry and display the derived keys."""
|
||||||
try:
|
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()
|
notes = input("Notes (optional): ").strip()
|
||||||
index = self.entry_manager.add_nostr_key(label, notes=notes)
|
index = self.entry_manager.add_nostr_key(label, notes=notes)
|
||||||
npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
|
npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
|
||||||
@@ -1147,6 +1197,12 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(colored(f"nsec: {nsec}", "cyan"))
|
print(colored(f"nsec: {nsec}", "cyan"))
|
||||||
|
if confirm_action("Show QR code for npub? (Y/N): "):
|
||||||
|
TotpManager.print_qr_code(f"nostr:{npub}")
|
||||||
|
if confirm_action(
|
||||||
|
"WARNING: Displaying the nsec QR reveals your private key. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
TotpManager.print_qr_code(nsec)
|
||||||
try:
|
try:
|
||||||
self.sync_vault()
|
self.sync_vault()
|
||||||
except Exception as nostr_error: # pragma: no cover - best effort
|
except Exception as nostr_error: # pragma: no cover - best effort
|
||||||
@@ -1158,6 +1214,26 @@ class PasswordManager:
|
|||||||
logging.error(f"Error during Nostr key setup: {e}", exc_info=True)
|
logging.error(f"Error during Nostr key setup: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to add Nostr key: {e}", "red"))
|
print(colored(f"Error: Failed to add Nostr key: {e}", "red"))
|
||||||
|
|
||||||
|
def show_entry_details_by_index(self, index: int) -> None:
|
||||||
|
"""Display entry details using :meth:`handle_retrieve_entry` for the
|
||||||
|
given index without prompting for it again."""
|
||||||
|
|
||||||
|
original_input = builtins.input
|
||||||
|
first_call = True
|
||||||
|
|
||||||
|
def patched_input(prompt: str = "") -> str:
|
||||||
|
nonlocal first_call
|
||||||
|
if first_call:
|
||||||
|
first_call = False
|
||||||
|
return str(index)
|
||||||
|
return original_input(prompt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
builtins.input = patched_input
|
||||||
|
self.handle_retrieve_entry()
|
||||||
|
finally:
|
||||||
|
builtins.input = original_input
|
||||||
|
|
||||||
def handle_retrieve_entry(self) -> None:
|
def handle_retrieve_entry(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handles retrieving a password from the index by prompting the user for the index number
|
Handles retrieving a password from the index by prompting the user for the index number
|
||||||
@@ -1232,10 +1308,23 @@ class PasswordManager:
|
|||||||
return
|
return
|
||||||
if entry_type == EntryType.SSH.value:
|
if entry_type == EntryType.SSH.value:
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
|
label = entry.get("label", "")
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying SSH keys reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("SSH key display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
|
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
|
||||||
index, self.parent_seed
|
index, self.parent_seed
|
||||||
)
|
)
|
||||||
|
print(colored("\n[+] Retrieved SSH Key Pair:\n", "green"))
|
||||||
|
if label:
|
||||||
|
print(colored(f"Label: {label}", "cyan"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
|
print(colored("Public Key:", "cyan"))
|
||||||
|
print(pub_pem)
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
copy_to_clipboard(priv_pem, self.clipboard_clear_delay)
|
copy_to_clipboard(priv_pem, self.clipboard_clear_delay)
|
||||||
print(
|
print(
|
||||||
@@ -1244,24 +1333,28 @@ class PasswordManager:
|
|||||||
"green",
|
"green",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(colored("Public Key:", "cyan"))
|
|
||||||
print(pub_pem)
|
|
||||||
else:
|
else:
|
||||||
print(colored("\n[+] Retrieved SSH Key Pair:\n", "green"))
|
|
||||||
print(colored("Public Key:", "cyan"))
|
|
||||||
print(pub_pem)
|
|
||||||
print(colored("Private Key:", "cyan"))
|
print(colored("Private Key:", "cyan"))
|
||||||
print(priv_pem)
|
print(priv_pem)
|
||||||
if notes:
|
|
||||||
print(colored(f"Notes: {notes}", "cyan"))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deriving SSH key pair: {e}", exc_info=True)
|
logging.error(f"Error deriving SSH key pair: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to derive SSH keys: {e}", "red"))
|
print(colored(f"Error: Failed to derive SSH keys: {e}", "red"))
|
||||||
return
|
return
|
||||||
if entry_type == EntryType.SEED.value:
|
if entry_type == EntryType.SEED.value:
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
|
label = entry.get("label", "")
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying the seed phrase reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("Seed phrase display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed)
|
phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed)
|
||||||
|
print(colored("\n[+] Retrieved Seed Phrase:\n", "green"))
|
||||||
|
if label:
|
||||||
|
print(colored(f"Label: {label}", "cyan"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
copy_to_clipboard(phrase, self.clipboard_clear_delay)
|
copy_to_clipboard(phrase, self.clipboard_clear_delay)
|
||||||
print(
|
print(
|
||||||
@@ -1271,9 +1364,8 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(colored("\n[+] Retrieved Seed Phrase:\n", "green"))
|
|
||||||
print(colored(phrase, "yellow"))
|
print(colored(phrase, "yellow"))
|
||||||
if confirm_action("Show SeedQR? (Y/N): "):
|
if confirm_action("Show Compact Seed QR? (Y/N): "):
|
||||||
from password_manager.seedqr import encode_seedqr
|
from password_manager.seedqr import encode_seedqr
|
||||||
|
|
||||||
TotpManager.print_qr_code(encode_seedqr(phrase))
|
TotpManager.print_qr_code(encode_seedqr(phrase))
|
||||||
@@ -1300,10 +1392,22 @@ class PasswordManager:
|
|||||||
return
|
return
|
||||||
if entry_type == EntryType.PGP.value:
|
if entry_type == EntryType.PGP.value:
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
|
label = entry.get("user_id", "")
|
||||||
|
if not confirm_action(
|
||||||
|
"WARNING: Displaying the PGP key reveals sensitive information. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
print(colored("PGP key display cancelled.", "yellow"))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
priv_key, fingerprint = self.entry_manager.get_pgp_key(
|
priv_key, fingerprint = self.entry_manager.get_pgp_key(
|
||||||
index, self.parent_seed
|
index, self.parent_seed
|
||||||
)
|
)
|
||||||
|
print(colored("\n[+] Retrieved PGP Key:\n", "green"))
|
||||||
|
if label:
|
||||||
|
print(colored(f"User ID: {label}", "cyan"))
|
||||||
|
if notes:
|
||||||
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
|
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
copy_to_clipboard(priv_key, self.clipboard_clear_delay)
|
copy_to_clipboard(priv_key, self.clipboard_clear_delay)
|
||||||
print(
|
print(
|
||||||
@@ -1313,11 +1417,7 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(colored("\n[+] Retrieved PGP Key:\n", "green"))
|
|
||||||
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
|
|
||||||
print(priv_key)
|
print(priv_key)
|
||||||
if notes:
|
|
||||||
print(colored(f"Notes: {notes}", "cyan"))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deriving PGP key: {e}", exc_info=True)
|
logging.error(f"Error deriving PGP key: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to derive PGP key: {e}", "red"))
|
print(colored(f"Error: Failed to derive PGP key: {e}", "red"))
|
||||||
@@ -1342,6 +1442,12 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(colored(f"nsec: {nsec}", "cyan"))
|
print(colored(f"nsec: {nsec}", "cyan"))
|
||||||
|
if confirm_action("Show QR code for npub? (Y/N): "):
|
||||||
|
TotpManager.print_qr_code(f"nostr:{npub}")
|
||||||
|
if confirm_action(
|
||||||
|
"WARNING: Displaying the nsec QR reveals your private key. Continue? (Y/N): "
|
||||||
|
):
|
||||||
|
TotpManager.print_qr_code(nsec)
|
||||||
if notes:
|
if notes:
|
||||||
print(colored(f"Notes: {notes}", "cyan"))
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1682,21 +1788,134 @@ class PasswordManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
print(colored("\n[+] Search Results:\n", "green"))
|
print(colored("\n[+] Search Results:\n", "green"))
|
||||||
for entry in results:
|
for match in results:
|
||||||
index, website, username, url, blacklisted = entry
|
self.display_entry_details(match[0])
|
||||||
print(colored(f"Index: {index}", "cyan"))
|
|
||||||
print(colored(f" Website: {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")
|
|
||||||
)
|
|
||||||
print("-" * 40)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to search entries: {e}", exc_info=True)
|
logging.error(f"Failed to search entries: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to search entries: {e}", "red"))
|
print(colored(f"Error: Failed to search entries: {e}", "red"))
|
||||||
|
|
||||||
|
def display_entry_details(self, index: int) -> None:
|
||||||
|
"""Print detailed information for a single entry."""
|
||||||
|
entry = self.entry_manager.retrieve_entry(index)
|
||||||
|
if not entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||||
|
print(colored(f"Index: {index}", "cyan"))
|
||||||
|
if etype == EntryType.TOTP.value:
|
||||||
|
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
|
||||||
|
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f" Period: {entry.get('period', 30)}s Digits: {entry.get('digits', 6)}",
|
||||||
|
"cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
notes = entry.get("notes", "")
|
||||||
|
if notes:
|
||||||
|
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", "")
|
||||||
|
if notes:
|
||||||
|
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:
|
||||||
|
print(colored(f" User ID: {uid}", "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.NOSTR.value:
|
||||||
|
print(colored(" Type: Nostr 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"))
|
||||||
|
else:
|
||||||
|
website = entry.get("label", entry.get("website", ""))
|
||||||
|
username = entry.get("username", "")
|
||||||
|
url = entry.get("url", "")
|
||||||
|
blacklisted = entry.get("blacklisted", False)
|
||||||
|
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"))
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
def handle_list_entries(self) -> None:
|
||||||
|
"""List entries and optionally show details."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
print("\nList Entries:")
|
||||||
|
print("1. All")
|
||||||
|
print("2. Passwords")
|
||||||
|
print("3. 2FA (TOTP)")
|
||||||
|
print("4. SSH Key")
|
||||||
|
print("5. Seed Phrase")
|
||||||
|
print("6. Nostr Key Pair")
|
||||||
|
print("7. PGP")
|
||||||
|
print("8. Back")
|
||||||
|
choice = input("Select entry type: ").strip()
|
||||||
|
if choice == "1":
|
||||||
|
filter_kind = None
|
||||||
|
elif choice == "2":
|
||||||
|
filter_kind = EntryType.PASSWORD.value
|
||||||
|
elif choice == "3":
|
||||||
|
filter_kind = EntryType.TOTP.value
|
||||||
|
elif choice == "4":
|
||||||
|
filter_kind = EntryType.SSH.value
|
||||||
|
elif choice == "5":
|
||||||
|
filter_kind = EntryType.SEED.value
|
||||||
|
elif choice == "6":
|
||||||
|
filter_kind = EntryType.NOSTR.value
|
||||||
|
elif choice == "7":
|
||||||
|
filter_kind = EntryType.PGP.value
|
||||||
|
elif choice == "8":
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(colored("Invalid choice.", "red"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
summaries = self.entry_manager.get_entry_summaries(filter_kind)
|
||||||
|
if not summaries:
|
||||||
|
continue
|
||||||
|
while True:
|
||||||
|
print(colored("\n[+] Entries:\n", "green"))
|
||||||
|
for idx, etype, label in summaries:
|
||||||
|
if filter_kind is None:
|
||||||
|
display_type = etype.capitalize()
|
||||||
|
print(colored(f"{idx}. {display_type} - {label}", "cyan"))
|
||||||
|
else:
|
||||||
|
print(colored(f"{idx}. {label}", "cyan"))
|
||||||
|
idx_input = input(
|
||||||
|
"Enter index to view details or press Enter to go back: "
|
||||||
|
).strip()
|
||||||
|
if not idx_input:
|
||||||
|
break
|
||||||
|
if not idx_input.isdigit():
|
||||||
|
print(colored("Invalid index.", "red"))
|
||||||
|
continue
|
||||||
|
self.show_entry_details_by_index(int(idx_input))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to list entries: {e}", exc_info=True)
|
||||||
|
print(colored(f"Error: Failed to list entries: {e}", "red"))
|
||||||
|
|
||||||
def delete_entry(self) -> None:
|
def delete_entry(self) -> None:
|
||||||
"""Deletes an entry from the password index."""
|
"""Deletes an entry from the password index."""
|
||||||
try:
|
try:
|
||||||
|
@@ -33,6 +33,10 @@ def _v1_to_v2(data: dict) -> dict:
|
|||||||
for k, v in passwords.items():
|
for k, v in passwords.items():
|
||||||
v.setdefault("type", "password")
|
v.setdefault("type", "password")
|
||||||
v.setdefault("notes", "")
|
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
|
entries[k] = v
|
||||||
data["entries"] = entries
|
data["entries"] = entries
|
||||||
data["schema_version"] = 2
|
data["schema_version"] = 2
|
||||||
@@ -46,6 +50,10 @@ def _v2_to_v3(data: dict) -> dict:
|
|||||||
for entry in entries.values():
|
for entry in entries.values():
|
||||||
entry.setdefault("custom_fields", [])
|
entry.setdefault("custom_fields", [])
|
||||||
entry.setdefault("origin", "")
|
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
|
data["schema_version"] = 3
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@@ -31,7 +31,7 @@ def test_auto_sync_triggers_post(monkeypatch):
|
|||||||
called = True
|
called = True
|
||||||
|
|
||||||
monkeypatch.setattr(main, "handle_post_to_nostr", fake_post)
|
monkeypatch.setattr(main, "handle_post_to_nostr", fake_post)
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main.display_menu(pm, sync_interval=0.1)
|
main.display_menu(pm, sync_interval=0.1)
|
||||||
|
@@ -25,7 +25,7 @@ def test_backup_restore_workflow(monkeypatch):
|
|||||||
"schema_version": 3,
|
"schema_version": 3,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "a",
|
"label": "a",
|
||||||
"length": 10,
|
"length": 10,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"kind": "password",
|
"kind": "password",
|
||||||
@@ -48,7 +48,7 @@ def test_backup_restore_workflow(monkeypatch):
|
|||||||
"schema_version": 3,
|
"schema_version": 3,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "b",
|
"label": "b",
|
||||||
"length": 12,
|
"length": 12,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"kind": "password",
|
"kind": "password",
|
||||||
|
@@ -52,7 +52,7 @@ def _make_pm(called, locked=None):
|
|||||||
def test_empty_and_non_numeric_choice(monkeypatch, capsys):
|
def test_empty_and_non_numeric_choice(monkeypatch, capsys):
|
||||||
called = {"add": False, "retrieve": False, "modify": False}
|
called = {"add": False, "retrieve": False, "modify": False}
|
||||||
pm, _ = _make_pm(called)
|
pm, _ = _make_pm(called)
|
||||||
inputs = iter(["", "abc", "7"])
|
inputs = iter(["", "abc", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
||||||
@@ -65,7 +65,7 @@ def test_empty_and_non_numeric_choice(monkeypatch, capsys):
|
|||||||
def test_out_of_range_menu(monkeypatch, capsys):
|
def test_out_of_range_menu(monkeypatch, capsys):
|
||||||
called = {"add": False, "retrieve": False, "modify": False}
|
called = {"add": False, "retrieve": False, "modify": False}
|
||||||
pm, _ = _make_pm(called)
|
pm, _ = _make_pm(called)
|
||||||
inputs = iter(["9", "7"])
|
inputs = iter(["9", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
||||||
@@ -77,7 +77,7 @@ def test_out_of_range_menu(monkeypatch, capsys):
|
|||||||
def test_invalid_add_entry_submenu(monkeypatch, capsys):
|
def test_invalid_add_entry_submenu(monkeypatch, capsys):
|
||||||
called = {"add": False, "retrieve": False, "modify": False}
|
called = {"add": False, "retrieve": False, "modify": False}
|
||||||
pm, _ = _make_pm(called)
|
pm, _ = _make_pm(called)
|
||||||
inputs = iter(["1", "8", "7", "7"])
|
inputs = iter(["1", "8", "7", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
@@ -92,7 +92,7 @@ def test_inactivity_timeout_loop(monkeypatch, capsys):
|
|||||||
pm, locked = _make_pm(called)
|
pm, locked = _make_pm(called)
|
||||||
pm.last_activity = 0
|
pm.last_activity = 0
|
||||||
monkeypatch.setattr(time, "time", lambda: 100.0)
|
monkeypatch.setattr(time, "time", lambda: 100.0)
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
|
@@ -30,7 +30,7 @@ def test_add_and_retrieve_entry():
|
|||||||
entry = entry_mgr.retrieve_entry(index)
|
entry = entry_mgr.retrieve_entry(index)
|
||||||
|
|
||||||
assert entry == {
|
assert entry == {
|
||||||
"website": "example.com",
|
"label": "example.com",
|
||||||
"length": 12,
|
"length": 12,
|
||||||
"username": "user",
|
"username": "user",
|
||||||
"url": "",
|
"url": "",
|
||||||
@@ -69,9 +69,9 @@ def test_round_trip_entry_types(method, expected_type):
|
|||||||
index = 0
|
index = 0
|
||||||
else:
|
else:
|
||||||
if method == "add_ssh_key":
|
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":
|
elif method == "add_seed":
|
||||||
index = entry_mgr.add_seed(TEST_SEED)
|
index = entry_mgr.add_seed("seed", TEST_SEED)
|
||||||
else:
|
else:
|
||||||
index = getattr(entry_mgr, method)()
|
index = getattr(entry_mgr, method)()
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ def test_inactivity_triggers_lock(monkeypatch):
|
|||||||
unlock_vault=unlock_vault,
|
unlock_vault=unlock_vault,
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
||||||
@@ -72,7 +72,7 @@ def test_input_timeout_triggers_lock(monkeypatch):
|
|||||||
unlock_vault=unlock_vault,
|
unlock_vault=unlock_vault,
|
||||||
)
|
)
|
||||||
|
|
||||||
responses = iter([TimeoutError(), "7"])
|
responses = iter([TimeoutError(), "8"])
|
||||||
|
|
||||||
def fake_input(*_args, **_kwargs):
|
def fake_input(*_args, **_kwargs):
|
||||||
val = next(responses)
|
val = next(responses)
|
||||||
|
@@ -34,7 +34,7 @@ def test_index_export_import_round_trip():
|
|||||||
"schema_version": 3,
|
"schema_version": 3,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "example",
|
"label": "example",
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"custom_fields": [],
|
"custom_fields": [],
|
||||||
@@ -52,7 +52,7 @@ def test_index_export_import_round_trip():
|
|||||||
"schema_version": 3,
|
"schema_version": 3,
|
||||||
"entries": {
|
"entries": {
|
||||||
"0": {
|
"0": {
|
||||||
"website": "changed",
|
"label": "changed",
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"custom_fields": [],
|
"custom_fields": [],
|
||||||
|
84
src/tests/test_manager_list_entries.py
Normal file
84
src/tests/test_manager_list_entries.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_list_entries(monkeypatch, capsys):
|
||||||
|
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
|
||||||
|
|
||||||
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
entry_mgr.add_entry("example.com", 12)
|
||||||
|
|
||||||
|
inputs = iter(["1", ""]) # list all, then exit
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
||||||
|
|
||||||
|
pm.handle_list_entries()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Example" in out
|
||||||
|
assert "example.com" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_entries_show_details(monkeypatch, capsys):
|
||||||
|
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 = SimpleNamespace()
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(sys.stdin, "readline", lambda *a, **k: "b\n")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.select.select",
|
||||||
|
lambda *a, **k: ([sys.stdin], [], []),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
41
src/tests/test_manager_search_display.py
Normal file
41
src/tests/test_manager_search_display.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_entries_shows_totp_details(monkeypatch, capsys):
|
||||||
|
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
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: "Example")
|
||||||
|
|
||||||
|
pm.handle_search_entries()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Label: Example" in out
|
||||||
|
assert "Derivation Index" in out
|
@@ -30,7 +30,7 @@ def _make_pm(calls):
|
|||||||
def test_menu_totp_option(monkeypatch):
|
def test_menu_totp_option(monkeypatch):
|
||||||
calls = []
|
calls = []
|
||||||
pm = _make_pm(calls)
|
pm = _make_pm(calls)
|
||||||
inputs = iter(["5", "7"])
|
inputs = iter(["6", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
monkeypatch.setattr(main, "handle_settings", lambda *_: None)
|
monkeypatch.setattr(main, "handle_settings", lambda *_: None)
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
@@ -41,7 +41,7 @@ def test_menu_totp_option(monkeypatch):
|
|||||||
def test_menu_settings_option(monkeypatch):
|
def test_menu_settings_option(monkeypatch):
|
||||||
calls = []
|
calls = []
|
||||||
pm = _make_pm(calls)
|
pm = _make_pm(calls)
|
||||||
inputs = iter(["6", "7"])
|
inputs = iter(["7", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
monkeypatch.setattr(main, "handle_settings", lambda *_: calls.append("settings"))
|
monkeypatch.setattr(main, "handle_settings", lambda *_: calls.append("settings"))
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
|
@@ -30,7 +30,7 @@ def _make_pm(called):
|
|||||||
def test_menu_search_option(monkeypatch):
|
def test_menu_search_option(monkeypatch):
|
||||||
called = []
|
called = []
|
||||||
pm = _make_pm(called)
|
pm = _make_pm(called)
|
||||||
inputs = iter(["3", "7"])
|
inputs = iter(["3", "8"])
|
||||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||||
monkeypatch.setattr("builtins.input", lambda *_: "query")
|
monkeypatch.setattr("builtins.input", lambda *_: "query")
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
|
@@ -20,7 +20,7 @@ def test_migrate_v0_to_v3(tmp_path: Path):
|
|||||||
data = vault.load_index()
|
data = vault.load_index()
|
||||||
assert data["schema_version"] == LATEST_VERSION
|
assert data["schema_version"] == LATEST_VERSION
|
||||||
expected_entry = {
|
expected_entry = {
|
||||||
"website": "a",
|
"label": "a",
|
||||||
"length": 8,
|
"length": 8,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
@@ -37,7 +37,7 @@ def test_migrate_v1_to_v3(tmp_path: Path):
|
|||||||
data = vault.load_index()
|
data = vault.load_index()
|
||||||
assert data["schema_version"] == LATEST_VERSION
|
assert data["schema_version"] == LATEST_VERSION
|
||||||
expected_entry = {
|
expected_entry = {
|
||||||
"website": "b",
|
"label": "b",
|
||||||
"length": 10,
|
"length": 10,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
@@ -59,7 +59,7 @@ def test_migrate_v2_to_v3(tmp_path: Path):
|
|||||||
data = vault.load_index()
|
data = vault.load_index()
|
||||||
assert data["schema_version"] == LATEST_VERSION
|
assert data["schema_version"] == LATEST_VERSION
|
||||||
expected_entry = {
|
expected_entry = {
|
||||||
"website": "c",
|
"label": "c",
|
||||||
"length": 5,
|
"length": 5,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
|
@@ -58,7 +58,7 @@ def test_nostr_index_size_limits(pytestconfig: pytest.Config):
|
|||||||
if max_entries is not None and entry_count >= max_entries:
|
if max_entries is not None and entry_count >= max_entries:
|
||||||
break
|
break
|
||||||
entry_mgr.add_entry(
|
entry_mgr.add_entry(
|
||||||
website_name=f"site-{entry_count + 1}",
|
label=f"site-{entry_count + 1}",
|
||||||
length=12,
|
length=12,
|
||||||
username="u" * size,
|
username="u" * size,
|
||||||
url="https://example.com/" + "a" * size,
|
url="https://example.com/" + "a" * size,
|
||||||
|
60
src/tests/test_nostr_qr.py
Normal file
60
src/tests/test_nostr_qr.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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, TotpManager
|
||||||
|
from password_manager.config_manager import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
|
class FakeNostrClient:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.published = []
|
||||||
|
|
||||||
|
def publish_snapshot(self, data: bytes):
|
||||||
|
self.published.append(data)
|
||||||
|
return None, "abcd"
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_qr_for_nostr_keys(monkeypatch):
|
||||||
|
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)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: str(idx))
|
||||||
|
responses = iter([True, False])
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.confirm_action",
|
||||||
|
lambda *_a, **_k: next(responses),
|
||||||
|
)
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.TotpManager.print_qr_code",
|
||||||
|
lambda data: called.append(data),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.handle_retrieve_entry()
|
||||||
|
assert called == [f"nostr:{npub}"]
|
@@ -19,7 +19,9 @@ def test_pgp_key_determinism():
|
|||||||
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
entry_mgr = EntryManager(vault, backup_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)
|
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||||
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||||
|
|
||||||
|
@@ -23,8 +23,8 @@ def test_seed_phrase_determinism():
|
|||||||
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
entry_mgr = EntryManager(vault, backup_mgr)
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
|
||||||
idx_12 = entry_mgr.add_seed(TEST_SEED, words_num=12)
|
idx_12 = entry_mgr.add_seed("seed12", TEST_SEED, words_num=12)
|
||||||
idx_24 = entry_mgr.add_seed(TEST_SEED, words_num=24)
|
idx_24 = entry_mgr.add_seed("seed24", TEST_SEED, words_num=24)
|
||||||
|
|
||||||
phrase12_a = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)
|
phrase12_a = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)
|
||||||
phrase12_b = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)
|
phrase12_b = entry_mgr.get_seed_phrase(idx_12, TEST_SEED)
|
||||||
|
@@ -20,9 +20,15 @@ def test_add_and_retrieve_ssh_key_pair():
|
|||||||
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
entry_mgr = EntryManager(vault, backup_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)
|
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)
|
priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
|
||||||
priv2, pub2 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
|
priv2, pub2 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
|
||||||
|
@@ -21,7 +21,7 @@ def test_ssh_private_key_corresponds_to_public():
|
|||||||
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
entry_mgr = EntryManager(vault, backup_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_pem, pub_pem = entry_mgr.get_ssh_key_pair(idx, TEST_SEED)
|
||||||
|
|
||||||
priv_key = serialization.load_pem_private_key(
|
priv_key = serialization.load_pem_private_key(
|
||||||
|
Reference in New Issue
Block a user