@@ -316,6 +316,30 @@ def decrypt(
316316 # Determine the algorithm suite from the metadata
317317 algorithm_suite = self ._determine_algorithm_suite (metadata )
318318
319+ # Reject metadata that contains keys from multiple format versions.
320+ # This prevents format confusion attacks where an attacker injects
321+ # V2 keys via an instruction file to bypass V3 key-commitment verification.
322+ if metadata .has_exclusive_key_collision ():
323+ raise S3EncryptionClientError (
324+ "Object metadata contains keys from multiple format versions. "
325+ "The object or its instruction file may have been tampered with."
326+ )
327+
328+ # Also reject V2 format metadata that contains V3 content keys.
329+ # In the instruction file injection scenario, the attacker replaces
330+ # V3 EDK keys with V2 keys, but V3 content keys (x-amz-c, x-amz-d,
331+ # x-amz-i) remain from the object metadata. This combination is
332+ # never produced by legitimate encryption.
333+ if metadata .is_v2_format () and (
334+ metadata .content_cipher_v3 is not None
335+ or metadata .key_commitment_v3 is not None
336+ or metadata .message_id_v3 is not None
337+ ):
338+ raise S3EncryptionClientError ( # pragma: no cover — only reachable via instruction file merge; covered by TestInstructionFileFormatConfusion
339+ "Object metadata contains V2 format keys alongside V3 content keys. "
340+ "The object or its instruction file may have been tampered with."
341+ )
342+
319343 # Determine which format we're dealing with and get decryption materials
320344 if metadata .is_v1_format ():
321345 dec_materials = self ._decrypt_v1 (metadata , encryption_context )
@@ -590,7 +614,13 @@ def _decrypt_v3(self, metadata, encryption_context) -> DecryptionMaterials:
590614
591615 # Map V3 compressed wrapping algorithm to canonical key_provider_info
592616 raw_wrap_alg = metadata .encrypted_data_key_algorithm_v3 or "12"
593- wrap_alg = self ._V3_WRAP_ALG_MAP .get (raw_wrap_alg , raw_wrap_alg )
617+ wrap_alg = self ._V3_WRAP_ALG_MAP .get (raw_wrap_alg )
618+ if wrap_alg is None :
619+ raise S3EncryptionClientError (
620+ f"Unknown V3 wrapping algorithm: '{ raw_wrap_alg } '. "
621+ f"Valid values are: { list (self ._V3_WRAP_ALG_MAP .keys ())} . "
622+ f"The object metadata may have been tampered with."
623+ )
594624
595625 encrypted_data_key = EncryptedDataKey (
596626 key_provider_id = b"S3Keyring" ,
@@ -607,8 +637,13 @@ def _decrypt_v3(self, metadata, encryption_context) -> DecryptionMaterials:
607637 stored_context = {}
608638 if wrap_alg == "kms+context" :
609639 raw_ctx = metadata .encryption_context_v3
610- else :
640+ elif wrap_alg in ( "AES/GCM" , "RSA-OAEP-SHA1" ) :
611641 raw_ctx = metadata .mat_desc_v3
642+ else :
643+ raise S3EncryptionClientError ( # pragma: no cover — defense in depth, unreachable
644+ f"Unexpected V3 wrapping algorithm for context selection: '{ wrap_alg } '. "
645+ f"The object metadata may have been tampered with."
646+ )
612647
613648 if raw_ctx is not None :
614649 if isinstance (raw_ctx , dict ):
0 commit comments