Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions examples/src/instruction_file_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,10 @@ def instruction_file_get(

# 2. Decrypt using a custom instruction file suffix.
# The client will fetch "<key>.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"
4 changes: 1 addition & 3 deletions examples/test/test_i_instruction_file_example.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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")
Expand Down
35 changes: 21 additions & 14 deletions src/s3_encryption/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions test/integration/test_i_s3_encryption_instruction_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
Loading