mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Merge pull request #812 from PR0M3TH3AN/codex/implement-vault-locking-mechanism
Track vault lock state with explicit lock flag
This commit is contained in:
@@ -24,6 +24,7 @@ import threading
|
||||
import queue
|
||||
from dataclasses import dataclass
|
||||
import dataclasses
|
||||
from functools import wraps
|
||||
from termcolor import colored
|
||||
from utils.color_scheme import color_text
|
||||
from utils.input_utils import timed_input
|
||||
@@ -72,6 +73,7 @@ from utils.fingerprint import generate_fingerprint
|
||||
from utils.atomic_write import atomic_write
|
||||
from constants import MIN_HEALTHY_RELAYS
|
||||
from .migrations import LATEST_VERSION
|
||||
from ..errors import VaultLockedError
|
||||
|
||||
from constants import (
|
||||
APP_DIR,
|
||||
@@ -164,6 +166,18 @@ class Notification:
|
||||
level: str = "INFO"
|
||||
|
||||
|
||||
def requires_unlocked(func):
|
||||
"""Decorator to ensure the vault is unlocked before proceeding."""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if getattr(self, "is_locked", False):
|
||||
raise VaultLockedError("Vault is locked")
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class AuthGuard:
|
||||
"""Helper to enforce inactivity timeouts."""
|
||||
|
||||
@@ -228,6 +242,7 @@ class PasswordManager:
|
||||
self.last_update: float = time.time()
|
||||
self.last_activity: float = time.time()
|
||||
self.locked: bool = False
|
||||
self.is_locked: bool = False
|
||||
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
||||
self.secret_mode_enabled: bool = False
|
||||
self.clipboard_clear_delay: int = 45
|
||||
@@ -264,6 +279,7 @@ class PasswordManager:
|
||||
self.fingerprint_manager.get_current_fingerprint_dir()
|
||||
)
|
||||
|
||||
@requires_unlocked
|
||||
def get_bip85_entropy(self, purpose: int, index: int, bytes_len: int = 32) -> bytes:
|
||||
"""Return deterministic entropy via the cached BIP-85 function."""
|
||||
|
||||
@@ -273,6 +289,7 @@ class PasswordManager:
|
||||
index=index, bytes_len=bytes_len, app_no=purpose
|
||||
)
|
||||
|
||||
@requires_unlocked
|
||||
def clear_bip85_cache(self) -> None:
|
||||
"""Clear the internal BIP-85 cache."""
|
||||
|
||||
@@ -400,6 +417,8 @@ class PasswordManager:
|
||||
|
||||
@property
|
||||
def entry_service(self) -> EntryService:
|
||||
if getattr(self, "is_locked", False):
|
||||
raise VaultLockedError("Vault is locked")
|
||||
if getattr(self, "_entry_service", None) is None:
|
||||
self._entry_service = EntryService(self)
|
||||
return self._entry_service
|
||||
@@ -408,15 +427,7 @@ class PasswordManager:
|
||||
"""Clear sensitive information from memory."""
|
||||
if self.entry_manager is not None:
|
||||
self.entry_manager.clear_cache()
|
||||
self.parent_seed = None
|
||||
self.encryption_manager = None
|
||||
self.entry_manager = None
|
||||
self.password_generator = None
|
||||
self.backup_manager = None
|
||||
self.vault = None
|
||||
self.bip85 = None
|
||||
self.nostr_client = None
|
||||
self.config_manager = None
|
||||
self.is_locked = True
|
||||
self.locked = True
|
||||
bus.publish("vault_locked")
|
||||
|
||||
@@ -441,6 +452,7 @@ class PasswordManager:
|
||||
self.setup_encryption_manager(self.fingerprint_dir, password)
|
||||
self.initialize_bip85()
|
||||
self.initialize_managers()
|
||||
self.is_locked = False
|
||||
self.locked = False
|
||||
self.update_activity()
|
||||
if (
|
||||
@@ -746,9 +758,11 @@ class PasswordManager:
|
||||
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
@requires_unlocked
|
||||
def handle_switch_fingerprint(self, *, password: Optional[str] = None) -> bool:
|
||||
return self.profile_service.handle_switch_fingerprint(password=password)
|
||||
|
||||
@requires_unlocked
|
||||
def load_managed_account(self, index: int) -> None:
|
||||
"""Load a managed account derived from the current seed profile."""
|
||||
if not self.entry_manager or not self.parent_seed:
|
||||
@@ -777,6 +791,7 @@ class PasswordManager:
|
||||
self.update_activity()
|
||||
self.start_background_sync()
|
||||
|
||||
@requires_unlocked
|
||||
def exit_managed_account(self) -> None:
|
||||
"""Return to the parent seed profile if one is on the stack."""
|
||||
if not self.profile_stack:
|
||||
@@ -1041,6 +1056,7 @@ class PasswordManager:
|
||||
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
@requires_unlocked
|
||||
def generate_new_seed(self) -> Optional[str]:
|
||||
"""
|
||||
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
|
||||
@@ -1146,6 +1162,7 @@ class PasswordManager:
|
||||
print(colored(f"Error: Failed to generate BIP-85 seed: {e}", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
@requires_unlocked
|
||||
def save_and_encrypt_seed(
|
||||
self, seed: str, fingerprint_dir: Path, *, password: Optional[str] = None
|
||||
) -> None:
|
||||
|
4
src/seedpass/errors.py
Normal file
4
src/seedpass/errors.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class VaultLockedError(Exception):
|
||||
"""Raised when an operation requires an unlocked vault."""
|
||||
|
||||
pass
|
52
src/tests/test_vault_lock_flag.py
Normal file
52
src/tests/test_vault_lock_flag.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from seedpass.core.manager import PasswordManager
|
||||
from seedpass.errors import VaultLockedError
|
||||
|
||||
|
||||
class DummyEntryManager:
|
||||
def __init__(self):
|
||||
self.cleared = False
|
||||
|
||||
def clear_cache(self):
|
||||
self.cleared = True
|
||||
|
||||
|
||||
def test_lock_vault_sets_flag_and_keeps_objects():
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
em = DummyEntryManager()
|
||||
pm.entry_manager = em
|
||||
pm.is_locked = False
|
||||
pm.locked = False
|
||||
pm.lock_vault()
|
||||
assert pm.is_locked
|
||||
assert pm.locked
|
||||
assert pm.entry_manager is em
|
||||
assert em.cleared
|
||||
|
||||
|
||||
def test_entry_service_requires_unlocked():
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
service = SimpleNamespace()
|
||||
pm._entry_service = service
|
||||
pm.is_locked = True
|
||||
with pytest.raises(VaultLockedError):
|
||||
_ = pm.entry_service
|
||||
pm.is_locked = False
|
||||
assert pm.entry_service is service
|
||||
|
||||
|
||||
def test_unlock_vault_clears_locked_flag(tmp_path):
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
pm.fingerprint_dir = tmp_path
|
||||
pm.parent_seed = "seed"
|
||||
pm.setup_encryption_manager = lambda *a, **k: None
|
||||
pm.initialize_bip85 = lambda: None
|
||||
pm.initialize_managers = lambda: None
|
||||
pm.update_activity = lambda: None
|
||||
pm.is_locked = True
|
||||
pm.locked = True
|
||||
pm.unlock_vault("pw")
|
||||
assert not pm.is_locked
|
||||
assert not pm.locked
|
Reference in New Issue
Block a user