From c4bb8dfa64ea0013a90f2ad3b611340ebfa214df Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:23:49 -0400 Subject: [PATCH] Allow non-interactive unlock --- src/password_manager/manager.py | 26 ++++-- src/tests/test_noninteractive_init_unlock.py | 84 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/tests/test_noninteractive_init_unlock.py diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index c3c2205..c84b592 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -117,7 +117,9 @@ class PasswordManager: verification, ensuring the integrity and confidentiality of the stored password database. """ - def __init__(self, fingerprint: Optional[str] = None) -> None: + def __init__( + self, fingerprint: Optional[str] = None, *, password: Optional[str] = None + ) -> None: """Initialize the PasswordManager. Parameters @@ -161,7 +163,7 @@ class PasswordManager: if fingerprint: # Load the specified profile without prompting - self.select_fingerprint(fingerprint) + self.select_fingerprint(fingerprint, password=password) else: # Ensure a parent seed is set up before accessing the fingerprint directory self.setup_parent_seed() @@ -187,6 +189,11 @@ class PasswordManager: ) ) + @staticmethod + def get_password_prompt() -> str: + """Return the standard prompt for requesting a master password.""" + return "Enter your master password: " + @property def parent_seed(self) -> Optional[str]: """Return the decrypted parent seed if set.""" @@ -269,12 +276,15 @@ class PasswordManager: self.config_manager = None self.locked = True - def unlock_vault(self) -> None: - """Prompt for password and reinitialize managers.""" + def unlock_vault(self, password: Optional[str] = None) -> None: + """Unlock the vault using ``password`` without prompting if provided.""" start = time.perf_counter() if not self.fingerprint_dir: raise ValueError("Fingerprint directory not set") - self.setup_encryption_manager(self.fingerprint_dir) + if password is None: + self.setup_encryption_manager(self.fingerprint_dir) + else: + self.setup_encryption_manager(self.fingerprint_dir, password) self.initialize_bip85() self.initialize_managers() self.locked = False @@ -394,7 +404,9 @@ class PasswordManager: print(colored(f"Error: Failed to add new seed profile: {e}", "red")) sys.exit(1) - def select_fingerprint(self, fingerprint: str) -> None: + def select_fingerprint( + self, fingerprint: str, *, password: Optional[str] = None + ) -> None: if self.fingerprint_manager.select_fingerprint(fingerprint): self.current_fingerprint = fingerprint # Add this line self.fingerprint_dir = ( @@ -409,7 +421,7 @@ class PasswordManager: ) sys.exit(1) # Setup the encryption manager and load parent seed - self.setup_encryption_manager(self.fingerprint_dir) + self.setup_encryption_manager(self.fingerprint_dir, password) # Initialize BIP85 and other managers self.initialize_bip85() self.initialize_managers() diff --git a/src/tests/test_noninteractive_init_unlock.py b/src/tests/test_noninteractive_init_unlock.py new file mode 100644 index 0000000..d239fb4 --- /dev/null +++ b/src/tests/test_noninteractive_init_unlock.py @@ -0,0 +1,84 @@ +import importlib +import bcrypt +from pathlib import Path +from tempfile import TemporaryDirectory + +import constants +import password_manager.manager as manager_module +from utils.fingerprint_manager import FingerprintManager +from password_manager.config_manager import ConfigManager +from tests.helpers import TEST_SEED, TEST_PASSWORD, create_vault + + +def test_init_with_password(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + monkeypatch.setattr(Path, "home", lambda: tmp) + importlib.reload(constants) + importlib.reload(manager_module) + + fm = FingerprintManager(constants.APP_DIR) + fp = fm.add_fingerprint(TEST_SEED) + dir_path = constants.APP_DIR / fp + vault, _enc = create_vault(dir_path, TEST_SEED, TEST_PASSWORD) + cfg = ConfigManager(vault, dir_path) + cfg.set_password_hash( + bcrypt.hashpw(TEST_PASSWORD.encode(), bcrypt.gensalt()).decode() + ) + cfg.set_kdf_iterations(100_000) + + called = {} + + def fake_setup(self, path, pw=None, **_): + called["password"] = pw + return True + + monkeypatch.setattr( + manager_module.PasswordManager, "initialize_bip85", lambda self: None + ) + monkeypatch.setattr( + manager_module.PasswordManager, "initialize_managers", lambda self: None + ) + monkeypatch.setattr( + manager_module.PasswordManager, "setup_encryption_manager", fake_setup + ) + + pm = manager_module.PasswordManager(fingerprint=fp, password=TEST_PASSWORD) + assert called["password"] == TEST_PASSWORD + + +def test_unlock_with_password(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + monkeypatch.setattr(Path, "home", lambda: tmp) + importlib.reload(constants) + importlib.reload(manager_module) + + fm = FingerprintManager(constants.APP_DIR) + fp = fm.add_fingerprint(TEST_SEED) + dir_path = constants.APP_DIR / fp + vault, _enc = create_vault(dir_path, TEST_SEED, TEST_PASSWORD) + cfg = ConfigManager(vault, dir_path) + cfg.set_password_hash( + bcrypt.hashpw(TEST_PASSWORD.encode(), bcrypt.gensalt()).decode() + ) + + pm = manager_module.PasswordManager.__new__(manager_module.PasswordManager) + pm.fingerprint_dir = dir_path + pm.config_manager = cfg + pm.locked = True + called = {} + + def fake_setup(path, pw=None): + called["password"] = pw + + monkeypatch.setattr( + manager_module.PasswordManager, "initialize_bip85", lambda self: None + ) + monkeypatch.setattr( + manager_module.PasswordManager, "initialize_managers", lambda self: None + ) + pm.setup_encryption_manager = fake_setup + + pm.unlock_vault(TEST_PASSWORD) + assert called["password"] == TEST_PASSWORD