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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive module grouping in Hex docs for all keyrings, CMMs, caching, and streaming modules (#72)
- User guides for Getting Started, Choosing Components, and Security Best Practices (#73)
- Automated testing for guide code examples with extraction and validation (#73)
- Advanced feature examples demonstrating streaming, caching, and required encryption context (#75)
- Streaming file encryption example with 10MB test file and memory-efficient processing
- Caching CMM example showing 2x performance improvement for high-throughput scenarios
- Required Encryption Context example enforcing mandatory context keys for compliance

### Changed
- Consolidated CHANGELOG entries to improve readability and scannability (#81)
- Enhanced streaming module documentation with usage guidance, memory efficiency details, and verification handling (#72)
- Examples reorganized into complexity-based subdirectories (01_basics, 02_advanced, 03_aws_kms) (#75)
- Examples README updated with category-based navigation and quick start commands

### Fixed
- RSA keyring PEM loading to correctly decode keys using `pem_entry_decode` instead of `der_decode` (#74)
- All KMS examples updated to use correct Client API format (map-based return values)
- Client module now supports Caching CMM in dispatch clauses for encryption and decryption (#75)

## [0.6.0] - 2026-01-31

Expand Down
File renamed without changes.
File renamed without changes.
171 changes: 171 additions & 0 deletions examples/02_advanced/caching_cmm.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env elixir
# Caching CMM Example
#
# Demonstrates the Caching Cryptographic Materials Manager (CMM) which
# caches data keys to reduce calls to your key provider. This is useful
# for high-throughput scenarios where key generation latency matters.
#
# Run with: mix run examples/caching_cmm.exs

alias AwsEncryptionSdk.Client
alias AwsEncryptionSdk.Cmm.Default
alias AwsEncryptionSdk.Cmm.Caching
alias AwsEncryptionSdk.Cache.LocalCache
alias AwsEncryptionSdk.Keyring.RawAes

defmodule CachingDemo do
@num_encryptions 100
@message_size 1024 # 1KB messages

def run do
IO.puts(String.duplicate("=", 60))
IO.puts("Caching CMM Example")
IO.puts(String.duplicate("=", 60))
IO.puts("")

# Generate test data
messages = for i <- 1..@num_encryptions do
"Message #{i}: " <> :crypto.strong_rand_bytes(@message_size - 20)
end

# Step 1: Set up keyring
IO.puts("Step 1: Setting up Raw AES keyring...")
{:ok, keyring} = setup_keyring()
IO.puts(" ✓ Keyring created with 256-bit AES key")
IO.puts("")

# Step 2: Benchmark WITHOUT caching
IO.puts("Step 2: Encrypting #{@num_encryptions} messages WITHOUT caching...")
IO.puts(" (Each encryption generates a new data key)")
{non_cached_time, _results} = benchmark_non_cached(keyring, messages)
IO.puts(" ✓ Completed in #{format_time(non_cached_time)}")
IO.puts("")

# Step 3: Set up caching CMM
IO.puts("Step 3: Setting up Caching CMM...")
{:ok, cache} = LocalCache.start_link([])

# Cache settings:
# - max_age: 60 seconds TTL
# - max_messages: 1000 encryptions before re-keying
# - max_bytes: 10MB of plaintext before re-keying
caching_cmm = Caching.new_with_keyring(keyring, cache,
max_age: 60,
max_messages: 1000,
max_bytes: 10 * 1024 * 1024
)

IO.puts(" ✓ Cache configured:")
IO.puts(" - max_age: 60 seconds (TTL)")
IO.puts(" - max_messages: 1000 (re-key after 1000 encryptions)")
IO.puts(" - max_bytes: 10 MB (re-key after 10MB encrypted)")
IO.puts("")

# Step 4: Benchmark WITH caching
IO.puts("Step 4: Encrypting #{@num_encryptions} messages WITH caching...")
IO.puts(" (First encryption generates key, rest reuse cached key)")
{cached_time, _results} = benchmark_cached(caching_cmm, messages)
IO.puts(" ✓ Completed in #{format_time(cached_time)}")
IO.puts("")

# Step 5: Show comparison
IO.puts("Step 5: Performance comparison...")
speedup = non_cached_time / max(cached_time, 1)
IO.puts(" Without caching: #{format_time(non_cached_time)}")
IO.puts(" With caching: #{format_time(cached_time)}")
IO.puts(" Speedup: #{Float.round(speedup, 1)}x faster")
IO.puts("")

# Step 6: Demonstrate cache behavior
IO.puts("Step 6: Demonstrating cache behavior...")
demonstrate_cache_behavior(keyring, cache)
IO.puts("")

# Step 7: When to use caching
IO.puts("Step 7: When to use Caching CMM...")
IO.puts(" ✓ High-throughput encryption (many messages per second)")
IO.puts(" ✓ Latency-sensitive applications")
IO.puts(" ✓ Reducing KMS API calls (cost savings with AWS KMS)")
IO.puts(" ✗ Single encryption operations (no benefit)")
IO.puts(" ✗ When each message needs unique key material")
IO.puts("")

IO.puts(String.duplicate("=", 60))
IO.puts("Caching CMM demonstration completed!")
IO.puts(String.duplicate("=", 60))
end

defp setup_keyring do
wrapping_key = :crypto.strong_rand_bytes(32)
RawAes.new("example", "caching-demo-key", wrapping_key, :aes_256_gcm)
end

defp benchmark_non_cached(keyring, messages) do
cmm = Default.new(keyring)
client = Client.new(cmm)

encryption_context = %{"benchmark" => "non-cached"}

:timer.tc(fn ->
Enum.map(messages, fn msg ->
{:ok, result} = Client.encrypt(client, msg, encryption_context: encryption_context)
result
end)
end)
end

defp benchmark_cached(caching_cmm, messages) do
client = Client.new(caching_cmm)

encryption_context = %{"benchmark" => "cached"}

:timer.tc(fn ->
Enum.map(messages, fn msg ->
{:ok, result} = Client.encrypt(client, msg, encryption_context: encryption_context)
result
end)
end)
end

defp demonstrate_cache_behavior(keyring, cache) do
# Create a new caching CMM with low limits to show re-keying
caching_cmm = Caching.new_with_keyring(keyring, cache,
max_age: 60,
max_messages: 5 # Re-key after 5 messages
)
client = Client.new(caching_cmm)

IO.puts(" Testing with max_messages: 5")

# Encrypt 8 messages to show re-keying at message 6
for i <- 1..8 do
{time, {:ok, _result}} = :timer.tc(fn ->
Client.encrypt(client, "test message #{i}",
encryption_context: %{"test" => "cache-behavior"}
)
end)

status = if i == 1 or i == 6 do
"cache MISS (new key generated)"
else
"cache HIT (reusing key)"
end

IO.puts(" Message #{i}: #{format_time(time)} - #{status}")
end

IO.puts(" ✓ Re-keying occurred at message 6 (exceeded max_messages: 5)")
end

defp format_time(microseconds) when microseconds >= 1_000_000 do
"#{Float.round(microseconds / 1_000_000, 2)} sec"
end
defp format_time(microseconds) when microseconds >= 1000 do
"#{Float.round(microseconds / 1000, 2)} ms"
end
defp format_time(microseconds) do
"#{microseconds} μs"
end
end

CachingDemo.run()
158 changes: 158 additions & 0 deletions examples/02_advanced/required_encryption_context.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env elixir
# Required Encryption Context CMM Example
#
# Demonstrates enforcing mandatory encryption context keys using the
# Required Encryption Context CMM. This is useful for compliance and
# security policies that require certain metadata on all encrypted data.
#
# Run with: mix run examples/required_encryption_context.exs

alias AwsEncryptionSdk.Client
alias AwsEncryptionSdk.Cmm.Default
alias AwsEncryptionSdk.Cmm.RequiredEncryptionContext
alias AwsEncryptionSdk.Keyring.RawAes

defmodule RequiredContextDemo do
def run do
IO.puts(String.duplicate("=", 60))
IO.puts("Required Encryption Context CMM Example")
IO.puts(String.duplicate("=", 60))
IO.puts("")

# Step 1: Set up keyring
IO.puts("Step 1: Setting up Raw AES keyring...")
{:ok, keyring} = setup_keyring()
IO.puts(" ✓ Keyring created")
IO.puts("")

# Step 2: Create Required Encryption Context CMM
IO.puts("Step 2: Creating Required Encryption Context CMM...")
IO.puts(" Required keys: [\"tenant_id\", \"environment\"]")
cmm = RequiredEncryptionContext.new_with_keyring(
["tenant_id", "environment"],
keyring
)
client = Client.new(cmm)
IO.puts(" ✓ CMM configured to require 'tenant_id' and 'environment'")
IO.puts("")

# Step 3: Successful encryption with all required keys
IO.puts("Step 3: Encrypting with all required keys (should succeed)...")
encryption_context = %{
"tenant_id" => "acme-corp",
"environment" => "production",
"optional_key" => "some-value"
}

case Client.encrypt(client, "sensitive data", encryption_context: encryption_context) do
{:ok, result} ->
IO.puts(" ✓ Encryption succeeded!")
IO.puts(" Context: #{inspect(encryption_context)}")
IO.puts(" Ciphertext length: #{byte_size(result.ciphertext)} bytes")

# Store for later decryption tests
Process.put(:encrypted_result, result)
Process.put(:encryption_context, encryption_context)

{:error, reason} ->
IO.puts(" ✗ Unexpected error: #{inspect(reason)}")
System.halt(1)
end
IO.puts("")

# Step 4: Failed encryption - missing required key
IO.puts("Step 4: Encrypting without 'environment' key (should fail)...")
incomplete_context = %{
"tenant_id" => "acme-corp"
# Missing "environment"
}

case Client.encrypt(client, "sensitive data", encryption_context: incomplete_context) do
{:ok, _result} ->
IO.puts(" ✗ Should have failed but succeeded!")
System.halt(1)

{:error, {:missing_required_encryption_context_keys, missing}} ->
IO.puts(" ✓ Correctly rejected! Missing keys: #{inspect(missing)}")
end
IO.puts("")

# Step 5: Failed encryption - no context at all
IO.puts("Step 5: Encrypting without any context (should fail)...")

case Client.encrypt(client, "sensitive data") do
{:ok, _result} ->
IO.puts(" ✗ Should have failed but succeeded!")
System.halt(1)

{:error, {:missing_required_encryption_context_keys, missing}} ->
IO.puts(" ✓ Correctly rejected! Missing keys: #{inspect(missing)}")
end
IO.puts("")

# Step 6: Successful decryption with reproduced context
IO.puts("Step 6: Decrypting with reproduced context (should succeed)...")
result = Process.get(:encrypted_result)
encryption_context = Process.get(:encryption_context)

case Client.decrypt(client, result.ciphertext, encryption_context: encryption_context) do
{:ok, decrypt_result} ->
IO.puts(" ✓ Decryption succeeded!")
IO.puts(" Plaintext: #{inspect(decrypt_result.plaintext)}")

{:error, reason} ->
IO.puts(" ✗ Unexpected error: #{inspect(reason)}")
System.halt(1)
end
IO.puts("")

# Step 7: Failed decryption - missing reproduced context
IO.puts("Step 7: Decrypting without reproduced context (should fail)...")

case Client.decrypt(client, result.ciphertext) do
{:ok, _decrypt_result} ->
IO.puts(" ✗ Should have failed but succeeded!")
System.halt(1)

{:error, {:missing_required_encryption_context_keys, missing}} ->
IO.puts(" ✓ Correctly rejected! Missing keys: #{inspect(missing)}")
end
IO.puts("")

# Step 8: Compare with non-enforcing client
IO.puts("Step 8: Comparison with standard CMM (no enforcement)...")
standard_cmm = Default.new(keyring)
standard_client = Client.new(standard_cmm)

case Client.encrypt(standard_client, "data", encryption_context: %{}) do
{:ok, _result} ->
IO.puts(" Standard CMM: Allows empty encryption context")

{:error, _reason} ->
IO.puts(" Standard CMM: Rejected (unexpected)")
end

IO.puts(" Required EC CMM: Enforces mandatory keys")
IO.puts(" ✓ Use Required EC CMM for compliance requirements")
IO.puts("")

# Step 9: Use cases
IO.puts("Step 9: Common use cases for Required Encryption Context CMM...")
IO.puts(" • Multi-tenant systems: Require 'tenant_id' on all data")
IO.puts(" • Compliance: Require 'data_classification' or 'retention_policy'")
IO.puts(" • Auditing: Require 'created_by' or 'request_id'")
IO.puts(" • Environment separation: Require 'environment' (prod/staging/dev)")
IO.puts("")

IO.puts(String.duplicate("=", 60))
IO.puts("Required Encryption Context demonstration completed!")
IO.puts(String.duplicate("=", 60))
end

defp setup_keyring do
wrapping_key = :crypto.strong_rand_bytes(32)
RawAes.new("example", "required-context-demo-key", wrapping_key, :aes_256_gcm)
end
end

RequiredContextDemo.run()
Loading