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 (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. to the old one. This is the ONLY place decryption logic should live.
""" """
try:
# Try the new V2 format first # Try the new V2 format first
if encrypted_data.startswith(b"V2:"): if encrypted_data.startswith(b"V2:"):
try: try:
@@ -104,7 +105,9 @@ class EncryptionManager:
raise InvalidToken("AES-GCM payload too short") raise InvalidToken("AES-GCM payload too short")
return self.cipher.decrypt(nonce, ciphertext, None) return self.cipher.decrypt(nonce, ciphertext, None)
except InvalidTag as e: except InvalidTag as e:
logger.error("AES-GCM decryption failed: Invalid authentication tag.") logger.error(
"AES-GCM decryption failed: Invalid authentication tag."
)
try: try:
result = self.fernet.decrypt(encrypted_data[3:]) result = self.fernet.decrypt(encrypted_data[3:])
logger.warning( logger.warning(
@@ -127,6 +130,27 @@ class EncryptionManager:
"Could not decrypt data with any available method." "Could not decrypt data with any available method."
) from e ) 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 --- # --- All functions below this point now use the smart `decrypt_data` method ---
def resolve_relative_path(self, relative_path: Path) -> Path: 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"