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,35 +94,59 @@ 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 the new V2 format first try:
if encrypted_data.startswith(b"V2:"): # Try the new V2 format first
try: if encrypted_data.startswith(b"V2:"):
nonce = encrypted_data[3:15]
ciphertext = encrypted_data[15:]
if len(ciphertext) < 16:
logger.error("AES-GCM payload too short")
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.")
try: try:
result = self.fernet.decrypt(encrypted_data[3:]) nonce = encrypted_data[3:15]
logger.warning( ciphertext = encrypted_data[15:]
"Legacy-format file had incorrect 'V2:' header; decrypted with Fernet" if len(ciphertext) < 16:
logger.error("AES-GCM payload too short")
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."
) )
return result try:
except InvalidToken: result = self.fernet.decrypt(encrypted_data[3:])
raise InvalidToken("AES-GCM decryption failed.") from e logger.warning(
"Legacy-format file had incorrect 'V2:' header; decrypted with Fernet"
)
return result
except InvalidToken:
raise InvalidToken("AES-GCM decryption failed.") from e
# If it's not V2, it must be the legacy Fernet format # If it's not V2, it must be the legacy Fernet format
else: else:
logger.warning("Data is in legacy Fernet format. Attempting migration.") logger.warning("Data is in legacy Fernet format. Attempting migration.")
try:
return self.fernet.decrypt(encrypted_data)
except InvalidToken as e:
logger.error(
"Legacy Fernet decryption failed. Vault may be corrupt or key is incorrect."
)
raise InvalidToken(
"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: try:
return self.fernet.decrypt(encrypted_data) password = prompt_existing_password(
except InvalidToken as e: "Enter your master password for legacy decryption: "
logger.error(
"Legacy Fernet decryption failed. Vault may be corrupt or key is incorrect."
) )
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( raise InvalidToken(
"Could not decrypt data with any available method." "Could not decrypt data with any available method."
) from e ) from e

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"