From 4d9bcf6d3b011bcfad56b0f3619d809de9aa537d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:02:53 -0400 Subject: [PATCH] Add encryption mode change feature --- src/password_manager/config_manager.py | 12 +++++ src/password_manager/manager.py | 51 +++++++++++++++++++++ src/tests/test_encryption_mode_change.py | 56 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/tests/test_encryption_mode_change.py diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index b64842b..ac4b46a 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -12,6 +12,10 @@ import bcrypt from password_manager.vault import Vault from nostr.client import DEFAULT_RELAYS as DEFAULT_NOSTR_RELAYS +from utils.key_derivation import ( + EncryptionMode, + DEFAULT_ENCRYPTION_MODE, +) logger = logging.getLogger(__name__) @@ -41,6 +45,7 @@ class ConfigManager: "relays": list(DEFAULT_NOSTR_RELAYS), "pin_hash": "", "password_hash": "", + "encryption_mode": DEFAULT_ENCRYPTION_MODE.value, } try: data = self.vault.load_config() @@ -50,6 +55,7 @@ class ConfigManager: data.setdefault("relays", list(DEFAULT_NOSTR_RELAYS)) data.setdefault("pin_hash", "") data.setdefault("password_hash", "") + data.setdefault("encryption_mode", DEFAULT_ENCRYPTION_MODE.value) # Migrate legacy hashed_password.enc if present and password_hash is missing legacy_file = self.fingerprint_dir / "hashed_password.enc" @@ -113,3 +119,9 @@ class ConfigManager: config = self.load_config(require_pin=False) config["password_hash"] = password_hash self.save_config(config) + + def set_encryption_mode(self, mode: EncryptionMode) -> None: + """Persist the selected encryption mode in the config.""" + config = self.load_config(require_pin=False) + config["encryption_mode"] = mode.value + self.save_config(config) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index e576ee6..d2886a7 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -723,6 +723,7 @@ class PasswordManager: ) self.store_hashed_password(password) + self.config_manager.set_encryption_mode(self.encryption_mode) logging.info("User password hashed and stored successfully.") seed_mgr.encrypt_parent_seed(seed) @@ -1459,3 +1460,53 @@ class PasswordManager: except Exception as e: logging.error(f"Failed to change password: {e}", exc_info=True) print(colored(f"Error: Failed to change password: {e}", "red")) + + def change_encryption_mode(self, new_mode: EncryptionMode) -> None: + """Re-encrypt the index using a different encryption mode.""" + try: + password = prompt_existing_password("Enter your current master password: ") + if not self.verify_password(password): + print(colored("Incorrect password.", "red")) + return + + index_data = self.vault.load_index() + config_data = self.config_manager.load_config(require_pin=False) + + new_key = derive_index_key(self.parent_seed, password, new_mode) + new_mgr = EncryptionManager(new_key, self.fingerprint_dir) + + self.vault.set_encryption_manager(new_mgr) + self.vault.save_index(index_data) + self.config_manager.vault = self.vault + config_data["encryption_mode"] = new_mode.value + self.config_manager.save_config(config_data) + + self.encryption_manager = new_mgr + self.password_generator.encryption_manager = new_mgr + self.encryption_mode = new_mode + + relay_list = config_data.get("relays", list(DEFAULT_RELAYS)) + self.nostr_client = NostrClient( + encryption_manager=self.encryption_manager, + fingerprint=self.current_fingerprint, + relays=relay_list, + parent_seed=getattr(self, "parent_seed", None), + ) + + print(colored("Encryption mode changed successfully.", "green")) + + try: + encrypted_data = self.get_encrypted_data() + if encrypted_data: + summary = f"mode-change-{int(time.time())}" + self.nostr_client.publish_json_to_nostr( + encrypted_data, + alt_summary=summary, + ) + except Exception as nostr_error: + logging.error( + f"Failed to post updated index to Nostr after encryption mode change: {nostr_error}" + ) + except Exception as e: + logging.error(f"Failed to change encryption mode: {e}", exc_info=True) + print(colored(f"Error: Failed to change encryption mode: {e}", "red")) diff --git a/src/tests/test_encryption_mode_change.py b/src/tests/test_encryption_mode_change.py new file mode 100644 index 0000000..b2c225e --- /dev/null +++ b/src/tests/test_encryption_mode_change.py @@ -0,0 +1,56 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from unittest.mock import patch + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.entry_management import EntryManager +from password_manager.config_manager import ConfigManager +from password_manager.vault import Vault +from password_manager.manager import PasswordManager +from utils.key_derivation import EncryptionMode + + +def test_change_encryption_mode(monkeypatch): + with TemporaryDirectory() as tmpdir: + fp = Path(tmpdir) + vault, enc_mgr = create_vault( + fp, TEST_SEED, TEST_PASSWORD, EncryptionMode.SEED_ONLY + ) + entry_mgr = EntryManager(vault, fp) + cfg_mgr = ConfigManager(vault, fp) + vault.save_index({"passwords": {}}) + + pm = PasswordManager.__new__(PasswordManager) + 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 = TEST_SEED + pm.encryption_mode = EncryptionMode.SEED_ONLY + + monkeypatch.setattr( + "password_manager.manager.prompt_existing_password", + lambda *_: TEST_PASSWORD, + ) + pm.verify_password = lambda pw: True + + with patch("password_manager.manager.NostrClient") as MockClient: + mock = MockClient.return_value + pm.nostr_client = mock + pm.change_encryption_mode(EncryptionMode.SEED_PLUS_PW) + mock.publish_json_to_nostr.assert_called_once() + + assert pm.encryption_mode is EncryptionMode.SEED_PLUS_PW + assert pm.password_generator.encryption_manager is pm.encryption_manager + loaded = vault.load_index() + assert loaded["passwords"] == {} + cfg = cfg_mgr.load_config(require_pin=False) + assert cfg["encryption_mode"] == EncryptionMode.SEED_PLUS_PW.value