Merge pull request #743 from PR0M3TH3AN/codex/fix-aes-gcm-decryption-error-due-to-key-mismatch

Add legacy decryption fallback and tests
This commit is contained in:
thePR0M3TH3AN
2025-08-03 17:38:16 -04:00
committed by GitHub
2 changed files with 80 additions and 24 deletions

View File

@@ -94,6 +94,7 @@ class EncryptionManager:
(3) The core migration logic. Tries the new format first, then falls back
to the old one. This is the ONLY place decryption logic should live.
"""
try:
# Try the new V2 format first
if encrypted_data.startswith(b"V2:"):
try:
@@ -104,7 +105,9 @@ class EncryptionManager:
raise InvalidToken("AES-GCM payload too short")
return self.cipher.decrypt(nonce, ciphertext, None)
except InvalidTag as e:
logger.error("AES-GCM decryption failed: Invalid authentication tag.")
logger.error(
"AES-GCM decryption failed: Invalid authentication tag."
)
try:
result = self.fernet.decrypt(encrypted_data[3:])
logger.warning(
@@ -127,6 +130,27 @@ class EncryptionManager:
"Could not decrypt data with any available method."
) from e
except (InvalidToken, InvalidTag) as e:
if isinstance(e, InvalidToken) and str(e) == "AES-GCM payload too short":
raise
logger.error(f"FATAL: Could not decrypt data: {e}", exc_info=True)
try:
password = prompt_existing_password(
"Enter your master password for legacy decryption: "
)
legacy_key = _derive_legacy_key_from_password(password)
legacy_mgr = EncryptionManager(legacy_key, self.fingerprint_dir)
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
# --- All functions below this point now use the smart `decrypt_data` method ---
def resolve_relative_path(self, relative_path: Path) -> Path:

View File

@@ -0,0 +1,32 @@
import base64
import hashlib
import unicodedata
from helpers import TEST_PASSWORD
import seedpass.core.encryption as enc_module
from seedpass.core.encryption import EncryptionManager
from utils.key_derivation import derive_key_from_password
def _fast_legacy_key(password: str, iterations: int = 100_000) -> bytes:
normalized = unicodedata.normalize("NFKD", password).strip().encode("utf-8")
key = hashlib.pbkdf2_hmac("sha256", normalized, b"", 1, dklen=32)
return base64.urlsafe_b64encode(key)
def test_decrypt_data_password_fallback(tmp_path, monkeypatch):
monkeypatch.setattr(
enc_module, "_derive_legacy_key_from_password", _fast_legacy_key
)
monkeypatch.setattr(
enc_module, "prompt_existing_password", lambda *_a, **_k: TEST_PASSWORD
)
legacy_key = _fast_legacy_key(TEST_PASSWORD)
legacy_mgr = EncryptionManager(legacy_key, tmp_path)
payload = legacy_mgr.encrypt_data(b"secret")
new_key = derive_key_from_password(TEST_PASSWORD, "fp")
new_mgr = EncryptionManager(new_key, tmp_path)
assert new_mgr.decrypt_data(payload) == b"secret"