mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +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
|
import queue
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from functools import wraps
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
from utils.color_scheme import color_text
|
from utils.color_scheme import color_text
|
||||||
from utils.input_utils import timed_input
|
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 utils.atomic_write import atomic_write
|
||||||
from constants import MIN_HEALTHY_RELAYS
|
from constants import MIN_HEALTHY_RELAYS
|
||||||
from .migrations import LATEST_VERSION
|
from .migrations import LATEST_VERSION
|
||||||
|
from ..errors import VaultLockedError
|
||||||
|
|
||||||
from constants import (
|
from constants import (
|
||||||
APP_DIR,
|
APP_DIR,
|
||||||
@@ -164,6 +166,18 @@ class Notification:
|
|||||||
level: str = "INFO"
|
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:
|
class AuthGuard:
|
||||||
"""Helper to enforce inactivity timeouts."""
|
"""Helper to enforce inactivity timeouts."""
|
||||||
|
|
||||||
@@ -228,6 +242,7 @@ class PasswordManager:
|
|||||||
self.last_update: float = time.time()
|
self.last_update: float = time.time()
|
||||||
self.last_activity: float = time.time()
|
self.last_activity: float = time.time()
|
||||||
self.locked: bool = False
|
self.locked: bool = False
|
||||||
|
self.is_locked: bool = False
|
||||||
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
||||||
self.secret_mode_enabled: bool = False
|
self.secret_mode_enabled: bool = False
|
||||||
self.clipboard_clear_delay: int = 45
|
self.clipboard_clear_delay: int = 45
|
||||||
@@ -264,6 +279,7 @@ class PasswordManager:
|
|||||||
self.fingerprint_manager.get_current_fingerprint_dir()
|
self.fingerprint_manager.get_current_fingerprint_dir()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def get_bip85_entropy(self, purpose: int, index: int, bytes_len: int = 32) -> bytes:
|
def get_bip85_entropy(self, purpose: int, index: int, bytes_len: int = 32) -> bytes:
|
||||||
"""Return deterministic entropy via the cached BIP-85 function."""
|
"""Return deterministic entropy via the cached BIP-85 function."""
|
||||||
|
|
||||||
@@ -273,6 +289,7 @@ class PasswordManager:
|
|||||||
index=index, bytes_len=bytes_len, app_no=purpose
|
index=index, bytes_len=bytes_len, app_no=purpose
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def clear_bip85_cache(self) -> None:
|
def clear_bip85_cache(self) -> None:
|
||||||
"""Clear the internal BIP-85 cache."""
|
"""Clear the internal BIP-85 cache."""
|
||||||
|
|
||||||
@@ -400,6 +417,8 @@ class PasswordManager:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def entry_service(self) -> EntryService:
|
def entry_service(self) -> EntryService:
|
||||||
|
if getattr(self, "is_locked", False):
|
||||||
|
raise VaultLockedError("Vault is locked")
|
||||||
if getattr(self, "_entry_service", None) is None:
|
if getattr(self, "_entry_service", None) is None:
|
||||||
self._entry_service = EntryService(self)
|
self._entry_service = EntryService(self)
|
||||||
return self._entry_service
|
return self._entry_service
|
||||||
@@ -408,15 +427,7 @@ class PasswordManager:
|
|||||||
"""Clear sensitive information from memory."""
|
"""Clear sensitive information from memory."""
|
||||||
if self.entry_manager is not None:
|
if self.entry_manager is not None:
|
||||||
self.entry_manager.clear_cache()
|
self.entry_manager.clear_cache()
|
||||||
self.parent_seed = None
|
self.is_locked = True
|
||||||
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.locked = True
|
self.locked = True
|
||||||
bus.publish("vault_locked")
|
bus.publish("vault_locked")
|
||||||
|
|
||||||
@@ -441,6 +452,7 @@ class PasswordManager:
|
|||||||
self.setup_encryption_manager(self.fingerprint_dir, password)
|
self.setup_encryption_manager(self.fingerprint_dir, password)
|
||||||
self.initialize_bip85()
|
self.initialize_bip85()
|
||||||
self.initialize_managers()
|
self.initialize_managers()
|
||||||
|
self.is_locked = False
|
||||||
self.locked = False
|
self.locked = False
|
||||||
self.update_activity()
|
self.update_activity()
|
||||||
if (
|
if (
|
||||||
@@ -746,9 +758,11 @@ class PasswordManager:
|
|||||||
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def handle_switch_fingerprint(self, *, password: Optional[str] = None) -> bool:
|
def handle_switch_fingerprint(self, *, password: Optional[str] = None) -> bool:
|
||||||
return self.profile_service.handle_switch_fingerprint(password=password)
|
return self.profile_service.handle_switch_fingerprint(password=password)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def load_managed_account(self, index: int) -> None:
|
def load_managed_account(self, index: int) -> None:
|
||||||
"""Load a managed account derived from the current seed profile."""
|
"""Load a managed account derived from the current seed profile."""
|
||||||
if not self.entry_manager or not self.parent_seed:
|
if not self.entry_manager or not self.parent_seed:
|
||||||
@@ -777,6 +791,7 @@ class PasswordManager:
|
|||||||
self.update_activity()
|
self.update_activity()
|
||||||
self.start_background_sync()
|
self.start_background_sync()
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def exit_managed_account(self) -> None:
|
def exit_managed_account(self) -> None:
|
||||||
"""Return to the parent seed profile if one is on the stack."""
|
"""Return to the parent seed profile if one is on the stack."""
|
||||||
if not self.profile_stack:
|
if not self.profile_stack:
|
||||||
@@ -1041,6 +1056,7 @@ class PasswordManager:
|
|||||||
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def generate_new_seed(self) -> Optional[str]:
|
def generate_new_seed(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
|
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"))
|
print(colored(f"Error: Failed to generate BIP-85 seed: {e}", "red"))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@requires_unlocked
|
||||||
def save_and_encrypt_seed(
|
def save_and_encrypt_seed(
|
||||||
self, seed: str, fingerprint_dir: Path, *, password: Optional[str] = None
|
self, seed: str, fingerprint_dir: Path, *, password: Optional[str] = None
|
||||||
) -> 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