Field-level encryption for Oproto.FluentDynamoDb using AWS Key Management Service (KMS) and the AWS Encryption SDK.
This package provides transparent field-level encryption for DynamoDB entities using AWS KMS. It integrates seamlessly with the Oproto.FluentDynamoDb source generator, allowing you to encrypt sensitive fields with a simple [Encrypted] attribute.
Key Features:
- 🔐 AWS KMS Integration - Industry-standard encryption using AWS Key Management Service
- 🔄 Transparent Encryption/Decryption - Automatic encryption on write, decryption on read
- 🏢 Multi-Tenant Support - Different encryption keys per tenant/customer/context
- ⚡ Data Key Caching - Minimize KMS API calls with configurable caching
- 🛡️ AWS Encryption SDK - Battle-tested encryption library with key commitment
- 📊 CloudTrail Integration - Audit trail of encryption operations
- 🔗 Blob Storage Support - Integrate with external storage for large encrypted fields
Use this package when you need to:
- Encrypt sensitive data at rest in DynamoDB
- Comply with data protection regulations (GDPR, HIPAA, PCI-DSS)
- Implement multi-tenant data isolation with separate encryption keys
- Provide customers with their own encryption keys
- Meet data residency requirements
- Create audit trails of data access
Note: If you only need to exclude sensitive fields from logs, use the built-in [Sensitive] attribute instead. This package is only required for encryption at rest.
dotnet add package Oproto.FluentDynamoDb.Encryption.KmsPrerequisites:
- Oproto.FluentDynamoDb (core library)
- Oproto.FluentDynamoDb.Attributes
- AWS account with KMS access
- IAM permissions for
kms:GenerateDataKeyandkms:Decrypt
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbEntity]
public partial class CustomerData
{
[PartitionKey]
public string CustomerId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
[Encrypted] // Encrypted at rest
[Sensitive] // Also redacted from logs
public string SocialSecurityNumber { get; set; } = string.Empty;
[Encrypted]
[Sensitive]
public string CreditCardNumber { get; set; } = string.Empty;
}using Oproto.FluentDynamoDb.Encryption.Kms;
// Load KMS key ARN from secure configuration
var keyResolver = new DefaultKmsKeyResolver(
defaultKeyId: configuration["Kms:DefaultKeyArn"]);
var options = new AwsEncryptionSdkOptions
{
DefaultKeyId = configuration["Kms:DefaultKeyArn"],
EnableCaching = true,
DefaultCacheTtlSeconds = 300 // 5 minutes
};
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver, options);var table = new CustomerDataTable(dynamoClient, "customers", encryptor);
// Encryption happens automatically
await table.PutItem(customerData).ExecuteAsync();
// Decryption happens automatically
var result = await table.GetItem("customer-123").ExecuteAsync();Complete configuration options:
var options = new AwsEncryptionSdkOptions
{
// Required: Default KMS key ARN
DefaultKeyId = "arn:aws:kms:us-east-1:123456789012:key/abc-123",
// Optional: Context-specific keys for multi-tenancy
ContextKeyMap = new Dictionary<string, string>
{
["tenant-a"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-a",
["tenant-b"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-b"
},
// Enable data key caching (recommended)
EnableCaching = true,
// Cache TTL for data keys (seconds)
DefaultCacheTtlSeconds = 300, // 5 minutes
// Maximum messages encrypted with a single data key
MaxMessagesPerDataKey = 100,
// Maximum bytes encrypted with a single data key
MaxBytesPerDataKey = 100 * 1024 * 1024, // 100 MB
// Algorithm suite (default uses key commitment)
Algorithm = CryptoAlgorithm.AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384,
// External blob storage configuration
ExternalBlobBucket = "my-encrypted-blobs",
ExternalBlobKeyPrefix = "encrypted-fields/",
AutoExternalBlobThreshold = 350 * 1024 // 350KB
};Override cache TTL per field:
[Encrypted(CacheTtlSeconds = 600)] // 10 minutes for this field
public string HighFrequencyField { get; set; } = string.Empty;
[Encrypted(CacheTtlSeconds = 60)] // 1 minute for this field
public string LowFrequencyField { get; set; } = string.Empty;Pass encryption context per operation (recommended):
await customerTable.PutItem(customerData)
.WithEncryptionContext("tenant-123")
.ExecuteAsync();
await customerTable.GetItem("customer-id")
.WithEncryptionContext("tenant-123")
.ExecuteAsync();Use ambient context for middleware scenarios:
// In middleware or request handler
EncryptionContext.Current = httpContext.GetTenantId();
// All operations in this async flow use the context
await customerTable.PutItem(data).ExecuteAsync();
await customerTable.GetItem("key").ExecuteAsync();
// Context automatically cleared when request completesThread Safety: EncryptionContext.Current uses AsyncLocal<string?>, which:
- Flows through async/await calls
- Does NOT leak across threads or requests
- Is isolated per async execution context
Implement IKmsKeyResolver for dynamic key resolution:
public class DatabaseKmsKeyResolver : IKmsKeyResolver
{
private readonly IKeyRepository _keyRepo;
private readonly string _defaultKey;
public DatabaseKmsKeyResolver(IKeyRepository keyRepo, string defaultKey)
{
_keyRepo = keyRepo;
_defaultKey = defaultKey;
}
public string ResolveKeyId(string? contextId)
{
if (contextId == null)
return _defaultKey;
// Load from database, cache, external service, etc.
var keyArn = _keyRepo.GetKmsKeyForTenant(contextId);
return keyArn ?? _defaultKey;
}
}
// Usage
var keyResolver = new DatabaseKmsKeyResolver(keyRepository, defaultKeyArn);
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver, options);This package uses the AWS Encryption SDK, which provides:
- Standardized Format - Recognized by AWS services and tools
- Algorithm Agility - Easy to upgrade encryption algorithms
- Key Commitment - Prevents key substitution attacks
- Encryption Context - Additional authenticated data (AAD)
- Digital Signatures - Non-repudiation
Each encrypted field includes encryption context for auditability:
{
"field": "SensitiveData",
"context": "tenant-123", // Your context ID
"entity": "CustomerData"
}This context:
- Appears in CloudTrail logs for audit trails
- Provides additional security (AAD)
- Prevents ciphertext substitution attacks
- Is validated during decryption
The AWS Encryption SDK's CachingCryptoMaterialsManager is used to minimize KMS API calls:
var options = new AwsEncryptionSdkOptions
{
EnableCaching = true,
DefaultCacheTtlSeconds = 300, // Cache data keys for 5 minutes
MaxMessagesPerDataKey = 100, // Max 100 messages per data key
MaxBytesPerDataKey = 100 * 1024 * 1024 // Max 100MB per data key
};Benefits:
- Reduced KMS API calls (lower costs)
- Improved performance (fewer network round-trips)
- Configurable limits for security best practices
Cache Key: Includes context ID, so different contexts use different cached keys.
For large encrypted fields that might exceed DynamoDB's 400KB item size limit, combine encryption with external blob storage.
[DynamoDbEntity]
public partial class Document
{
[PartitionKey]
public string DocumentId { get; set; } = string.Empty;
// Encrypted AND stored in S3
[Encrypted]
[BlobReference(BlobProvider.S3, BucketName = "my-encrypted-blobs", KeyPrefix = "documents/")]
[Sensitive]
public byte[] LargeEncryptedContent { get; set; } = Array.Empty<byte>();
}using Oproto.FluentDynamoDb.BlobStorage.S3;
var s3Client = new AmazonS3Client();
var blobStorage = new S3BlobStorage(
s3Client,
bucketName: "my-encrypted-blobs",
keyPrefix: "documents/");
var encryptor = new AwsEncryptionSdkFieldEncryptor(
keyResolver,
options,
blobStorage); // Pass blob storage provider- Encryption First - Data is encrypted using AWS Encryption SDK
- Blob Storage - Encrypted data is stored in S3
- DynamoDB Reference - DynamoDB stores the S3 URI
- Transparent Retrieval - On read, fetches from S3 and decrypts automatically
Configure automatic external storage for large fields:
var options = new AwsEncryptionSdkOptions
{
AutoExternalBlobThreshold = 350 * 1024, // 350KB
ExternalBlobBucket = "my-encrypted-blobs",
ExternalBlobKeyPrefix = "auto/"
};When encrypted data exceeds the threshold, it's automatically stored externally even without [BlobReference].
All encryption errors throw FieldEncryptionException:
try
{
await table.PutItem(data)
.WithEncryptionContext("tenant-123")
.ExecuteAsync();
}
catch (FieldEncryptionException ex)
{
Console.WriteLine($"Field: {ex.FieldName}");
Console.WriteLine($"Context: {ex.ContextId}");
Console.WriteLine($"Key: {ex.KeyId}");
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Inner: {ex.InnerException?.Message}");
}Error:
FieldEncryptionException: Failed to encrypt field 'SensitiveData' - KMS access denied
Solution: Check IAM permissions for kms:GenerateDataKey and kms:Decrypt
Required IAM Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/*"
}
]
}Error:
FieldEncryptionException: Failed to generate data key for field 'SensitiveData'
Solution: Verify KMS key exists and is enabled
Error:
FieldEncryptionException: Failed to decrypt field 'SensitiveData' - data corruption or wrong key
Solutions:
- Verify correct KMS key is being used
- Check encryption context matches
- Verify data is not corrupted
KMS API calls have costs:
GenerateDataKey: ~$0.03 per 10,000 requestsDecrypt: ~$0.03 per 10,000 requests
Recommendation: Enable caching to minimize costs.
Encryption adds latency:
- First encryption (no cache): ~50-100ms (KMS API call)
- Cached encryption: ~1-5ms (local encryption only)
- Decryption: Similar to encryption
Recommendation: Use caching and tune cache TTL based on your requirements.
Encryption is CPU-bound:
- AES-256-GCM: ~1-5 microseconds per KB
- Minimal impact on throughput for typical field sizes
Recommendation: Only encrypt truly sensitive fields.
- Never Hardcode Keys - Load KMS key ARNs from secure configuration
- Use IAM Policies - Restrict KMS key access using IAM and key policies
- Enable CloudTrail - Monitor KMS API calls for audit trails
- Rotate Keys - Use KMS automatic key rotation
- Separate Keys - Use different keys per tenant/environment
- Combine Attributes - Use both
[Encrypted]and[Sensitive] - Selective Encryption - Only encrypt truly sensitive fields
- Validate Context - Ensure encryption context is set correctly
- Test Isolation - Verify tenant data isolation
- Always Use [Sensitive] - Mark encrypted fields as sensitive
- Structured Logging - Use structured logging to filter sensitive data
- Production Logging - Consider disabling detailed logging in production
If you use [Encrypted] without this package, the source generator emits a warning:
Warning FDDB4001: Property 'SensitiveData' has [Encrypted] attribute but Oproto.FluentDynamoDb.Encryption.Kms package is not referenced
Solution: Add the package reference:
dotnet add package Oproto.FluentDynamoDb.Encryption.KmsProblem: Data stored as plaintext
Solutions:
- Verify
IFieldEncryptoris passed to table constructor - Check
[Encrypted]attribute is applied - Rebuild project to regenerate source code
- Verify KMS key ARN is valid
Problem: Wrong encryption key used
Solutions:
- Verify
WithEncryptionContext()is called - Check
EncryptionContext.Currentis set - Verify
IKmsKeyResolveris configured correctly - Test key resolution logic
Problem: High latency or KMS costs
Solutions:
- Enable caching:
EnableCaching = true - Increase cache TTL:
DefaultCacheTtlSeconds = 600 - Reduce encrypted field count
- Monitor KMS API calls in CloudWatch
// Entity definition
[DynamoDbEntity]
public partial class User
{
[PartitionKey]
public string UserId { get; set; } = string.Empty;
[Encrypted]
[Sensitive]
public string Email { get; set; } = string.Empty;
}
// Setup
var keyResolver = new DefaultKmsKeyResolver(kmsKeyArn);
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver);
var table = new UserTable(dynamoClient, "users", encryptor);
// Usage
await table.PutItem(user).ExecuteAsync();// Setup with tenant-specific keys
var keyResolver = new DefaultKmsKeyResolver(
defaultKeyId: defaultKeyArn,
contextKeyMap: new Dictionary<string, string>
{
["tenant-a"] = tenantAKeyArn,
["tenant-b"] = tenantBKeyArn
});
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver);
var table = new CustomerTable(dynamoClient, "customers", encryptor);
// Usage with context
await table.PutItem(customer)
.WithEncryptionContext("tenant-a")
.ExecuteAsync();// Custom resolver
public class DynamicKeyResolver : IKmsKeyResolver
{
private readonly IConfiguration _config;
public string ResolveKeyId(string? contextId)
{
if (contextId == null)
return _config["Kms:DefaultKey"];
return _config[$"Kms:Tenant:{contextId}:Key"]
?? _config["Kms:DefaultKey"];
}
}
// Usage
var keyResolver = new DynamicKeyResolver(configuration);
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver);// Setup with blob storage
var s3Client = new AmazonS3Client();
var blobStorage = new S3BlobStorage(s3Client, "my-bucket", "encrypted/");
var options = new AwsEncryptionSdkOptions
{
DefaultKeyId = kmsKeyArn,
AutoExternalBlobThreshold = 350 * 1024
};
var encryptor = new AwsEncryptionSdkFieldEncryptor(
keyResolver,
options,
blobStorage);
// Entity with large encrypted field
[DynamoDbEntity]
public partial class Document
{
[PartitionKey]
public string DocumentId { get; set; } = string.Empty;
[Encrypted]
[BlobReference(BlobProvider.S3, BucketName = "my-bucket", KeyPrefix = "docs/")]
public byte[] LargeContent { get; set; } = Array.Empty<byte>();
}- Field-Level Security Guide - Complete security guide
- Attribute Reference - Attribute documentation
- Advanced Types - Blob storage integration
- Error Handling - Exception handling
- 📚 Documentation: fluentdynamodb.dev
- 🐙 GitHub: github.com/oproto/fluent-dynamodb
- 📦 NuGet: Oproto.FluentDynamoDb.Encryption.Kms
MIT License - see LICENSE for details.