Merge pull request #472 from PR0M3TH3AN/codex/add-kdf_iterations-setting-to-configmanager

Support configurable PBKDF2 iterations
This commit is contained in:
thePR0M3TH3AN
2025-07-12 09:19:03 -04:00
committed by GitHub
3 changed files with 57 additions and 7 deletions

View File

@@ -44,6 +44,7 @@ class ConfigManager:
"pin_hash": "", "pin_hash": "",
"password_hash": "", "password_hash": "",
"inactivity_timeout": INACTIVITY_TIMEOUT, "inactivity_timeout": INACTIVITY_TIMEOUT,
"kdf_iterations": 100_000,
"additional_backup_path": "", "additional_backup_path": "",
"secret_mode_enabled": False, "secret_mode_enabled": False,
"clipboard_clear_delay": 45, "clipboard_clear_delay": 45,
@@ -57,6 +58,7 @@ class ConfigManager:
data.setdefault("pin_hash", "") data.setdefault("pin_hash", "")
data.setdefault("password_hash", "") data.setdefault("password_hash", "")
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT) data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
data.setdefault("kdf_iterations", 100_000)
data.setdefault("additional_backup_path", "") data.setdefault("additional_backup_path", "")
data.setdefault("secret_mode_enabled", False) data.setdefault("secret_mode_enabled", False)
data.setdefault("clipboard_clear_delay", 45) data.setdefault("clipboard_clear_delay", 45)
@@ -137,6 +139,19 @@ class ConfigManager:
config = self.load_config(require_pin=False) config = self.load_config(require_pin=False)
return float(config.get("inactivity_timeout", INACTIVITY_TIMEOUT)) 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: def set_additional_backup_path(self, path: Optional[str]) -> None:
"""Persist an optional additional backup path in the config.""" """Persist an optional additional backup path in the config."""
config = self.load_config(require_pin=False) config = self.load_config(require_pin=False)

View File

@@ -381,7 +381,12 @@ class PasswordManager:
if password is None: if password is None:
password = prompt_existing_password("Enter your master password: ") 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) seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
try: try:
self.parent_seed = seed_mgr.decrypt_parent_seed() self.parent_seed = seed_mgr.decrypt_parent_seed()
@@ -428,7 +433,12 @@ class PasswordManager:
password = prompt_existing_password("Enter your master password: ") password = prompt_existing_password("Enter your master password: ")
try: 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) seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
self.parent_seed = seed_mgr.decrypt_parent_seed() self.parent_seed = seed_mgr.decrypt_parent_seed()
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
@@ -572,7 +582,12 @@ class PasswordManager:
password = getpass.getpass(prompt="Enter your login password: ").strip() password = getpass.getpass(prompt="Enter your login password: ").strip()
# Derive encryption key from password # 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 # Initialize FingerprintManager if not already initialized
if not self.fingerprint_manager: if not self.fingerprint_manager:
@@ -692,7 +707,8 @@ class PasswordManager:
# Initialize EncryptionManager with key and fingerprint_dir # Initialize EncryptionManager with key and fingerprint_dir
password = prompt_for_password() password = prompt_for_password()
index_key = derive_index_key(parent_seed) 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) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
seed_mgr = EncryptionManager(seed_key, fingerprint_dir) seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
@@ -833,7 +849,12 @@ class PasswordManager:
password = prompt_for_password() password = prompt_for_password()
index_key = derive_index_key(seed) 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) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
seed_mgr = EncryptionManager(seed_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): "): if confirm_action("Encrypt export with a password? (Y/N): "):
password = prompt_new_password() 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) enc_mgr = EncryptionManager(key, dest.parent)
data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8")) data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8"))
dest = dest.with_suffix(dest.suffix + ".enc") dest = dest.with_suffix(dest.suffix + ".enc")
@@ -3569,7 +3591,8 @@ class PasswordManager:
# Create a new encryption manager with the new password # Create a new encryption manager with the new password
new_key = derive_index_key(self.parent_seed) 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) seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir) new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir)

View File

@@ -23,6 +23,7 @@ def test_config_defaults_and_round_trip():
assert cfg["pin_hash"] == "" assert cfg["pin_hash"] == ""
assert cfg["password_hash"] == "" assert cfg["password_hash"] == ""
assert cfg["additional_backup_path"] == "" assert cfg["additional_backup_path"] == ""
assert cfg["kdf_iterations"] == 100_000
cfg_mgr.set_pin("1234") cfg_mgr.set_pin("1234")
cfg_mgr.set_relays(["wss://example.com"], require_pin=False) 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) cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["secret_mode_enabled"] is True assert cfg2["secret_mode_enabled"] is True
assert cfg2["clipboard_clear_delay"] == 99 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