mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Support multiple legacy PBKDF2 iterations
This commit is contained in:
@@ -161,25 +161,41 @@ class EncryptionManager:
|
||||
raise InvalidToken(
|
||||
"User declined legacy decryption or provided invalid choice."
|
||||
) from e
|
||||
try:
|
||||
password = prompt_existing_password(
|
||||
"Enter your master password for legacy decryption: "
|
||||
)
|
||||
legacy_key = _derive_legacy_key_from_password(
|
||||
password, iterations=50_000
|
||||
)
|
||||
legacy_mgr = EncryptionManager(legacy_key, self.fingerprint_dir)
|
||||
legacy_mgr._legacy_migrate_flag = False
|
||||
result = legacy_mgr.decrypt_data(encrypted_data)
|
||||
logger.warning(
|
||||
"Data decrypted using legacy password-only key derivation."
|
||||
)
|
||||
return result
|
||||
except Exception as e2: # pragma: no cover - exceptional path
|
||||
logger.error(f"Failed legacy decryption attempt: {e2}", exc_info=True)
|
||||
raise InvalidToken(
|
||||
"Could not decrypt data with any available method."
|
||||
) from e
|
||||
password = prompt_existing_password(
|
||||
"Enter your master password for legacy decryption: "
|
||||
)
|
||||
last_exc: Optional[Exception] = None
|
||||
for iter_count in [50_000, 100_000]:
|
||||
try:
|
||||
legacy_key = _derive_legacy_key_from_password(
|
||||
password, iterations=iter_count
|
||||
)
|
||||
legacy_mgr = EncryptionManager(legacy_key, self.fingerprint_dir)
|
||||
legacy_mgr._legacy_migrate_flag = False
|
||||
result = legacy_mgr.decrypt_data(encrypted_data)
|
||||
try: # record iteration count for future runs
|
||||
from .vault import Vault
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
cfg_mgr = ConfigManager(
|
||||
Vault(self, self.fingerprint_dir), self.fingerprint_dir
|
||||
)
|
||||
cfg_mgr.set_kdf_iterations(iter_count)
|
||||
except Exception: # pragma: no cover - best effort
|
||||
logger.error(
|
||||
"Failed to record PBKDF2 iteration count in config",
|
||||
exc_info=True,
|
||||
)
|
||||
logger.warning(
|
||||
"Data decrypted using legacy password-only key derivation."
|
||||
)
|
||||
return result
|
||||
except Exception as e2: # pragma: no cover - try next iteration
|
||||
last_exc = e2
|
||||
logger.error(f"Failed legacy decryption attempt: {last_exc}", exc_info=True)
|
||||
raise InvalidToken(
|
||||
"Could not decrypt data with any available method."
|
||||
) from e
|
||||
|
||||
# --- All functions below this point now use the smart `decrypt_data` method ---
|
||||
|
||||
|
66
src/tests/test_legacy_migration_iterations.py
Normal file
66
src/tests/test_legacy_migration_iterations.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import pytest
|
||||
|
||||
import seedpass.core.encryption as enc_module
|
||||
from helpers import TEST_PASSWORD
|
||||
from seedpass.core.encryption import (
|
||||
EncryptionManager,
|
||||
_derive_legacy_key_from_password,
|
||||
)
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
from seedpass.core.vault import Vault
|
||||
|
||||
|
||||
def _setup_legacy_file(tmp_path: Path, iterations: int) -> None:
|
||||
legacy_key = _derive_legacy_key_from_password(TEST_PASSWORD, iterations=iterations)
|
||||
mgr = EncryptionManager(legacy_key, tmp_path)
|
||||
data = {"entries": {"0": {"kind": "test"}}}
|
||||
json_bytes = json.dumps(data, separators=(",", ":")).encode("utf-8")
|
||||
legacy_encrypted = mgr.fernet.encrypt(json_bytes)
|
||||
(tmp_path / "seedpass_entries_db.json.enc").write_bytes(legacy_encrypted)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", [50_000, 100_000])
|
||||
def test_migrate_iterations(tmp_path, monkeypatch, iterations):
|
||||
_setup_legacy_file(tmp_path, iterations)
|
||||
|
||||
new_key = base64.urlsafe_b64encode(b"B" * 32)
|
||||
mgr = EncryptionManager(new_key, tmp_path)
|
||||
|
||||
prompts: list[int] = []
|
||||
|
||||
def fake_prompt(_msg: str) -> str:
|
||||
prompts.append(1)
|
||||
return TEST_PASSWORD
|
||||
|
||||
monkeypatch.setattr(enc_module, "prompt_existing_password", fake_prompt)
|
||||
monkeypatch.setattr("builtins.input", lambda *_a, **_k: "2")
|
||||
|
||||
calls: list[int] = []
|
||||
orig_derive = enc_module._derive_legacy_key_from_password
|
||||
|
||||
def tracking_derive(password: str, iterations: int = 100_000) -> bytes:
|
||||
calls.append(iterations)
|
||||
return orig_derive(password, iterations=iterations)
|
||||
|
||||
monkeypatch.setattr(enc_module, "_derive_legacy_key_from_password", tracking_derive)
|
||||
|
||||
mgr.load_json_data()
|
||||
# Loading again should not prompt for password or attempt legacy counts
|
||||
mgr.load_json_data()
|
||||
|
||||
assert prompts == [1]
|
||||
expected = [50_000] if iterations == 50_000 else [50_000, 100_000]
|
||||
assert calls == expected
|
||||
|
||||
cfg = ConfigManager(Vault(mgr, tmp_path), tmp_path)
|
||||
assert cfg.get_kdf_iterations() == iterations
|
||||
|
||||
content = (tmp_path / "seedpass_entries_db.json.enc").read_bytes()
|
||||
assert content.startswith(b"V2:")
|
Reference in New Issue
Block a user