diff --git a/examples/src/instruction_file_example.py b/examples/src/instruction_file_example.py index 9364b7a7..b93e3307 100644 --- a/examples/src/instruction_file_example.py +++ b/examples/src/instruction_file_example.py @@ -47,12 +47,10 @@ def instruction_file_get( # 2. Decrypt using a custom instruction file suffix. # The client will fetch ".custom-suffix-instruction" for the encryption metadata. - custom_config = S3EncryptionClientConfig( - keyring=keyring, - instruction_file_suffix=".custom-suffix-instruction", + # InstructionFileSuffix is a per-request keyword argument on get_object, + # so the same client can use different suffixes per request. + response = s3ec.get_object( + Bucket=bucket, Key=key, InstructionFileSuffix=".custom-suffix-instruction" ) - custom_s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=custom_config) - - response = custom_s3ec.get_object(Bucket=bucket, Key=key) plaintext = response["Body"].read() assert plaintext == expected_plaintext, "Custom suffix: decrypted plaintext does not match" diff --git a/examples/test/test_i_instruction_file_example.py b/examples/test/test_i_instruction_file_example.py index 1aadafb3..938147f8 100644 --- a/examples/test/test_i_instruction_file_example.py +++ b/examples/test/test_i_instruction_file_example.py @@ -1,6 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """Test suite for the instruction file example.""" + import boto3 import pytest @@ -13,9 +14,6 @@ KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:key/a3889cd9-99eb-4138-a93a-aea9d52ec2ef" -# TODO(#152): Move instruction_file_suffix from config to get_object request context -# so a single S3EncryptionClient can use different suffixes per request. -@pytest.mark.xfail(reason="instruction_file_suffix is per-client, not per-request") def test_instruction_file_get(): s3_client = boto3.client("s3", region_name="us-west-2") kms_client = boto3.client("kms", region_name="us-west-2") diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index 9b8772d6..cd5a1a7b 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -26,10 +26,16 @@ _CTX_KEY = "key" _CTX_S3_CLIENT = "s3_client" _CTX_INSTRUCTION_FILE_MODE = "instruction_file_mode" +_CTX_INSTRUCTION_FILE_SUFFIX = "instruction_file_suffix" # Attributes to clean up after get_object completes # (s3_client is intentionally excluded — it is not request-scoped) -_GET_OBJECT_CLEANUP_ATTRS = (_CTX_ENCRYPTION_CONTEXT, _CTX_BUCKET, _CTX_KEY) +_GET_OBJECT_CLEANUP_ATTRS = ( + _CTX_ENCRYPTION_CONTEXT, + _CTX_BUCKET, + _CTX_KEY, + _CTX_INSTRUCTION_FILE_SUFFIX, +) @define @@ -46,8 +52,6 @@ class S3EncryptionClientConfig: encrypted with legacy CBC algorithm suites. Defaults to False. cmm: Crypto materials manager. Defaults to a DefaultCryptoMaterialsManager wrapping the provided keyring. - instruction_file_suffix: Suffix appended to the S3 object key when - fetching instruction files. Defaults to ".instruction". enable_delayed_authentication: If True, release plaintext from streams before GCM tag verification. Defaults to False. Has no effect for CBC encrypted ciphertext, which is always streamed as there is no @@ -71,16 +75,6 @@ class S3EncryptionClientConfig: ##% The option to enable legacy unauthenticated modes MUST be set to false by default. enable_legacy_unauthenticated_modes: bool = field(default=False) cmm: AbstractCryptoMaterialsManager = field() - ##= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file - ##= type=implementation - ##% The S3EC SHOULD support providing a custom Instruction File suffix - ##% on GetObject requests, regardless of whether or not re-encryption is supported. - - ##= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file - ##= type=implementation - ##% The default Instruction File behavior uses the same S3 object key - ##% as its associated object suffixed with ".instruction". - instruction_file_suffix: str = field(default=".instruction") ##= specification/s3-encryption/client.md#enable-delayed-authentication ##= type=implementation @@ -251,7 +245,7 @@ def on_get_object_after_call(self, parsed, **kwargs): ) decrypted_data = pipeline.decrypt( response, - instruction_suffix=self.config.instruction_file_suffix, + instruction_suffix=getattr(self._context, _CTX_INSTRUCTION_FILE_SUFFIX, ".instruction"), enable_delayed_authentication=self.config.enable_delayed_authentication, encryption_context=encryption_context, bucket=getattr(self._context, _CTX_BUCKET, None), @@ -361,6 +355,8 @@ def get_object(self, **kwargs): Args: **kwargs: Arguments to pass to the S3 client's get_object method. May include EncryptionContext if it was used during encryption. + May include InstructionFileSuffix to override the default + ".instruction" suffix for instruction file lookups. Returns: The response from the S3 client's get_object method with the Body @@ -371,9 +367,20 @@ def get_object(self, **kwargs): """ # Extract EncryptionContext if provided (not a standard S3 parameter) encryption_context = kwargs.pop("EncryptionContext", None) + ##= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file + ##= type=implementation + ##% The S3EC SHOULD support providing a custom Instruction File suffix + ##% on GetObject requests, regardless of whether or not re-encryption is supported. + + ##= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file + ##= type=implementation + ##% The default Instruction File behavior uses the same S3 object key + ##% as its associated object suffixed with ".instruction". + instruction_file_suffix = kwargs.pop("InstructionFileSuffix", ".instruction") # Store encryption context in thread-local storage for the event handler setattr(self._plugin._context, _CTX_ENCRYPTION_CONTEXT, encryption_context) + setattr(self._plugin._context, _CTX_INSTRUCTION_FILE_SUFFIX, instruction_file_suffix) # Store wrapped client in thread-local storage for # the event handler to fetch instruction files setattr(self._plugin._context, _CTX_S3_CLIENT, self.wrapped_s3_client) diff --git a/test/integration/test_i_s3_encryption_instruction_file.py b/test/integration/test_i_s3_encryption_instruction_file.py index c56883f2..0c8b2b2a 100644 --- a/test/integration/test_i_s3_encryption_instruction_file.py +++ b/test/integration/test_i_s3_encryption_instruction_file.py @@ -137,12 +137,13 @@ def test_decrypt_v3_instruction_file_custom_suffix(): wrapped_client = boto3.client("s3") config = S3EncryptionClientConfig( keyring, - instruction_file_suffix=".custom-suffix-instruction", commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, ) s3ec = S3EncryptionClient(wrapped_client, config) - response = s3ec.get_object(Bucket=bucket, Key=key) + response = s3ec.get_object( + Bucket=bucket, Key=key, InstructionFileSuffix=".custom-suffix-instruction" + ) output = response["Body"].read().decode("utf-8") assert output == "static-v3-instruction-file-from-java-v4" @@ -160,13 +161,14 @@ def test_decrypt_v2_instruction_file_custom_suffix(delayed_auth): config = S3EncryptionClientConfig( keyring, encryption_algorithm=AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF, - instruction_file_suffix=".custom-suffix-instruction", commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, enable_delayed_authentication=delayed_auth, ) s3ec = S3EncryptionClient(wrapped_client, config) - response = s3ec.get_object(Bucket=bucket, Key=key) + response = s3ec.get_object( + Bucket=bucket, Key=key, InstructionFileSuffix=".custom-suffix-instruction" + ) output = response["Body"].read().decode("utf-8") assert output == "static-v2-instruction-file-from-java-v4"