From 888d50a6a743a65886bd06fe1c021ba69fc883e2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 12 Jul 2025 09:18:17 -0400 Subject: [PATCH] Add configurable KDF iterations --- src/password_manager/config_manager.py | 15 +++++++++++ src/password_manager/manager.py | 37 +++++++++++++++++++++----- src/tests/test_config_manager.py | 12 +++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index b1c8b8e..68a154e 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -44,6 +44,7 @@ class ConfigManager: "pin_hash": "", "password_hash": "", "inactivity_timeout": INACTIVITY_TIMEOUT, + "kdf_iterations": 100_000, "additional_backup_path": "", "secret_mode_enabled": False, "clipboard_clear_delay": 45, @@ -57,6 +58,7 @@ class ConfigManager: data.setdefault("pin_hash", "") data.setdefault("password_hash", "") data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT) + data.setdefault("kdf_iterations", 100_000) data.setdefault("additional_backup_path", "") data.setdefault("secret_mode_enabled", False) data.setdefault("clipboard_clear_delay", 45) @@ -137,6 +139,19 @@ class ConfigManager: config = self.load_config(require_pin=False) return float(config.get("inactivity_timeout", INACTIVITY_TIMEOUT)) + def set_kdf_iterations(self, iterations: int) -> None: + """Persist the PBKDF2 iteration count in the config.""" + if iterations <= 0: + raise ValueError("Iterations must be positive") + config = self.load_config(require_pin=False) + config["kdf_iterations"] = int(iterations) + self.save_config(config) + + def get_kdf_iterations(self) -> int: + """Retrieve the PBKDF2 iteration count.""" + config = self.load_config(require_pin=False) + return int(config.get("kdf_iterations", 100_000)) + def set_additional_backup_path(self, path: Optional[str]) -> None: """Persist an optional additional backup path in the config.""" config = self.load_config(require_pin=False) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 428d7ce..87127a3 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -381,7 +381,12 @@ class PasswordManager: if password is None: password = prompt_existing_password("Enter your master password: ") - seed_key = derive_key_from_password(password) + iterations = ( + self.config_manager.get_kdf_iterations() + if getattr(self, "config_manager", None) + else 100_000 + ) + seed_key = derive_key_from_password(password, iterations=iterations) seed_mgr = EncryptionManager(seed_key, fingerprint_dir) try: self.parent_seed = seed_mgr.decrypt_parent_seed() @@ -428,7 +433,12 @@ class PasswordManager: password = prompt_existing_password("Enter your master password: ") try: - seed_key = derive_key_from_password(password) + iterations = ( + self.config_manager.get_kdf_iterations() + if getattr(self, "config_manager", None) + else 100_000 + ) + seed_key = derive_key_from_password(password, iterations=iterations) seed_mgr = EncryptionManager(seed_key, fingerprint_dir) self.parent_seed = seed_mgr.decrypt_parent_seed() seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() @@ -572,7 +582,12 @@ class PasswordManager: password = getpass.getpass(prompt="Enter your login password: ").strip() # Derive encryption key from password - key = derive_key_from_password(password) + iterations = ( + self.config_manager.get_kdf_iterations() + if getattr(self, "config_manager", None) + else 100_000 + ) + key = derive_key_from_password(password, iterations=iterations) # Initialize FingerprintManager if not already initialized if not self.fingerprint_manager: @@ -692,7 +707,8 @@ class PasswordManager: # Initialize EncryptionManager with key and fingerprint_dir password = prompt_for_password() index_key = derive_index_key(parent_seed) - seed_key = derive_key_from_password(password) + iterations = self.config_manager.get_kdf_iterations() + seed_key = derive_key_from_password(password, iterations=iterations) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) seed_mgr = EncryptionManager(seed_key, fingerprint_dir) @@ -833,7 +849,12 @@ class PasswordManager: password = prompt_for_password() index_key = derive_index_key(seed) - seed_key = derive_key_from_password(password) + iterations = ( + self.config_manager.get_kdf_iterations() + if getattr(self, "config_manager", None) + else 100_000 + ) + seed_key = derive_key_from_password(password, iterations=iterations) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) seed_mgr = EncryptionManager(seed_key, fingerprint_dir) @@ -3357,7 +3378,8 @@ class PasswordManager: if confirm_action("Encrypt export with a password? (Y/N): "): password = prompt_new_password() - key = derive_key_from_password(password) + iterations = self.config_manager.get_kdf_iterations() + key = derive_key_from_password(password, iterations=iterations) enc_mgr = EncryptionManager(key, dest.parent) data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8")) dest = dest.with_suffix(dest.suffix + ".enc") @@ -3569,7 +3591,8 @@ class PasswordManager: # Create a new encryption manager with the new password new_key = derive_index_key(self.parent_seed) - seed_key = derive_key_from_password(new_password) + iterations = self.config_manager.get_kdf_iterations() + seed_key = derive_key_from_password(new_password, iterations=iterations) seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir) new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir) diff --git a/src/tests/test_config_manager.py b/src/tests/test_config_manager.py index b035a6b..385657d 100644 --- a/src/tests/test_config_manager.py +++ b/src/tests/test_config_manager.py @@ -23,6 +23,7 @@ def test_config_defaults_and_round_trip(): assert cfg["pin_hash"] == "" assert cfg["password_hash"] == "" assert cfg["additional_backup_path"] == "" + assert cfg["kdf_iterations"] == 100_000 cfg_mgr.set_pin("1234") cfg_mgr.set_relays(["wss://example.com"], require_pin=False) @@ -146,3 +147,14 @@ def test_secret_mode_round_trip(): cfg2 = cfg_mgr.load_config(require_pin=False) assert cfg2["secret_mode_enabled"] is True assert cfg2["clipboard_clear_delay"] == 99 + + +def test_kdf_iterations_round_trip(): + with TemporaryDirectory() as tmpdir: + vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, Path(tmpdir)) + + assert cfg_mgr.get_kdf_iterations() == 100_000 + + cfg_mgr.set_kdf_iterations(200_000) + assert cfg_mgr.get_kdf_iterations() == 200_000