Skip to content

Commit 30a135c

Browse files
committed
feat(examples): add usage examples with integration tests
- KMS Keyring put/get roundtrip with encryption context - Legacy V1 object decryption with enable_legacy_wrapping_algorithms - Delayed authentication streaming decryption for large files - Instruction file decryption with default and custom suffixes (xfail, #152) - Register examples pytest mark in pyproject.toml - Add examples step to CI workflow
1 parent ce010b3 commit 30a135c

13 files changed

Lines changed: 401 additions & 0 deletions

.github/workflows/python-integ.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ jobs:
5656
CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }}
5757
CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }}
5858

59+
- name: Run examples
60+
run: uv run pytest examples/ -v -m examples
61+
5962
- name: Generate coverage HTML report
6063
if: always()
6164
run: uv run coverage html -d coverage-report

examples/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0

examples/src/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
This example demonstrates streaming decryption with delayed authentication
5+
using the S3 Encryption Client.
6+
7+
By default, the S3 Encryption Client buffers the entire ciphertext and verifies
8+
the authentication tag before releasing any plaintext. This is the safest mode,
9+
but requires holding the entire object in memory.
10+
11+
With delayed authentication enabled, plaintext is released incrementally as it
12+
is decrypted, before the authentication tag has been verified. This allows
13+
processing large files without buffering the entire object in memory.
14+
15+
WARNING: With delayed authentication, plaintext is released before it has been
16+
authenticated. An attacker could modify the ciphertext and the client would
17+
release tampered plaintext before detecting the modification. Only use this
18+
mode when you need to process files too large to buffer in memory and you
19+
understand the security implications.
20+
21+
This example:
22+
1. Creates a KMS Keyring
23+
2. Configures the S3 Encryption Client with delayed authentication enabled
24+
3. Encrypts and uploads a large object to S3
25+
4. Streams the decrypted object back, reading it in chunks
26+
5. Verifies the decrypted content matches the original
27+
"""
28+
29+
from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig
30+
from s3_encryption.materials.kms_keyring import KmsKeyring
31+
32+
# 10 MB of example data
33+
EXAMPLE_DATA: bytes = b"A" * (10 * 1024 * 1024)
34+
CHUNK_SIZE = 1024 * 1024 # 1 MB
35+
36+
37+
def delayed_auth_streaming_decrypt(
38+
s3_client, kms_client, kms_key_id: str, bucket: str, key: str
39+
):
40+
"""Demonstrate streaming decryption with delayed authentication.
41+
42+
Args:
43+
s3_client: boto3 S3 client.
44+
kms_client: boto3 KMS client.
45+
kms_key_id: KMS key ARN or alias to use for encryption/decryption.
46+
bucket: S3 bucket name.
47+
key: S3 object key.
48+
"""
49+
# 1. Create a KMS Keyring.
50+
keyring = KmsKeyring(kms_client=kms_client, kms_key_id=kms_key_id)
51+
52+
# 2. Configure the S3 Encryption Client with delayed authentication.
53+
config = S3EncryptionClientConfig(
54+
keyring=keyring,
55+
enable_delayed_authentication=True,
56+
)
57+
s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=config)
58+
59+
# 3. Encrypt and upload the object.
60+
s3ec.put_object(Bucket=bucket, Key=key, Body=EXAMPLE_DATA)
61+
62+
# 4. Stream the decrypted object back in chunks.
63+
# With delayed authentication, plaintext is released incrementally
64+
# without buffering the entire object in memory.
65+
response = s3ec.get_object(Bucket=bucket, Key=key)
66+
body = response["Body"]
67+
68+
chunks = []
69+
while True:
70+
chunk = body.read(CHUNK_SIZE)
71+
if not chunk:
72+
break
73+
chunks.append(chunk)
74+
75+
plaintext = b"".join(chunks)
76+
77+
# 5. Verify the decrypted content matches the original.
78+
assert plaintext == EXAMPLE_DATA, "Decrypted plaintext does not match original data"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
This example demonstrates decrypting S3 objects that store their encryption
5+
metadata in instruction files rather than S3 object metadata.
6+
7+
An instruction file is a companion S3 object that contains the encryption
8+
metadata (encrypted data key, IV, algorithm, etc.) as JSON. By default,
9+
the instruction file has the same key as the encrypted object with a
10+
".instruction" suffix appended.
11+
12+
You can also use a custom instruction file suffix. This requires configuring
13+
the S3 Encryption Client with the matching suffix.
14+
15+
This example:
16+
1. Decrypts an object using the default instruction file suffix (".instruction")
17+
2. Decrypts the same object using a custom instruction file suffix
18+
"""
19+
20+
from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig
21+
from s3_encryption.materials.kms_keyring import KmsKeyring
22+
23+
24+
def instruction_file_get(
25+
s3_client, kms_client, kms_key_id: str, bucket: str, key: str, expected_plaintext: bytes
26+
):
27+
"""Demonstrate decrypting objects with default and custom instruction file suffixes.
28+
29+
Args:
30+
s3_client: boto3 S3 client.
31+
kms_client: boto3 KMS client.
32+
kms_key_id: KMS key ARN or alias used to encrypt the object.
33+
bucket: S3 bucket containing the encrypted object and instruction files.
34+
key: S3 object key of the encrypted object.
35+
expected_plaintext: Expected plaintext content for verification.
36+
"""
37+
keyring = KmsKeyring(kms_client=kms_client, kms_key_id=kms_key_id)
38+
39+
# 1. Decrypt using the default instruction file suffix (".instruction").
40+
# The client will fetch "<key>.instruction" for the encryption metadata.
41+
config = S3EncryptionClientConfig(keyring=keyring)
42+
s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=config)
43+
44+
response = s3ec.get_object(Bucket=bucket, Key=key)
45+
plaintext = response["Body"].read()
46+
assert plaintext == expected_plaintext, "Default suffix: decrypted plaintext does not match"
47+
48+
# 2. Decrypt using a custom instruction file suffix.
49+
# The client will fetch "<key>.custom-suffix-instruction" for the encryption metadata.
50+
custom_config = S3EncryptionClientConfig(
51+
keyring=keyring,
52+
instruction_file_suffix=".custom-suffix-instruction",
53+
)
54+
custom_s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=custom_config)
55+
56+
response = custom_s3ec.get_object(Bucket=bucket, Key=key)
57+
plaintext = response["Body"].read()
58+
assert plaintext == expected_plaintext, "Custom suffix: decrypted plaintext does not match"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
This example demonstrates a basic put/get roundtrip using the S3 Encryption Client
5+
with a KMS Keyring.
6+
7+
The KMS Keyring uses a symmetric KMS key to generate and decrypt data keys.
8+
The S3 Encryption Client encrypts the object before uploading to S3 and decrypts
9+
it on download, so the data is protected at rest.
10+
11+
This example:
12+
1. Creates a KMS Keyring with the provided KMS key ID
13+
2. Wraps a boto3 S3 client with the S3 Encryption Client
14+
3. Creates an encryption context bound to the S3 bucket and key
15+
4. Puts an encrypted object to S3
16+
5. Gets and decrypts the object from S3
17+
6. Verifies the decrypted plaintext matches the original
18+
19+
Here is an example KMS Key Policy statement that would validate the
20+
Encryption Context used in this example::
21+
22+
Sid: RestrictToEncryptionContextBucket
23+
Effect: Allow
24+
Principal:
25+
AWS: "arn:aws:iam::<account-id>:role/<role-name>"
26+
Action:
27+
- kms:GenerateDataKey
28+
- kms:Decrypt
29+
Resource: "*"
30+
Condition:
31+
StringEquals:
32+
"kms:EncryptionContext:aws-s3-bucket": "<bucket>"
33+
"""
34+
35+
from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig
36+
from s3_encryption.materials.kms_keyring import KmsKeyring
37+
38+
EXAMPLE_DATA: bytes = b"Hello, S3 Encryption Client!"
39+
40+
41+
def kms_keyring_put_get(s3_client, kms_client, kms_key_id: str, bucket: str, key: str):
42+
"""Demonstrate an encrypt/decrypt cycle using a KMS Keyring with S3.
43+
44+
Args:
45+
s3_client: boto3 S3 client.
46+
kms_client: boto3 KMS client.
47+
kms_key_id: KMS key ARN or alias to use for encryption/decryption.
48+
bucket: S3 bucket name.
49+
key: S3 object key.
50+
"""
51+
# 1. Create a KMS Keyring.
52+
keyring = KmsKeyring(kms_client=kms_client, kms_key_id=kms_key_id)
53+
54+
# 2. Wrap the S3 client with the S3 Encryption Client.
55+
# The default commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT,
56+
# which enforces key-committing algorithm suites on both encrypt and decrypt.
57+
config = S3EncryptionClientConfig(keyring=keyring)
58+
s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=config)
59+
60+
# 3. Create an encryption context.
61+
# The encryption context is a set of key-value pairs that are bound to the ciphertext.
62+
# Including the bucket and key ensures the ciphertext is tied to this specific S3 object.
63+
# This will also be visible to KMS when evaluating key policies.
64+
# See the example KMS Key Policy in this module's docstring.
65+
# The encryption context is optional, but strongly recommended.
66+
encryption_context = {
67+
"aws-s3-bucket": bucket,
68+
"aws-s3-key": key,
69+
}
70+
71+
# 4. Put an encrypted object.
72+
s3ec.put_object(Bucket=bucket, Key=key, Body=EXAMPLE_DATA, EncryptionContext=encryption_context)
73+
74+
# 5. Get and decrypt the object.
75+
# If you specified an encryption context during encryption,
76+
# you must provide the same encryption context during decryption.
77+
response = s3ec.get_object(Bucket=bucket, Key=key, EncryptionContext=encryption_context)
78+
plaintext = response["Body"].read()
79+
80+
# 6. Optional Verify the decrypted plaintext matches the original.
81+
assert plaintext == EXAMPLE_DATA, "Decrypted plaintext does not match original data"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
This example demonstrates how to decrypt legacy S3 objects that were encrypted
5+
using older versions of the S3 Encryption Client (V1 or V2).
6+
7+
Legacy objects use the KmsV1 wrapping algorithm and may use unauthenticated
8+
content encryption (AES-CBC). To decrypt these objects, you must:
9+
1. Enable legacy wrapping algorithms on the KMS Keyring
10+
2. Enable legacy unauthenticated modes on the S3 Encryption Client config
11+
3. Use a commitment policy that allows non-key-committing algorithm suites
12+
13+
This example:
14+
1. Creates a KMS Keyring with legacy wrapping algorithms enabled
15+
2. Configures the S3 Encryption Client with legacy decryption support
16+
3. Decrypts a legacy V1 object from S3
17+
4. Verifies the decrypted plaintext matches the expected content
18+
"""
19+
20+
from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig
21+
from s3_encryption.materials.kms_keyring import KmsKeyring
22+
from s3_encryption.materials.materials import CommitmentPolicy
23+
24+
25+
def decrypt_legacy_object(s3_client, kms_client, kms_key_id: str, bucket: str, key: str):
26+
"""Decrypt a legacy S3 object encrypted by an older S3 Encryption Client.
27+
28+
Args:
29+
s3_client: boto3 S3 client.
30+
kms_client: boto3 KMS client.
31+
kms_key_id: KMS key ARN or alias used to encrypt the object.
32+
bucket: S3 bucket name.
33+
key: S3 object key.
34+
35+
Returns:
36+
Decrypted plaintext bytes.
37+
"""
38+
# 1. Create a KMS Keyring with legacy wrapping algorithms enabled.
39+
# This allows the keyring to decrypt data keys wrapped using the KmsV1 mode,
40+
# which older S3 Encryption Clients used.
41+
keyring = KmsKeyring(
42+
kms_client=kms_client,
43+
kms_key_id=kms_key_id,
44+
enable_legacy_wrapping_algorithms=True,
45+
)
46+
47+
# 2. Configure the S3 Encryption Client for legacy decryption.
48+
# - enable_legacy_unauthenticated_modes: allows decryption of AES-CBC content
49+
# - REQUIRE_ENCRYPT_ALLOW_DECRYPT: new objects are encrypted with key-committing
50+
# algorithm suites, while still allowing decryption of legacy objects
51+
config = S3EncryptionClientConfig(
52+
keyring=keyring,
53+
enable_legacy_unauthenticated_modes=True,
54+
commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT,
55+
)
56+
s3ec = S3EncryptionClient(wrapped_s3_client=s3_client, config=config)
57+
58+
# 3. Decrypt the legacy object.
59+
response = s3ec.get_object(Bucket=bucket, Key=key)
60+
return response["Body"].read()

examples/test/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the delayed auth streaming decrypt example."""
4+
import boto3
5+
import pytest
6+
7+
from ..src.delayed_auth_streaming_example import delayed_auth_streaming_decrypt
8+
9+
pytestmark = [pytest.mark.examples]
10+
11+
BUCKET = "s3ec-python-github-test-bucket"
12+
KEY = "examples/delayed-auth-streaming"
13+
KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key"
14+
15+
16+
def test_delayed_auth_streaming_decrypt():
17+
s3_client = boto3.client("s3", region_name="us-west-2")
18+
kms_client = boto3.client("kms", region_name="us-west-2")
19+
delayed_auth_streaming_decrypt(
20+
s3_client=s3_client,
21+
kms_client=kms_client,
22+
kms_key_id=KMS_KEY_ID,
23+
bucket=BUCKET,
24+
key=KEY,
25+
)
26+
# Clean up
27+
s3_client.delete_object(Bucket=BUCKET, Key=KEY)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the instruction file example."""
4+
import boto3
5+
import pytest
6+
7+
from ..src.instruction_file_example import instruction_file_get
8+
9+
pytestmark = [pytest.mark.examples]
10+
11+
BUCKET = "s3ec-static-test-objects"
12+
KEY = "static-v3-instruction-file-from-java-v4"
13+
KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:key/a3889cd9-99eb-4138-a93a-aea9d52ec2ef"
14+
15+
16+
# TODO(#152): Move instruction_file_suffix from config to get_object request context
17+
# so a single S3EncryptionClient can use different suffixes per request.
18+
@pytest.mark.xfail(reason="instruction_file_suffix is per-client, not per-request")
19+
def test_instruction_file_get():
20+
s3_client = boto3.client("s3", region_name="us-west-2")
21+
kms_client = boto3.client("kms", region_name="us-west-2")
22+
instruction_file_get(
23+
s3_client=s3_client,
24+
kms_client=kms_client,
25+
kms_key_id=KMS_KEY_ID,
26+
bucket=BUCKET,
27+
key=KEY,
28+
expected_plaintext=KEY.encode("utf-8"),
29+
)

0 commit comments

Comments
 (0)