diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 45d9b78..030c9e2 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -128,7 +128,6 @@ class PasswordManager: if not self.fingerprint_dir: raise ValueError("Fingerprint directory not set") self.setup_encryption_manager(self.fingerprint_dir) - self.load_parent_seed(self.fingerprint_dir) self.initialize_bip85() self.initialize_managers() self.locked = False @@ -240,7 +239,6 @@ class PasswordManager: sys.exit(1) # Setup the encryption manager and load parent seed self.setup_encryption_manager(self.fingerprint_dir) - self.load_parent_seed(self.fingerprint_dir) # Initialize BIP85 and other managers self.initialize_bip85() self.initialize_managers() @@ -257,41 +255,37 @@ class PasswordManager: def setup_encryption_manager( self, fingerprint_dir: Path, password: Optional[str] = None - ): - """ - Sets up the EncryptionManager for the selected fingerprint. + ) -> None: + """Set up encryption for the current fingerprint and load the seed.""" - Parameters: - fingerprint_dir (Path): The directory corresponding to the fingerprint. - password (Optional[str]): The user's master password. - """ try: - # Prompt for password if not provided if password is None: password = prompt_existing_password("Enter your master password: ") - # Derive key using the configured encryption mode if seed is known - if self.parent_seed: - key = derive_index_key( - self.parent_seed, - password, - self.encryption_mode, - ) - else: - key = derive_key_from_password(password) - self.encryption_manager = EncryptionManager(key, fingerprint_dir) - self.vault = Vault(self.encryption_manager, fingerprint_dir) - logger.debug( - "EncryptionManager set up successfully for selected fingerprint." + + if not self.parent_seed: + seed_key = derive_key_from_password(password) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) + try: + self.parent_seed = seed_mgr.decrypt_parent_seed() + except Exception: + print(colored("Invalid password. Exiting.", "red")) + raise + + key = derive_index_key( + self.parent_seed, + password, + self.encryption_mode, ) - # Initialize ConfigManager before verifying password + self.encryption_manager = EncryptionManager(key, fingerprint_dir) + self.vault = Vault(self.encryption_manager, fingerprint_dir) + self.config_manager = ConfigManager( vault=self.vault, fingerprint_dir=fingerprint_dir, ) - # Verify the password - self.fingerprint_dir = fingerprint_dir # Ensure self.fingerprint_dir is set + self.fingerprint_dir = fingerprint_dir if not self.verify_password(password): print(colored("Invalid password. Exiting.", "red")) sys.exit(1) @@ -301,22 +295,23 @@ class PasswordManager: print(colored(f"Error: Failed to set up encryption: {e}", "red")) sys.exit(1) - def load_parent_seed(self, fingerprint_dir: Path): - """ - Loads and decrypts the parent seed from the fingerprint directory. + def load_parent_seed( + self, fingerprint_dir: Path, password: Optional[str] = None + ) -> None: + """Load and decrypt the parent seed using the password-only key.""" + + if self.parent_seed: + return + + if password is None: + password = prompt_existing_password("Enter your master password: ") - Parameters: - fingerprint_dir (Path): The directory corresponding to the fingerprint. - """ try: - self.parent_seed = self.encryption_manager.decrypt_parent_seed() - logger.debug( - f"Parent seed loaded for fingerprint {self.current_fingerprint}." - ) - # Initialize BIP85 with the parent seed + seed_key = derive_key_from_password(password) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) + self.parent_seed = seed_mgr.decrypt_parent_seed() seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() self.bip85 = BIP85(seed_bytes) - logger.debug("BIP-85 initialized successfully.") except Exception as e: logger.error(f"Failed to load parent seed: {e}") logger.error(traceback.format_exc()) @@ -364,9 +359,6 @@ class PasswordManager: # Set up the encryption manager with the new password and seed profile directory self.setup_encryption_manager(self.fingerprint_dir, password) - # Load the parent seed for the selected seed profile - self.load_parent_seed(self.fingerprint_dir) - # Initialize BIP85 and other managers self.initialize_bip85() self.initialize_managers() @@ -546,16 +538,19 @@ class PasswordManager: # Initialize EncryptionManager with key and fingerprint_dir password = prompt_for_password() - key = derive_index_key( + index_key = derive_index_key( parent_seed, password, self.encryption_mode, ) - self.encryption_manager = EncryptionManager(key, fingerprint_dir) + seed_key = derive_key_from_password(password) + + self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) self.vault = Vault(self.encryption_manager, fingerprint_dir) # Encrypt and save the parent seed - self.encryption_manager.encrypt_parent_seed(parent_seed) + seed_mgr.encrypt_parent_seed(parent_seed) logging.info("Parent seed encrypted and saved successfully.") # Store the hashed password @@ -678,25 +673,23 @@ class PasswordManager: # Prompt for password password = prompt_for_password() - # Derive key using the configured encryption mode - key = derive_index_key( + + index_key = derive_index_key( seed, password, self.encryption_mode, ) + seed_key = derive_key_from_password(password) - # Re-initialize EncryptionManager with the new key and fingerprint_dir - self.encryption_manager = EncryptionManager(key, fingerprint_dir) + self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) - # Initialize the vault now that encryption manager is available self.vault = Vault(self.encryption_manager, fingerprint_dir) - # Store the hashed password self.store_hashed_password(password) logging.info("User password hashed and stored successfully.") - # Encrypt and save the parent seed - self.encryption_manager.encrypt_parent_seed(seed) + seed_mgr.encrypt_parent_seed(seed) logging.info("Parent seed encrypted and saved successfully.") self.parent_seed = seed # Ensure this is a string @@ -1344,12 +1337,14 @@ class PasswordManager: mode, ) except Exception: - # Fallback for tests or invalid seeds new_key = derive_key_from_password(new_password) + + seed_key = derive_key_from_password(new_password) + seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir) + new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir) - # Re-encrypt sensitive files using the new manager - new_enc_mgr.encrypt_parent_seed(self.parent_seed) + seed_mgr.encrypt_parent_seed(self.parent_seed) self.vault.set_encryption_manager(new_enc_mgr) self.vault.save_index(index_data) self.config_manager.vault = self.vault diff --git a/src/tests/test_password_unlock_after_change.py b/src/tests/test_password_unlock_after_change.py new file mode 100644 index 0000000..ac717c1 --- /dev/null +++ b/src/tests/test_password_unlock_after_change.py @@ -0,0 +1,85 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace + +import bcrypt + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.encryption import EncryptionManager +from password_manager.vault import Vault +from password_manager.entry_management import EntryManager +from password_manager.config_manager import ConfigManager +from password_manager.manager import PasswordManager, EncryptionMode +from utils.key_derivation import derive_index_key, derive_key_from_password + +SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + +def test_password_change_and_unlock(monkeypatch): + with TemporaryDirectory() as tmpdir: + fp = Path(tmpdir) + old_pw = "oldpw" + new_pw = "newpw" + + # initial encryption setup + index_key = derive_index_key(SEED, old_pw, EncryptionMode.SEED_PLUS_PW) + seed_key = derive_key_from_password(old_pw) + enc_mgr = EncryptionManager(index_key, fp) + seed_mgr = EncryptionManager(seed_key, fp) + vault = Vault(enc_mgr, fp) + entry_mgr = EntryManager(vault, fp) + cfg_mgr = ConfigManager(vault, fp) + + vault.save_index({"passwords": {}}) + cfg_mgr.save_config( + { + "relays": [], + "pin_hash": "", + "password_hash": bcrypt.hashpw( + old_pw.encode(), bcrypt.gensalt() + ).decode(), + } + ) + seed_mgr.encrypt_parent_seed(SEED) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_PLUS_PW + pm.encryption_manager = enc_mgr + pm.entry_manager = entry_mgr + pm.config_manager = cfg_mgr + pm.vault = vault + pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr) + pm.fingerprint_dir = fp + pm.current_fingerprint = "fp" + pm.parent_seed = SEED + pm.nostr_client = SimpleNamespace(publish_json_to_nostr=lambda *a, **k: None) + + monkeypatch.setattr( + "password_manager.manager.prompt_existing_password", lambda *_: old_pw + ) + monkeypatch.setattr( + "password_manager.manager.prompt_for_password", lambda: new_pw + ) + monkeypatch.setattr( + "password_manager.manager.NostrClient", + lambda *a, **kw: SimpleNamespace( + publish_json_to_nostr=lambda *a, **k: None + ), + ) + + pm.change_password() + pm.lock_vault() + + monkeypatch.setattr( + "password_manager.manager.prompt_existing_password", lambda *_: new_pw + ) + monkeypatch.setattr(PasswordManager, "initialize_bip85", lambda self: None) + monkeypatch.setattr(PasswordManager, "initialize_managers", lambda self: None) + + pm.unlock_vault() + + assert pm.parent_seed == SEED + assert pm.verify_password(new_pw) + assert not pm.locked