mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-10 00:09:04 +00:00
Update settings menu test for new option
This commit is contained in:
@@ -266,9 +266,10 @@ Back in the Settings menu you can:
|
|||||||
* Select `8` to export all 2FA codes.
|
* Select `8` to export all 2FA codes.
|
||||||
* Choose `9` to set an additional backup location.
|
* Choose `9` to set an additional backup location.
|
||||||
* Select `10` to change the inactivity timeout.
|
* Select `10` to change the inactivity timeout.
|
||||||
* Choose `11` to lock the vault and require re-entry of your password.
|
* Choose `11` to toggle Secret Mode and set the clipboard clear delay.
|
||||||
* Select `12` to return to the main menu.
|
* Select `12` to lock the vault and require re-entry of your password.
|
||||||
* Choose `13` to view seed profile stats.
|
* Choose `13` to return to the main menu.
|
||||||
|
* Select `14` to view seed profile stats.
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
@@ -440,6 +441,7 @@ The SeedPass roadmap outlines a structured development plan divided into distinc
|
|||||||
- **Toggle Setting:** Allow users to enable or disable "secret" mode.
|
- **Toggle Setting:** Allow users to enable or disable "secret" mode.
|
||||||
- **Clipboard Integration:** Ensure passwords are copied securely to the clipboard when "secret" mode is active.
|
- **Clipboard Integration:** Ensure passwords are copied securely to the clipboard when "secret" mode is active.
|
||||||
- **User Feedback:** Notify users that the password has been copied to the clipboard.
|
- **User Feedback:** Notify users that the password has been copied to the clipboard.
|
||||||
|
- **Settings Menu:** Toggle this mode under `Settings -> Toggle Secret Mode` and set how long the clipboard is retained.
|
||||||
- **Two-Factor Security Model with Random Index Generation**
|
- **Two-Factor Security Model with Random Index Generation**
|
||||||
- **Description:** Create a robust two-factor security system using a master seed and master password combination, enhanced with random index generation for additional security.
|
- **Description:** Create a robust two-factor security system using a master seed and master password combination, enhanced with random index generation for additional security.
|
||||||
- **Key Features:**
|
- **Key Features:**
|
||||||
|
@@ -494,6 +494,8 @@ seedpass set-secret --disable
|
|||||||
- `--enable`: Activates "secret" mode.
|
- `--enable`: Activates "secret" mode.
|
||||||
- `--disable`: Deactivates "secret" mode.
|
- `--disable`: Deactivates "secret" mode.
|
||||||
|
|
||||||
|
You can also enable or disable secret mode from the interactive Settings menu by selecting **Toggle Secret Mode**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 21. Batch Post Snapshot Deltas to Nostr
|
### 21. Batch Post Snapshot Deltas to Nostr
|
||||||
|
54
src/main.py
54
src/main.py
@@ -499,6 +499,47 @@ def handle_set_additional_backup_location(pm: PasswordManager) -> None:
|
|||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_toggle_secret_mode(pm: PasswordManager) -> None:
|
||||||
|
"""Toggle secret mode and adjust clipboard delay."""
|
||||||
|
cfg = pm.config_manager
|
||||||
|
if cfg is None:
|
||||||
|
print(colored("Configuration manager unavailable.", "red"))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
enabled = cfg.get_secret_mode_enabled()
|
||||||
|
delay = cfg.get_clipboard_clear_delay()
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Error loading secret mode settings: {exc}")
|
||||||
|
print(colored(f"Error loading settings: {exc}", "red"))
|
||||||
|
return
|
||||||
|
print(colored(f"Secret mode is currently {'ON' if enabled else 'OFF'}", "cyan"))
|
||||||
|
value = input("Enable secret mode? (y/n, blank to keep): ").strip().lower()
|
||||||
|
if value in ("y", "yes"):
|
||||||
|
enabled = True
|
||||||
|
elif value in ("n", "no"):
|
||||||
|
enabled = False
|
||||||
|
dur = input(f"Clipboard clear delay in seconds [{delay}]: ").strip()
|
||||||
|
if dur:
|
||||||
|
try:
|
||||||
|
delay = int(dur)
|
||||||
|
if delay <= 0:
|
||||||
|
print(colored("Delay must be positive.", "red"))
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
print(colored("Invalid number.", "red"))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
cfg.set_secret_mode_enabled(enabled)
|
||||||
|
cfg.set_clipboard_clear_delay(delay)
|
||||||
|
pm.secret_mode_enabled = enabled
|
||||||
|
pm.clipboard_clear_delay = delay
|
||||||
|
status = "enabled" if enabled else "disabled"
|
||||||
|
print(colored(f"Secret mode {status}.", "green"))
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Error saving secret mode: {exc}")
|
||||||
|
print(colored(f"Error: {exc}", "red"))
|
||||||
|
|
||||||
|
|
||||||
def handle_profiles_menu(password_manager: PasswordManager) -> None:
|
def handle_profiles_menu(password_manager: PasswordManager) -> None:
|
||||||
"""Submenu for managing seed profiles."""
|
"""Submenu for managing seed profiles."""
|
||||||
while True:
|
while True:
|
||||||
@@ -583,9 +624,10 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
|||||||
print("8. Export 2FA codes")
|
print("8. Export 2FA codes")
|
||||||
print("9. Set additional backup location")
|
print("9. Set additional backup location")
|
||||||
print("10. Set inactivity timeout")
|
print("10. Set inactivity timeout")
|
||||||
print("11. Lock Vault")
|
print("11. Toggle Secret Mode")
|
||||||
print("12. Stats")
|
print("12. Lock Vault")
|
||||||
print("13. Back")
|
print("13. Stats")
|
||||||
|
print("14. Back")
|
||||||
choice = input("Select an option: ").strip()
|
choice = input("Select an option: ").strip()
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
handle_profiles_menu(password_manager)
|
handle_profiles_menu(password_manager)
|
||||||
@@ -610,12 +652,14 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
|||||||
elif choice == "10":
|
elif choice == "10":
|
||||||
handle_set_inactivity_timeout(password_manager)
|
handle_set_inactivity_timeout(password_manager)
|
||||||
elif choice == "11":
|
elif choice == "11":
|
||||||
|
handle_toggle_secret_mode(password_manager)
|
||||||
|
elif choice == "12":
|
||||||
password_manager.lock_vault()
|
password_manager.lock_vault()
|
||||||
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
||||||
password_manager.unlock_vault()
|
password_manager.unlock_vault()
|
||||||
elif choice == "12":
|
|
||||||
handle_display_stats(password_manager)
|
|
||||||
elif choice == "13":
|
elif choice == "13":
|
||||||
|
handle_display_stats(password_manager)
|
||||||
|
elif choice == "14":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice.", "red"))
|
print(colored("Invalid choice.", "red"))
|
||||||
|
@@ -45,6 +45,8 @@ class ConfigManager:
|
|||||||
"password_hash": "",
|
"password_hash": "",
|
||||||
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
||||||
"additional_backup_path": "",
|
"additional_backup_path": "",
|
||||||
|
"secret_mode_enabled": False,
|
||||||
|
"clipboard_clear_delay": 45,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
data = self.vault.load_config()
|
data = self.vault.load_config()
|
||||||
@@ -56,6 +58,8 @@ class ConfigManager:
|
|||||||
data.setdefault("password_hash", "")
|
data.setdefault("password_hash", "")
|
||||||
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
||||||
data.setdefault("additional_backup_path", "")
|
data.setdefault("additional_backup_path", "")
|
||||||
|
data.setdefault("secret_mode_enabled", False)
|
||||||
|
data.setdefault("clipboard_clear_delay", 45)
|
||||||
|
|
||||||
# Migrate legacy hashed_password.enc if present and password_hash is missing
|
# Migrate legacy hashed_password.enc if present and password_hash is missing
|
||||||
legacy_file = self.fingerprint_dir / "hashed_password.enc"
|
legacy_file = self.fingerprint_dir / "hashed_password.enc"
|
||||||
@@ -144,3 +148,27 @@ class ConfigManager:
|
|||||||
config = self.load_config(require_pin=False)
|
config = self.load_config(require_pin=False)
|
||||||
value = config.get("additional_backup_path", "")
|
value = config.get("additional_backup_path", "")
|
||||||
return value or None
|
return value or None
|
||||||
|
|
||||||
|
def set_secret_mode_enabled(self, enabled: bool) -> None:
|
||||||
|
"""Persist the secret mode toggle."""
|
||||||
|
config = self.load_config(require_pin=False)
|
||||||
|
config["secret_mode_enabled"] = bool(enabled)
|
||||||
|
self.save_config(config)
|
||||||
|
|
||||||
|
def get_secret_mode_enabled(self) -> bool:
|
||||||
|
"""Retrieve whether secret mode is enabled."""
|
||||||
|
config = self.load_config(require_pin=False)
|
||||||
|
return bool(config.get("secret_mode_enabled", False))
|
||||||
|
|
||||||
|
def set_clipboard_clear_delay(self, delay: int) -> None:
|
||||||
|
"""Persist clipboard clear timeout in seconds."""
|
||||||
|
if delay <= 0:
|
||||||
|
raise ValueError("Delay must be positive")
|
||||||
|
config = self.load_config(require_pin=False)
|
||||||
|
config["clipboard_clear_delay"] = int(delay)
|
||||||
|
self.save_config(config)
|
||||||
|
|
||||||
|
def get_clipboard_clear_delay(self) -> int:
|
||||||
|
"""Retrieve clipboard clear delay in seconds."""
|
||||||
|
config = self.load_config(require_pin=False)
|
||||||
|
return int(config.get("clipboard_clear_delay", 45))
|
||||||
|
@@ -42,6 +42,7 @@ from utils.password_prompt import (
|
|||||||
confirm_action,
|
confirm_action,
|
||||||
)
|
)
|
||||||
from utils.memory_protection import InMemorySecret
|
from utils.memory_protection import InMemorySecret
|
||||||
|
from utils.clipboard import copy_to_clipboard
|
||||||
from constants import MIN_HEALTHY_RELAYS
|
from constants import MIN_HEALTHY_RELAYS
|
||||||
|
|
||||||
from constants import (
|
from constants import (
|
||||||
@@ -106,6 +107,8 @@ class PasswordManager:
|
|||||||
self.last_activity: float = time.time()
|
self.last_activity: float = time.time()
|
||||||
self.locked: bool = False
|
self.locked: bool = False
|
||||||
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
||||||
|
self.secret_mode_enabled: bool = False
|
||||||
|
self.clipboard_clear_delay: int = 45
|
||||||
|
|
||||||
# Initialize the fingerprint manager first
|
# Initialize the fingerprint manager first
|
||||||
self.initialize_fingerprint_manager()
|
self.initialize_fingerprint_manager()
|
||||||
@@ -776,6 +779,8 @@ class PasswordManager:
|
|||||||
self.inactivity_timeout = config.get(
|
self.inactivity_timeout = config.get(
|
||||||
"inactivity_timeout", INACTIVITY_TIMEOUT
|
"inactivity_timeout", INACTIVITY_TIMEOUT
|
||||||
)
|
)
|
||||||
|
self.secret_mode_enabled = bool(config.get("secret_mode_enabled", False))
|
||||||
|
self.clipboard_clear_delay = int(config.get("clipboard_clear_delay", 45))
|
||||||
|
|
||||||
self.nostr_client = NostrClient(
|
self.nostr_client = NostrClient(
|
||||||
encryption_manager=self.encryption_manager,
|
encryption_manager=self.encryption_manager,
|
||||||
@@ -1021,9 +1026,18 @@ class PasswordManager:
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
code = self.entry_manager.get_totp_code(index, self.parent_seed)
|
code = self.entry_manager.get_totp_code(index, self.parent_seed)
|
||||||
print(colored("\n[+] Retrieved 2FA Code:\n", "green"))
|
if self.secret_mode_enabled:
|
||||||
print(colored(f"Label: {label}", "cyan"))
|
copy_to_clipboard(code, self.clipboard_clear_delay)
|
||||||
print(colored(f"Code: {code}", "yellow"))
|
print(
|
||||||
|
colored(
|
||||||
|
f"[+] 2FA code for '{label}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||||
|
"green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(colored("\n[+] Retrieved 2FA Code:\n", "green"))
|
||||||
|
print(colored(f"Label: {label}", "cyan"))
|
||||||
|
print(colored(f"Code: {code}", "yellow"))
|
||||||
if notes:
|
if notes:
|
||||||
print(colored(f"Notes: {notes}", "cyan"))
|
print(colored(f"Notes: {notes}", "cyan"))
|
||||||
remaining = self.entry_manager.get_totp_time_remaining(index)
|
remaining = self.entry_manager.get_totp_time_remaining(index)
|
||||||
@@ -1084,18 +1098,30 @@ class PasswordManager:
|
|||||||
password = self.password_generator.generate_password(length, index)
|
password = self.password_generator.generate_password(length, index)
|
||||||
|
|
||||||
if password:
|
if password:
|
||||||
print(
|
if self.secret_mode_enabled:
|
||||||
colored(f"\n[+] Retrieved Password for {website_name}:\n", "green")
|
copy_to_clipboard(password, self.clipboard_clear_delay)
|
||||||
)
|
print(
|
||||||
print(colored(f"Password: {password}", "yellow"))
|
colored(
|
||||||
print(colored(f"Associated Username: {username or 'N/A'}", "cyan"))
|
f"[+] Password for '{website_name}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||||
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
"green",
|
||||||
print(
|
)
|
||||||
colored(
|
)
|
||||||
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
else:
|
||||||
"cyan",
|
print(
|
||||||
|
colored(
|
||||||
|
f"\n[+] Retrieved Password for {website_name}:\n",
|
||||||
|
"green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(colored(f"Password: {password}", "yellow"))
|
||||||
|
print(colored(f"Associated Username: {username or 'N/A'}", "cyan"))
|
||||||
|
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||||
|
"cyan",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(colored("Error: Failed to retrieve the password.", "red"))
|
print(colored("Error: Failed to retrieve the password.", "red"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1402,7 +1428,13 @@ class PasswordManager:
|
|||||||
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
||||||
filled = int(20 * (period - remaining) / period)
|
filled = int(20 * (period - remaining) / period)
|
||||||
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
||||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
if self.secret_mode_enabled:
|
||||||
|
copy_to_clipboard(code, self.clipboard_clear_delay)
|
||||||
|
print(
|
||||||
|
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||||
if imported_list:
|
if imported_list:
|
||||||
print(colored("\nImported 2FA Codes:", "green"))
|
print(colored("\nImported 2FA Codes:", "green"))
|
||||||
for label, idx, period, _ in imported_list:
|
for label, idx, period, _ in imported_list:
|
||||||
@@ -1410,7 +1442,13 @@ class PasswordManager:
|
|||||||
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
||||||
filled = int(20 * (period - remaining) / period)
|
filled = int(20 * (period - remaining) / period)
|
||||||
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
||||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
if self.secret_mode_enabled:
|
||||||
|
copy_to_clipboard(code, self.clipboard_clear_delay)
|
||||||
|
print(
|
||||||
|
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
try:
|
try:
|
||||||
if sys.stdin in select.select([sys.stdin], [], [], 1)[0]:
|
if sys.stdin in select.select([sys.stdin], [], [], 1)[0]:
|
||||||
|
68
src/tests/test_clipboard_utils.py
Normal file
68
src/tests/test_clipboard_utils.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import pyperclip
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from utils.clipboard import copy_to_clipboard
|
||||||
|
|
||||||
|
|
||||||
|
def test_copy_to_clipboard_clears(monkeypatch):
|
||||||
|
clipboard = {"text": ""}
|
||||||
|
|
||||||
|
def fake_copy(val):
|
||||||
|
clipboard["text"] = val
|
||||||
|
|
||||||
|
def fake_paste():
|
||||||
|
return clipboard["text"]
|
||||||
|
|
||||||
|
callbacks = {}
|
||||||
|
|
||||||
|
class DummyTimer:
|
||||||
|
def __init__(self, delay, func):
|
||||||
|
callbacks["delay"] = delay
|
||||||
|
callbacks["func"] = func
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
callbacks["started"] = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(pyperclip, "copy", fake_copy)
|
||||||
|
monkeypatch.setattr(pyperclip, "paste", fake_paste)
|
||||||
|
monkeypatch.setattr(threading, "Timer", DummyTimer)
|
||||||
|
|
||||||
|
copy_to_clipboard("secret", 2)
|
||||||
|
assert clipboard["text"] == "secret"
|
||||||
|
assert callbacks["delay"] == 2
|
||||||
|
assert callbacks["started"]
|
||||||
|
callbacks["func"]()
|
||||||
|
assert clipboard["text"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_copy_to_clipboard_does_not_clear_if_changed(monkeypatch):
|
||||||
|
clipboard = {"text": ""}
|
||||||
|
|
||||||
|
def fake_copy(val):
|
||||||
|
clipboard["text"] = val
|
||||||
|
|
||||||
|
def fake_paste():
|
||||||
|
return clipboard["text"]
|
||||||
|
|
||||||
|
callbacks = {}
|
||||||
|
|
||||||
|
class DummyTimer:
|
||||||
|
def __init__(self, delay, func):
|
||||||
|
callbacks["func"] = func
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(pyperclip, "copy", fake_copy)
|
||||||
|
monkeypatch.setattr(pyperclip, "paste", fake_paste)
|
||||||
|
monkeypatch.setattr(threading, "Timer", DummyTimer)
|
||||||
|
|
||||||
|
copy_to_clipboard("secret", 1)
|
||||||
|
fake_copy("other")
|
||||||
|
callbacks["func"]()
|
||||||
|
assert clipboard["text"] == "other"
|
@@ -130,3 +130,19 @@ def test_additional_backup_path_round_trip():
|
|||||||
cfg_mgr.set_additional_backup_path(None)
|
cfg_mgr.set_additional_backup_path(None)
|
||||||
cfg2 = cfg_mgr.load_config(require_pin=False)
|
cfg2 = cfg_mgr.load_config(require_pin=False)
|
||||||
assert cfg2["additional_backup_path"] == ""
|
assert cfg2["additional_backup_path"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_secret_mode_round_trip():
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, Path(tmpdir))
|
||||||
|
|
||||||
|
cfg = cfg_mgr.load_config(require_pin=False)
|
||||||
|
assert cfg["secret_mode_enabled"] is False
|
||||||
|
assert cfg["clipboard_clear_delay"] == 45
|
||||||
|
|
||||||
|
cfg_mgr.set_secret_mode_enabled(True)
|
||||||
|
cfg_mgr.set_clipboard_clear_delay(99)
|
||||||
|
cfg2 = cfg_mgr.load_config(require_pin=False)
|
||||||
|
assert cfg2["secret_mode_enabled"] is True
|
||||||
|
assert cfg2["clipboard_clear_delay"] == 99
|
||||||
|
@@ -39,6 +39,7 @@ def test_handle_display_totp_codes(monkeypatch, capsys):
|
|||||||
pm.nostr_client = FakeNostrClient()
|
pm.nostr_client = FakeNostrClient()
|
||||||
pm.fingerprint_dir = tmp_path
|
pm.fingerprint_dir = tmp_path
|
||||||
pm.is_dirty = False
|
pm.is_dirty = False
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
entry_mgr.add_totp("Example", TEST_SEED)
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ def test_display_totp_codes_excludes_blacklisted(monkeypatch, capsys):
|
|||||||
pm.nostr_client = FakeNostrClient()
|
pm.nostr_client = FakeNostrClient()
|
||||||
pm.fingerprint_dir = tmp_path
|
pm.fingerprint_dir = tmp_path
|
||||||
pm.is_dirty = False
|
pm.is_dirty = False
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
entry_mgr.add_totp("Visible", TEST_SEED)
|
entry_mgr.add_totp("Visible", TEST_SEED)
|
||||||
entry_mgr.add_totp("Hidden", TEST_SEED)
|
entry_mgr.add_totp("Hidden", TEST_SEED)
|
||||||
|
@@ -39,6 +39,7 @@ def test_handle_retrieve_totp_entry(monkeypatch, capsys):
|
|||||||
pm.nostr_client = FakeNostrClient()
|
pm.nostr_client = FakeNostrClient()
|
||||||
pm.fingerprint_dir = tmp_path
|
pm.fingerprint_dir = tmp_path
|
||||||
pm.is_dirty = False
|
pm.is_dirty = False
|
||||||
|
pm.secret_mode_enabled = False
|
||||||
|
|
||||||
entry_mgr.add_totp("Example", TEST_SEED)
|
entry_mgr.add_totp("Example", TEST_SEED)
|
||||||
|
|
||||||
|
82
src/tests/test_secret_mode.py
Normal file
82
src/tests/test_secret_mode.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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 setup_pm(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.password_generator = SimpleNamespace(generate_password=lambda l, i: "pw")
|
||||||
|
pm.parent_seed = TEST_SEED
|
||||||
|
pm.nostr_client = SimpleNamespace()
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.config_manager = cfg_mgr
|
||||||
|
pm.secret_mode_enabled = True
|
||||||
|
pm.clipboard_clear_delay = 5
|
||||||
|
return pm, entry_mgr
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_retrieve_secret_mode(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp = Path(tmpdir)
|
||||||
|
pm, entry_mgr = setup_pm(tmp)
|
||||||
|
entry_mgr.add_entry("example", 8)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *a, **k: "0")
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.copy_to_clipboard",
|
||||||
|
lambda text, t: called.append((text, t)),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.handle_retrieve_entry()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Password:" not in out
|
||||||
|
assert "copied to clipboard" in out
|
||||||
|
assert called == [("pw", 5)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_totp_display_secret_mode(monkeypatch, capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp = Path(tmpdir)
|
||||||
|
pm, entry_mgr = setup_pm(tmp)
|
||||||
|
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: 30
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.select.select",
|
||||||
|
lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()),
|
||||||
|
)
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.copy_to_clipboard",
|
||||||
|
lambda text, t: called.append((text, t)),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.handle_display_totp_codes()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "123456" not in out
|
||||||
|
assert "copied to clipboard" in out
|
||||||
|
assert called == [("123456", 5)]
|
@@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch):
|
|||||||
tmp_path = Path(tmpdir)
|
tmp_path = Path(tmpdir)
|
||||||
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
|
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
|
||||||
|
|
||||||
inputs = iter(["9", "13"])
|
inputs = iter(["9", "14"])
|
||||||
with patch("main.handle_set_additional_backup_location") as handler:
|
with patch("main.handle_set_additional_backup_location") as handler:
|
||||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||||
main.handle_settings(pm)
|
main.handle_settings(pm)
|
||||||
|
@@ -25,6 +25,7 @@ try:
|
|||||||
from .password_prompt import prompt_for_password
|
from .password_prompt import prompt_for_password
|
||||||
from .input_utils import timed_input
|
from .input_utils import timed_input
|
||||||
from .memory_protection import InMemorySecret
|
from .memory_protection import InMemorySecret
|
||||||
|
from .clipboard import copy_to_clipboard
|
||||||
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.info("Modules imported successfully.")
|
logger.info("Modules imported successfully.")
|
||||||
@@ -49,4 +50,5 @@ __all__ = [
|
|||||||
"prompt_for_password",
|
"prompt_for_password",
|
||||||
"timed_input",
|
"timed_input",
|
||||||
"InMemorySecret",
|
"InMemorySecret",
|
||||||
|
"copy_to_clipboard",
|
||||||
]
|
]
|
||||||
|
16
src/utils/clipboard.py
Normal file
16
src/utils/clipboard.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import threading
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
|
||||||
|
def copy_to_clipboard(text: str, timeout: int) -> None:
|
||||||
|
"""Copy text to the clipboard and clear after timeout seconds if unchanged."""
|
||||||
|
|
||||||
|
pyperclip.copy(text)
|
||||||
|
|
||||||
|
def clear_clipboard() -> None:
|
||||||
|
if pyperclip.paste() == text:
|
||||||
|
pyperclip.copy("")
|
||||||
|
|
||||||
|
timer = threading.Timer(timeout, clear_clipboard)
|
||||||
|
timer.daemon = True
|
||||||
|
timer.start()
|
Reference in New Issue
Block a user