mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +00:00
@@ -331,6 +331,15 @@ When **Secret Mode** is enabled, SeedPass copies retrieved passwords directly to
|
|||||||
2. Choose how many seconds to keep passwords on the clipboard.
|
2. Choose how many seconds to keep passwords on the clipboard.
|
||||||
3. Retrieve an entry and SeedPass will confirm the password was copied.
|
3. Retrieve an entry and SeedPass will confirm the password was copied.
|
||||||
|
|
||||||
|
### Viewing Entry Details
|
||||||
|
|
||||||
|
Selecting an item from **List Entries** or **Search Entries** first displays the
|
||||||
|
entry's metadata such as the label, username, tags and notes. Passwords, seed
|
||||||
|
phrases and other sensitive fields remain hidden until you choose to reveal
|
||||||
|
them. When you opt to show the secret, the details view presents the same action
|
||||||
|
menu as **Retrieve Entry** so you can edit, archive or display QR codes for the
|
||||||
|
entry.
|
||||||
|
|
||||||
### Additional Entry Types
|
### Additional Entry Types
|
||||||
|
|
||||||
SeedPass supports storing more than just passwords and 2FA secrets. You can also create entries for:
|
SeedPass supports storing more than just passwords and 2FA secrets. You can also create entries for:
|
||||||
|
@@ -156,6 +156,14 @@ $ seedpass entry get "email"
|
|||||||
Code: 123456
|
Code: 123456
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Viewing Entry Details
|
||||||
|
|
||||||
|
Picking an entry from `entry list` or `entry search` displays its metadata first
|
||||||
|
so you can review the label, username and notes. Sensitive fields are hidden
|
||||||
|
until you confirm you want to reveal them. After showing the secret, the details
|
||||||
|
view offers the same actions as `entry get`—edit the entry, archive it or show
|
||||||
|
QR codes for supported types.
|
||||||
|
|
||||||
### `vault` Commands
|
### `vault` Commands
|
||||||
|
|
||||||
- **`seedpass vault export`** – Export the entire vault to an encrypted JSON file.
|
- **`seedpass vault export`** – Export the entire vault to an encrypted JSON file.
|
||||||
|
@@ -437,8 +437,8 @@ class EntryManager:
|
|||||||
"""Return the npub and nsec for the specified entry."""
|
"""Return the npub and nsec for the specified entry."""
|
||||||
|
|
||||||
entry = self.retrieve_entry(index)
|
entry = self.retrieve_entry(index)
|
||||||
etype = entry.get("type") if entry else None
|
etype = entry.get("type", "").lower() if entry else ""
|
||||||
kind = entry.get("kind") if entry else None
|
kind = entry.get("kind", "").lower() if entry else ""
|
||||||
if not entry or (
|
if not entry or (
|
||||||
etype != EntryType.NOSTR.value and kind != EntryType.NOSTR.value
|
etype != EntryType.NOSTR.value and kind != EntryType.NOSTR.value
|
||||||
):
|
):
|
||||||
|
@@ -153,6 +153,7 @@ class PasswordManager:
|
|||||||
self.profile_stack: list[tuple[str, Path, str]] = []
|
self.profile_stack: list[tuple[str, Path, str]] = []
|
||||||
self.last_unlock_duration: float | None = None
|
self.last_unlock_duration: float | None = None
|
||||||
self.verbose_timing: bool = False
|
self.verbose_timing: bool = False
|
||||||
|
self._suppress_entry_actions_menu: bool = False
|
||||||
|
|
||||||
# Initialize the fingerprint manager first
|
# Initialize the fingerprint manager first
|
||||||
self.initialize_fingerprint_manager()
|
self.initialize_fingerprint_manager()
|
||||||
@@ -1792,24 +1793,32 @@ class PasswordManager:
|
|||||||
pause()
|
pause()
|
||||||
|
|
||||||
def show_entry_details_by_index(self, index: int) -> None:
|
def show_entry_details_by_index(self, index: int) -> None:
|
||||||
"""Display entry details using :meth:`handle_retrieve_entry` for the
|
"""Display details for entry ``index`` and offer actions."""
|
||||||
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:
|
try:
|
||||||
builtins.input = patched_input
|
entry = self.entry_manager.retrieve_entry(index)
|
||||||
self.handle_retrieve_entry()
|
if not entry:
|
||||||
finally:
|
return
|
||||||
builtins.input = original_input
|
|
||||||
|
fp, parent_fp, child_fp = self.header_fingerprint_args
|
||||||
|
clear_header_with_notification(
|
||||||
|
self,
|
||||||
|
fp,
|
||||||
|
"Entry Details",
|
||||||
|
parent_fingerprint=parent_fp,
|
||||||
|
child_fingerprint=child_fp,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.display_entry_details(index)
|
||||||
|
|
||||||
|
if confirm_action("Show sensitive information? (y/N): "):
|
||||||
|
self.display_sensitive_entry_info(entry, index)
|
||||||
|
|
||||||
|
pause()
|
||||||
|
self._entry_actions_menu(index, entry)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to display entry details: {e}", exc_info=True)
|
||||||
|
print(colored(f"Error: Failed to display entry details: {e}", "red"))
|
||||||
|
pause()
|
||||||
|
|
||||||
def _prompt_toggle_archive(self, entry: dict, index: int) -> None:
|
def _prompt_toggle_archive(self, entry: dict, index: int) -> None:
|
||||||
"""Prompt the user to archive or restore ``entry`` based on its status."""
|
"""Prompt the user to archive or restore ``entry`` based on its status."""
|
||||||
@@ -1828,6 +1837,13 @@ class PasswordManager:
|
|||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
|
|
||||||
|
def _entry_type_str(self, entry: dict) -> str:
|
||||||
|
"""Return the entry type as a lowercase string."""
|
||||||
|
entry_type = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||||
|
if isinstance(entry_type, EntryType):
|
||||||
|
entry_type = entry_type.value
|
||||||
|
return str(entry_type).lower()
|
||||||
|
|
||||||
def _entry_actions_menu(self, index: int, entry: dict) -> None:
|
def _entry_actions_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Provide actions for a retrieved entry."""
|
"""Provide actions for a retrieved entry."""
|
||||||
while True:
|
while True:
|
||||||
@@ -1840,7 +1856,7 @@ class PasswordManager:
|
|||||||
child_fingerprint=child_fp,
|
child_fingerprint=child_fp,
|
||||||
)
|
)
|
||||||
archived = entry.get("archived", entry.get("blacklisted", False))
|
archived = entry.get("archived", entry.get("blacklisted", False))
|
||||||
entry_type = entry.get("type", EntryType.PASSWORD.value)
|
entry_type = self._entry_type_str(entry)
|
||||||
print(colored("\n[+] Entry Actions:", "green"))
|
print(colored("\n[+] Entry Actions:", "green"))
|
||||||
if archived:
|
if archived:
|
||||||
print(colored("U. Unarchive", "cyan"))
|
print(colored("U. Unarchive", "cyan"))
|
||||||
@@ -1916,13 +1932,14 @@ class PasswordManager:
|
|||||||
self._entry_edit_menu(index, entry)
|
self._entry_edit_menu(index, entry)
|
||||||
elif choice == "q":
|
elif choice == "q":
|
||||||
self._entry_qr_menu(index, entry)
|
self._entry_qr_menu(index, entry)
|
||||||
|
pause()
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice.", "red"))
|
print(colored("Invalid choice.", "red"))
|
||||||
entry = self.entry_manager.retrieve_entry(index) or entry
|
entry = self.entry_manager.retrieve_entry(index) or entry
|
||||||
|
|
||||||
def _entry_edit_menu(self, index: int, entry: dict) -> None:
|
def _entry_edit_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Sub-menu for editing common entry fields."""
|
"""Sub-menu for editing common entry fields."""
|
||||||
entry_type = entry.get("type", EntryType.PASSWORD.value)
|
entry_type = self._entry_type_str(entry)
|
||||||
while True:
|
while True:
|
||||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
fp, parent_fp, child_fp = self.header_fingerprint_args
|
||||||
clear_header_with_notification(
|
clear_header_with_notification(
|
||||||
@@ -1982,7 +1999,7 @@ class PasswordManager:
|
|||||||
def _entry_qr_menu(self, index: int, entry: dict) -> None:
|
def _entry_qr_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Display QR codes for the given ``entry``."""
|
"""Display QR codes for the given ``entry``."""
|
||||||
|
|
||||||
entry_type = entry.get("type")
|
entry_type = self._entry_type_str(entry)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if entry_type in {EntryType.SEED.value, EntryType.MANAGED_ACCOUNT.value}:
|
if entry_type in {EntryType.SEED.value, EntryType.MANAGED_ACCOUNT.value}:
|
||||||
@@ -1997,6 +2014,7 @@ class PasswordManager:
|
|||||||
from password_manager.seedqr import encode_seedqr
|
from password_manager.seedqr import encode_seedqr
|
||||||
|
|
||||||
TotpManager.print_qr_code(encode_seedqr(seed))
|
TotpManager.print_qr_code(encode_seedqr(seed))
|
||||||
|
pause()
|
||||||
return
|
return
|
||||||
|
|
||||||
if entry_type == EntryType.NOSTR.value:
|
if entry_type == EntryType.NOSTR.value:
|
||||||
@@ -2032,6 +2050,7 @@ class PasswordManager:
|
|||||||
TotpManager.print_qr_code(nsec)
|
TotpManager.print_qr_code(nsec)
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice.", "red"))
|
print(colored("Invalid choice.", "red"))
|
||||||
|
pause()
|
||||||
entry = self.entry_manager.retrieve_entry(index) or entry
|
entry = self.entry_manager.retrieve_entry(index) or entry
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2040,35 +2059,20 @@ class PasswordManager:
|
|||||||
logging.error(f"Error displaying QR menu: {e}", exc_info=True)
|
logging.error(f"Error displaying QR menu: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to display QR codes: {e}", "red"))
|
print(colored(f"Error: Failed to display QR codes: {e}", "red"))
|
||||||
|
|
||||||
def handle_retrieve_entry(self) -> None:
|
def display_sensitive_entry_info(self, entry: dict, index: int) -> None:
|
||||||
"""
|
"""Display information for a sensitive entry.
|
||||||
Handles retrieving a password from the index by prompting the user for the index number
|
|
||||||
and displaying the corresponding password and associated details.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
|
||||||
clear_header_with_notification(
|
|
||||||
self,
|
|
||||||
fp,
|
|
||||||
"Main Menu > Retrieve Entry",
|
|
||||||
parent_fingerprint=parent_fp,
|
|
||||||
child_fingerprint=child_fp,
|
|
||||||
)
|
|
||||||
index_input = input(
|
|
||||||
"Enter the index number of the entry to retrieve: "
|
|
||||||
).strip()
|
|
||||||
if not index_input.isdigit():
|
|
||||||
print(colored("Error: Index must be a number.", "red"))
|
|
||||||
pause()
|
|
||||||
return
|
|
||||||
index = int(index_input)
|
|
||||||
|
|
||||||
entry = self.entry_manager.retrieve_entry(index)
|
Parameters
|
||||||
if not entry:
|
----------
|
||||||
pause()
|
entry: dict
|
||||||
return
|
Entry data retrieved from the vault.
|
||||||
|
index: int
|
||||||
|
Index of the entry being displayed.
|
||||||
|
"""
|
||||||
|
|
||||||
entry_type = entry.get("type", EntryType.PASSWORD.value)
|
self._suppress_entry_actions_menu = False
|
||||||
|
|
||||||
|
entry_type = self._entry_type_str(entry)
|
||||||
|
|
||||||
if entry_type == EntryType.TOTP.value:
|
if entry_type == EntryType.TOTP.value:
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
@@ -2124,12 +2128,11 @@ class PasswordManager:
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if exit_loop:
|
if exit_loop:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e: # pragma: no cover - best effort
|
||||||
logging.error(f"Error generating TOTP code: {e}", exc_info=True)
|
logging.error(f"Error generating TOTP code: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to generate TOTP code: {e}", "red"))
|
print(colored(f"Error: Failed to generate TOTP code: {e}", "red"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
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", "")
|
label = entry.get("label", "")
|
||||||
@@ -2163,12 +2166,11 @@ class PasswordManager:
|
|||||||
else:
|
else:
|
||||||
print(colored("Private Key:", "cyan"))
|
print(colored("Private Key:", "cyan"))
|
||||||
print(color_text(priv_pem, "deterministic"))
|
print(color_text(priv_pem, "deterministic"))
|
||||||
except Exception as e:
|
except Exception as e: # pragma: no cover - best effort
|
||||||
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"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
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", "")
|
label = entry.get("label", "")
|
||||||
@@ -2198,7 +2200,6 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(color_text(phrase, "deterministic"))
|
print(color_text(phrase, "deterministic"))
|
||||||
# Removed QR code display prompt and output
|
|
||||||
if confirm_action("Show derived entropy as hex? (Y/N): "):
|
if confirm_action("Show derived entropy as hex? (Y/N): "):
|
||||||
from local_bip85.bip85 import BIP85
|
from local_bip85.bip85 import BIP85
|
||||||
from bip_utils import Bip39SeedGenerator
|
from bip_utils import Bip39SeedGenerator
|
||||||
@@ -2214,12 +2215,11 @@ class PasswordManager:
|
|||||||
words_len=words,
|
words_len=words,
|
||||||
)
|
)
|
||||||
print(color_text(f"Entropy: {entropy.hex()}", "deterministic"))
|
print(color_text(f"Entropy: {entropy.hex()}", "deterministic"))
|
||||||
except Exception as e:
|
except Exception as e: # pragma: no cover - best effort
|
||||||
logging.error(f"Error deriving seed phrase: {e}", exc_info=True)
|
logging.error(f"Error deriving seed phrase: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to derive seed phrase: {e}", "red"))
|
print(colored(f"Error: Failed to derive seed phrase: {e}", "red"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
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", "")
|
label = entry.get("user_id", "")
|
||||||
@@ -2251,12 +2251,11 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(color_text(priv_key, "deterministic"))
|
print(color_text(priv_key, "deterministic"))
|
||||||
except Exception as e:
|
except Exception as e: # pragma: no cover - best effort
|
||||||
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"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if entry_type == EntryType.NOSTR.value:
|
if entry_type == EntryType.NOSTR.value:
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
@@ -2277,17 +2276,14 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(color_text(f"nsec: {nsec}", "deterministic"))
|
print(color_text(f"nsec: {nsec}", "deterministic"))
|
||||||
# QR code display removed for npub and nsec
|
|
||||||
if notes:
|
if notes:
|
||||||
print(colored(f"Notes: {notes}", "cyan"))
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
tags = entry.get("tags", [])
|
tags = entry.get("tags", [])
|
||||||
if tags:
|
if tags:
|
||||||
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
||||||
except Exception as e:
|
except Exception as e: # pragma: no cover - best effort
|
||||||
logging.error(f"Error deriving Nostr keys: {e}", exc_info=True)
|
logging.error(f"Error deriving Nostr keys: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to derive Nostr keys: {e}", "red"))
|
print(colored(f"Error: Failed to derive Nostr keys: {e}", "red"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if entry_type == EntryType.KEY_VALUE.value:
|
if entry_type == EntryType.KEY_VALUE.value:
|
||||||
@@ -2303,8 +2299,7 @@ class PasswordManager:
|
|||||||
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
f"Archived Status: {'Archived' if archived else 'Active'}",
|
f"Archived Status: {'Archived' if archived else 'Active'}", "cyan"
|
||||||
"cyan",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
@@ -2335,9 +2330,7 @@ class PasswordManager:
|
|||||||
if show == "y":
|
if show == "y":
|
||||||
for f_label, f_value in hidden_fields:
|
for f_label, f_value in hidden_fields:
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
copy_to_clipboard(
|
copy_to_clipboard(f_value, self.clipboard_clear_delay)
|
||||||
f_value, self.clipboard_clear_delay
|
|
||||||
)
|
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
f"[+] {f_label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
f"[+] {f_label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||||
@@ -2346,9 +2339,8 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(colored(f" {f_label}: {f_value}", "cyan"))
|
print(colored(f" {f_label}: {f_value}", "cyan"))
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if entry_type == EntryType.MANAGED_ACCOUNT.value:
|
if entry_type == EntryType.MANAGED_ACCOUNT.value:
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
@@ -2364,8 +2356,7 @@ class PasswordManager:
|
|||||||
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
f"Archived Status: {'Archived' if archived else 'Active'}",
|
f"Archived Status: {'Archived' if archived else 'Active'}", "cyan"
|
||||||
"cyan",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
action = (
|
action = (
|
||||||
@@ -2389,17 +2380,14 @@ class PasswordManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(color_text(seed, "deterministic"))
|
print(color_text(seed, "deterministic"))
|
||||||
# QR code display removed for managed account seed
|
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
return
|
return
|
||||||
if action == "l":
|
if action == "l":
|
||||||
|
self._suppress_entry_actions_menu = True
|
||||||
self.load_managed_account(index)
|
self.load_managed_account(index)
|
||||||
return
|
return
|
||||||
self._entry_actions_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Default: PASSWORD
|
||||||
website_name = entry.get("label", entry.get("website"))
|
website_name = entry.get("label", entry.get("website"))
|
||||||
length = entry.get("length")
|
length = entry.get("length")
|
||||||
username = entry.get("username")
|
username = entry.get("username")
|
||||||
@@ -2466,15 +2454,11 @@ class PasswordManager:
|
|||||||
else:
|
else:
|
||||||
print(colored(f" {label}: {value}", "cyan"))
|
print(colored(f" {label}: {value}", "cyan"))
|
||||||
if hidden_fields:
|
if hidden_fields:
|
||||||
show = (
|
show = input("Reveal hidden fields? (y/N): ").strip().lower()
|
||||||
input("Reveal hidden fields? (y/N): ").strip().lower()
|
|
||||||
)
|
|
||||||
if show == "y":
|
if show == "y":
|
||||||
for label, value in hidden_fields:
|
for label, value in hidden_fields:
|
||||||
if self.secret_mode_enabled:
|
if self.secret_mode_enabled:
|
||||||
copy_to_clipboard(
|
copy_to_clipboard(value, self.clipboard_clear_delay)
|
||||||
value, self.clipboard_clear_delay
|
|
||||||
)
|
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
f"[+] {label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
f"[+] {label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||||
@@ -2485,8 +2469,37 @@ class PasswordManager:
|
|||||||
print(colored(f" {label}: {value}", "cyan"))
|
print(colored(f" {label}: {value}", "cyan"))
|
||||||
else:
|
else:
|
||||||
print(colored("Error: Failed to retrieve the password.", "red"))
|
print(colored("Error: Failed to retrieve the password.", "red"))
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_retrieve_entry(self) -> None:
|
||||||
|
"""Prompt for an index and display the corresponding entry."""
|
||||||
|
try:
|
||||||
|
fp, parent_fp, child_fp = self.header_fingerprint_args
|
||||||
|
clear_header_with_notification(
|
||||||
|
self,
|
||||||
|
fp,
|
||||||
|
"Main Menu > Retrieve Entry",
|
||||||
|
parent_fingerprint=parent_fp,
|
||||||
|
child_fingerprint=child_fp,
|
||||||
|
)
|
||||||
|
index_input = input(
|
||||||
|
"Enter the index number of the entry to retrieve: "
|
||||||
|
).strip()
|
||||||
|
if not index_input.isdigit():
|
||||||
|
print(colored("Error: Index must be a number.", "red"))
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
index = int(index_input)
|
||||||
|
|
||||||
|
entry = self.entry_manager.retrieve_entry(index)
|
||||||
|
if not entry:
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.display_sensitive_entry_info(entry, index)
|
||||||
self._entry_actions_menu(index, entry)
|
self._entry_actions_menu(index, entry)
|
||||||
pause()
|
pause()
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error during password retrieval: {e}", exc_info=True)
|
logging.error(f"Error during password retrieval: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to retrieve password: {e}", "red"))
|
print(colored(f"Error: Failed to retrieve password: {e}", "red"))
|
||||||
@@ -2519,7 +2532,7 @@ class PasswordManager:
|
|||||||
if not entry:
|
if not entry:
|
||||||
return
|
return
|
||||||
|
|
||||||
entry_type = entry.get("type", EntryType.PASSWORD.value)
|
entry_type = self._entry_type_str(entry)
|
||||||
|
|
||||||
if entry_type == EntryType.TOTP.value:
|
if entry_type == EntryType.TOTP.value:
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
@@ -2912,7 +2925,7 @@ class PasswordManager:
|
|||||||
if not entry:
|
if not entry:
|
||||||
return
|
return
|
||||||
|
|
||||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
etype = self._entry_type_str(entry)
|
||||||
print(color_text(f"Index: {index}", "index"))
|
print(color_text(f"Index: {index}", "index"))
|
||||||
if etype == EntryType.TOTP.value:
|
if etype == EntryType.TOTP.value:
|
||||||
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
||||||
@@ -2934,9 +2947,13 @@ class PasswordManager:
|
|||||||
elif etype == EntryType.SEED.value:
|
elif etype == EntryType.SEED.value:
|
||||||
print(color_text(" Type: Seed Phrase", "index"))
|
print(color_text(" Type: Seed Phrase", "index"))
|
||||||
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
||||||
print(color_text(f" Words: {entry.get('words', 24)}", "index"))
|
words = entry.get("word_count", entry.get("words", 24))
|
||||||
|
print(color_text(f" Words: {words}", "index"))
|
||||||
print(
|
print(
|
||||||
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
color_text(
|
||||||
|
f" Derivation Index: {entry.get('index', index)}",
|
||||||
|
"index",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
if notes:
|
if notes:
|
||||||
@@ -2950,6 +2967,12 @@ class PasswordManager:
|
|||||||
print(
|
print(
|
||||||
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
||||||
)
|
)
|
||||||
|
pub_label = entry.get("public_key_label", "")
|
||||||
|
if pub_label:
|
||||||
|
print(color_text(f" Public Key Label: {pub_label}", "index"))
|
||||||
|
ssh_fingerprint = entry.get("fingerprint", "")
|
||||||
|
if ssh_fingerprint:
|
||||||
|
print(color_text(f" Fingerprint: {ssh_fingerprint}", "index"))
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
if notes:
|
if notes:
|
||||||
print(color_text(f" Notes: {notes}", "index"))
|
print(color_text(f" Notes: {notes}", "index"))
|
||||||
@@ -2968,6 +2991,14 @@ class PasswordManager:
|
|||||||
print(
|
print(
|
||||||
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
_priv, pgp_fp = self.entry_manager.get_pgp_key(index, self.parent_seed)
|
||||||
|
if pgp_fp:
|
||||||
|
print(color_text(f" Fingerprint: {pgp_fp}", "index"))
|
||||||
|
except Exception as pgp_err: # pragma: no cover - best effort logging
|
||||||
|
logging.error(
|
||||||
|
f"Failed to derive PGP fingerprint: {pgp_err}", exc_info=True
|
||||||
|
)
|
||||||
notes = entry.get("notes", "")
|
notes = entry.get("notes", "")
|
||||||
if notes:
|
if notes:
|
||||||
print(color_text(f" Notes: {notes}", "index"))
|
print(color_text(f" Notes: {notes}", "index"))
|
||||||
@@ -2986,6 +3017,37 @@ class PasswordManager:
|
|||||||
tags = entry.get("tags", [])
|
tags = entry.get("tags", [])
|
||||||
if tags:
|
if tags:
|
||||||
print(color_text(f" Tags: {', '.join(tags)}", "index"))
|
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: {entry.get('label', '')}", "index"))
|
||||||
|
print(color_text(f" Value: {entry.get('value', '')}", "index"))
|
||||||
|
notes = entry.get("notes", "")
|
||||||
|
if notes:
|
||||||
|
print(color_text(f" Notes: {notes}", "index"))
|
||||||
|
tags = entry.get("tags", [])
|
||||||
|
if tags:
|
||||||
|
print(color_text(f" Tags: {', '.join(tags)}", "index"))
|
||||||
|
blacklisted = entry.get("archived", entry.get("blacklisted", False))
|
||||||
|
print(color_text(f" Archived: {'Yes' if blacklisted else 'No'}", "index"))
|
||||||
|
elif etype == EntryType.MANAGED_ACCOUNT.value:
|
||||||
|
print(color_text(" Type: Managed Account", "index"))
|
||||||
|
print(color_text(f" Label: {entry.get('label', '')}", "index"))
|
||||||
|
words = entry.get("word_count", entry.get("words", 24))
|
||||||
|
print(color_text(f" Words: {words}", "index"))
|
||||||
|
print(
|
||||||
|
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
|
||||||
|
)
|
||||||
|
fingerprint = entry.get("fingerprint", "")
|
||||||
|
if fingerprint:
|
||||||
|
print(color_text(f" Fingerprint: {fingerprint}", "index"))
|
||||||
|
notes = entry.get("notes", "")
|
||||||
|
if notes:
|
||||||
|
print(color_text(f" Notes: {notes}", "index"))
|
||||||
|
tags = entry.get("tags", [])
|
||||||
|
if tags:
|
||||||
|
print(color_text(f" Tags: {', '.join(tags)}", "index"))
|
||||||
|
blacklisted = entry.get("archived", entry.get("blacklisted", False))
|
||||||
|
print(color_text(f" Archived: {'Yes' if blacklisted else 'No'}", "index"))
|
||||||
else:
|
else:
|
||||||
website = entry.get("label", entry.get("website", ""))
|
website = entry.get("label", entry.get("website", ""))
|
||||||
username = entry.get("username", "")
|
username = entry.get("username", "")
|
||||||
@@ -3218,7 +3280,9 @@ class PasswordManager:
|
|||||||
entries = data.get("entries", {})
|
entries = data.get("entries", {})
|
||||||
totp_list: list[tuple[str, int, int, bool]] = []
|
totp_list: list[tuple[str, int, int, bool]] = []
|
||||||
for idx_str, entry in entries.items():
|
for idx_str, entry in entries.items():
|
||||||
if entry.get("type") == EntryType.TOTP.value and not entry.get(
|
if self._entry_type_str(
|
||||||
|
entry
|
||||||
|
) == EntryType.TOTP.value and not entry.get(
|
||||||
"archived", entry.get("blacklisted", False)
|
"archived", entry.get("blacklisted", False)
|
||||||
):
|
):
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
@@ -3514,7 +3578,7 @@ class PasswordManager:
|
|||||||
|
|
||||||
totp_entries = []
|
totp_entries = []
|
||||||
for entry in entries.values():
|
for entry in entries.values():
|
||||||
if entry.get("type") == EntryType.TOTP.value:
|
if self._entry_type_str(entry) == EntryType.TOTP.value:
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
period = int(entry.get("period", 30))
|
period = int(entry.get("period", 30))
|
||||||
digits = int(entry.get("digits", 6))
|
digits = int(entry.get("digits", 6))
|
||||||
@@ -3810,7 +3874,7 @@ class PasswordManager:
|
|||||||
entries = data.get("entries", {})
|
entries = data.get("entries", {})
|
||||||
counts: dict[str, int] = {etype.value: 0 for etype in EntryType}
|
counts: dict[str, int] = {etype.value: 0 for etype in EntryType}
|
||||||
for entry in entries.values():
|
for entry in entries.values():
|
||||||
etype = entry.get("type", EntryType.PASSWORD.value)
|
etype = self._entry_type_str(entry)
|
||||||
counts[etype] = counts.get(etype, 0) + 1
|
counts[etype] = counts.get(etype, 0) + 1
|
||||||
stats["entries"] = counts
|
stats["entries"] = counts
|
||||||
stats["total_entries"] = len(entries)
|
stats["total_entries"] = len(entries)
|
||||||
|
@@ -2,6 +2,8 @@ from pathlib import Path
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -11,6 +13,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
|
|||||||
from password_manager.entry_management import EntryManager
|
from password_manager.entry_management import EntryManager
|
||||||
from password_manager.backup import BackupManager
|
from password_manager.backup import BackupManager
|
||||||
from password_manager.manager import PasswordManager, EncryptionMode
|
from password_manager.manager import PasswordManager, EncryptionMode
|
||||||
|
from password_manager.entry_types import EntryType
|
||||||
from password_manager.config_manager import ConfigManager
|
from password_manager.config_manager import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
@@ -80,12 +83,314 @@ def test_list_entries_show_details(monkeypatch, capsys):
|
|||||||
lambda *a, **k: "b",
|
lambda *a, **k: "b",
|
||||||
)
|
)
|
||||||
|
|
||||||
inputs = iter(["1", "0", "n"])
|
inputs = iter(["1", "0"])
|
||||||
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
||||||
|
|
||||||
pm.handle_list_entries()
|
pm.handle_list_entries()
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "Retrieved 2FA Code" in out
|
assert "Label: Example" in out
|
||||||
assert "123456" in out
|
assert "Period: 30s" in out
|
||||||
assert "API" in out
|
assert "API" in out
|
||||||
assert "acct" in out
|
assert "acct" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_entry_details_by_index(monkeypatch):
|
||||||
|
"""Ensure entry details screen triggers expected calls."""
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, tmp_path)
|
||||||
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
|
pm.encryption_manager = enc_mgr
|
||||||
|
pm.vault = vault
|
||||||
|
pm.entry_manager = entry_mgr
|
||||||
|
pm.backup_manager = backup_mgr
|
||||||
|
pm.nostr_client = SimpleNamespace()
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
|
||||||
|
index = entry_mgr.add_entry("example.com", 12)
|
||||||
|
|
||||||
|
header_calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.clear_header_with_notification",
|
||||||
|
lambda *a, **k: header_calls.append(True),
|
||||||
|
)
|
||||||
|
|
||||||
|
call_order = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm,
|
||||||
|
"display_entry_details",
|
||||||
|
lambda *a, **k: call_order.append("display"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm,
|
||||||
|
"_entry_actions_menu",
|
||||||
|
lambda *a, **k: call_order.append("actions"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.confirm_action", lambda *a, **k: False
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(index)
|
||||||
|
|
||||||
|
assert len(header_calls) == 1
|
||||||
|
assert call_order == ["display", "actions"]
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_manager(tmp_path):
|
||||||
|
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, tmp_path)
|
||||||
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
|
pm.encryption_manager = enc_mgr
|
||||||
|
pm.vault = vault
|
||||||
|
pm.entry_manager = entry_mgr
|
||||||
|
pm.backup_manager = backup_mgr
|
||||||
|
pm.parent_seed = TEST_SEED
|
||||||
|
pm.nostr_client = SimpleNamespace()
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
return pm, entry_mgr
|
||||||
|
|
||||||
|
|
||||||
|
def _detail_common(monkeypatch, pm):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.clear_header_with_notification",
|
||||||
|
lambda *a, **k: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.confirm_action", lambda *a, **k: False
|
||||||
|
)
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(pm, "_entry_actions_menu", lambda *a, **k: called.append(True))
|
||||||
|
return called
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_seed_entry_details(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
idx = entry_mgr.add_seed("seed", TEST_SEED, words_num=12)
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Type: Seed Phrase" in out
|
||||||
|
assert "Label: seed" in out
|
||||||
|
assert "Words: 12" in out
|
||||||
|
assert f"Derivation Index: {idx}" in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_ssh_entry_details(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
idx = entry_mgr.add_ssh_key("ssh", TEST_SEED)
|
||||||
|
data = entry_mgr._load_index(force_reload=True)
|
||||||
|
data["entries"][str(idx)]["public_key_label"] = "server"
|
||||||
|
data["entries"][str(idx)]["fingerprint"] = "abc123"
|
||||||
|
entry_mgr._save_index(data)
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Type: SSH Key" in out
|
||||||
|
assert "Label: ssh" in out
|
||||||
|
assert f"Derivation Index: {idx}" in out
|
||||||
|
assert "server" in out
|
||||||
|
assert "abc123" in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_pgp_entry_details(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
|
||||||
|
_k, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Type: PGP Key" in out
|
||||||
|
assert "Label: pgp" in out
|
||||||
|
assert "Key Type: ed25519" in out
|
||||||
|
assert "User ID: test" in out
|
||||||
|
assert f"Derivation Index: {idx}" in out
|
||||||
|
assert fp in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_nostr_entry_details(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
idx = entry_mgr.add_nostr_key("nostr")
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Type: Nostr Key" in out
|
||||||
|
assert "Label: nostr" in out
|
||||||
|
assert f"Derivation Index: {idx}" in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_managed_account_entry_details(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
idx = entry_mgr.add_managed_account("acct", TEST_SEED)
|
||||||
|
fp = entry_mgr.retrieve_entry(idx).get("fingerprint")
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Type: Managed Account" in out
|
||||||
|
assert "Label: acct" in out
|
||||||
|
assert f"Derivation Index: {idx}" in out
|
||||||
|
assert "Words: 12" in out
|
||||||
|
assert fp in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"entry_type",
|
||||||
|
[
|
||||||
|
"password",
|
||||||
|
"seed",
|
||||||
|
"ssh",
|
||||||
|
"pgp",
|
||||||
|
"nostr",
|
||||||
|
"totp",
|
||||||
|
"key_value",
|
||||||
|
"managed_account",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
|
||||||
|
"""Ensure sensitive details are displayed for each entry type."""
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
pm.password_generator = SimpleNamespace(generate_password=lambda l, i: "pw123")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.confirm_action", lambda *a, **k: True
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.copy_to_clipboard", lambda *a, **k: None
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b")
|
||||||
|
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.TotpManager.print_qr_code", lambda *a, **k: None
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.clear_header_with_notification",
|
||||||
|
lambda *a, **k: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("password_manager.manager.pause", lambda *a, **k: None)
|
||||||
|
|
||||||
|
input_val = "r" if entry_type == "managed_account" else ""
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: input_val)
|
||||||
|
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm, "_entry_actions_menu", lambda *a, **k: called.append(True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry_type == "password":
|
||||||
|
idx = entry_mgr.add_entry("example", 8)
|
||||||
|
expected = "pw123"
|
||||||
|
elif entry_type == "seed":
|
||||||
|
idx = entry_mgr.add_seed("seed", TEST_SEED, words_num=12)
|
||||||
|
expected = entry_mgr.get_seed_phrase(idx, TEST_SEED)
|
||||||
|
elif entry_type == "ssh":
|
||||||
|
idx = entry_mgr.add_ssh_key("ssh", TEST_SEED)
|
||||||
|
priv, pub = entry_mgr.get_ssh_key_pair(idx, TEST_SEED)
|
||||||
|
expected = priv
|
||||||
|
extra = pub
|
||||||
|
elif entry_type == "pgp":
|
||||||
|
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
|
||||||
|
priv, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||||
|
expected = priv
|
||||||
|
extra = fp
|
||||||
|
elif entry_type == "nostr":
|
||||||
|
idx = entry_mgr.add_nostr_key("nostr")
|
||||||
|
_npub, nsec = entry_mgr.get_nostr_key_pair(idx, TEST_SEED)
|
||||||
|
expected = nsec
|
||||||
|
elif entry_type == "totp":
|
||||||
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
idx = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm.entry_manager, "get_totp_code", lambda *a, **k: "123456"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1
|
||||||
|
)
|
||||||
|
expected = "123456"
|
||||||
|
elif entry_type == "key_value":
|
||||||
|
idx = entry_mgr.add_key_value("API", "abc")
|
||||||
|
expected = "abc"
|
||||||
|
else: # managed_account
|
||||||
|
idx = entry_mgr.add_managed_account("acct", TEST_SEED)
|
||||||
|
expected = entry_mgr.get_managed_account_seed(idx, TEST_SEED)
|
||||||
|
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert expected in out
|
||||||
|
if entry_type in {"ssh", "pgp"}:
|
||||||
|
assert extra in out
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"entry_type", [EntryType.PASSWORD, EntryType.TOTP, EntryType.KEY_VALUE]
|
||||||
|
)
|
||||||
|
def test_show_entry_details_with_enum_type(monkeypatch, capsys, entry_type):
|
||||||
|
"""Entries storing an EntryType enum should display correctly."""
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
pm, entry_mgr = _setup_manager(tmp_path)
|
||||||
|
|
||||||
|
if entry_type == EntryType.PASSWORD:
|
||||||
|
idx = entry_mgr.add_entry("example.com", 8)
|
||||||
|
expect = "example.com"
|
||||||
|
elif entry_type == EntryType.TOTP:
|
||||||
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
idx = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm.entry_manager, "get_totp_code", lambda *a, **k: "123456"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1
|
||||||
|
)
|
||||||
|
expect = "Label: Example"
|
||||||
|
else: # KEY_VALUE
|
||||||
|
idx = entry_mgr.add_key_value("API", "abc")
|
||||||
|
expect = "API"
|
||||||
|
|
||||||
|
data = entry_mgr._load_index(force_reload=True)
|
||||||
|
data["entries"][str(idx)]["type"] = entry_type
|
||||||
|
entry_mgr._save_index(data)
|
||||||
|
|
||||||
|
called = _detail_common(monkeypatch, pm)
|
||||||
|
pm.show_entry_details_by_index(idx)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert expect in out
|
||||||
|
assert called == [True]
|
||||||
|
@@ -41,11 +41,11 @@ def test_search_entries_prompt_for_details(monkeypatch, capsys):
|
|||||||
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
|
monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None)
|
||||||
monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b")
|
monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b")
|
||||||
|
|
||||||
inputs = iter(["Example", "0", "n", ""])
|
inputs = iter(["Example", "0"])
|
||||||
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
|
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
|
||||||
|
|
||||||
pm.handle_search_entries()
|
pm.handle_search_entries()
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "0. Example" in out
|
assert "0. Example" in out
|
||||||
assert "Retrieved 2FA Code" in out
|
assert "Label: Example" in out
|
||||||
assert "123456" in out
|
assert "Period: 30s" in out
|
||||||
|
@@ -93,3 +93,46 @@ def test_show_private_key_qr(monkeypatch, capsys):
|
|||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert called == [nsec]
|
assert called == [nsec]
|
||||||
assert color_text(f"nsec: {nsec}", "deterministic") in out
|
assert color_text(f"nsec: {nsec}", "deterministic") in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_menu_case_insensitive(monkeypatch):
|
||||||
|
"""QR menu should appear even if entry type is uppercase."""
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, tmp_path)
|
||||||
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
|
pm.encryption_manager = enc_mgr
|
||||||
|
pm.vault = vault
|
||||||
|
pm.entry_manager = entry_mgr
|
||||||
|
pm.backup_manager = backup_mgr
|
||||||
|
pm.parent_seed = TEST_SEED
|
||||||
|
pm.nostr_client = FakeNostrClient()
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.is_dirty = False
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
|
idx = entry_mgr.add_nostr_key("main")
|
||||||
|
npub, _ = entry_mgr.get_nostr_key_pair(idx, TEST_SEED)
|
||||||
|
|
||||||
|
# Modify index to use uppercase type/kind
|
||||||
|
data = enc_mgr.load_json_data(entry_mgr.index_file)
|
||||||
|
data["entries"][str(idx)]["type"] = "NOSTR"
|
||||||
|
data["entries"][str(idx)]["kind"] = "NOSTR"
|
||||||
|
enc_mgr.save_json_data(data, entry_mgr.index_file)
|
||||||
|
entry_mgr._index_cache = None
|
||||||
|
|
||||||
|
inputs = iter([str(idx), "q", "p", ""])
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.TotpManager.print_qr_code",
|
||||||
|
lambda data: called.append(data),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.handle_retrieve_entry()
|
||||||
|
assert called == [f"nostr:{npub}"]
|
||||||
|
58
src/tests/test_retrieve_pause_sensitive_entries.py
Normal file
58
src/tests/test_retrieve_pause_sensitive_entries.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from password_manager.entry_management import EntryManager
|
||||||
|
from password_manager.backup import BackupManager
|
||||||
|
from password_manager.manager import PasswordManager, EncryptionMode
|
||||||
|
from password_manager.config_manager import ConfigManager
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"adder,needs_confirm",
|
||||||
|
[
|
||||||
|
(lambda mgr: mgr.add_seed("seed", TEST_SEED), True),
|
||||||
|
(lambda mgr: mgr.add_pgp_key("pgp", TEST_SEED, user_id="test"), True),
|
||||||
|
(lambda mgr: mgr.add_ssh_key("ssh", TEST_SEED), True),
|
||||||
|
(lambda mgr: mgr.add_nostr_key("nostr"), False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_pause_before_entry_actions(monkeypatch, adder, needs_confirm):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, tmp_path)
|
||||||
|
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||||
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
|
pm.encryption_manager = enc_mgr
|
||||||
|
pm.vault = vault
|
||||||
|
pm.entry_manager = entry_mgr
|
||||||
|
pm.backup_manager = backup_mgr
|
||||||
|
pm.parent_seed = TEST_SEED
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
|
index = adder(entry_mgr)
|
||||||
|
|
||||||
|
pause_calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.pause", lambda *a, **k: pause_calls.append(True)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(pm, "_entry_actions_menu", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: str(index))
|
||||||
|
if needs_confirm:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.confirm_action", lambda *a, **k: True
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.handle_retrieve_entry()
|
||||||
|
assert len(pause_calls) == 1
|
Reference in New Issue
Block a user