Field-Level Encryption
Encrypt specific fields in your entities using the [Encrypted] attribute.
Overview
Field-level encryption lets you encrypt individual properties on your entities. Encrypted fields are stored as binary data in DynamoDB and decrypted transparently when entities are loaded.
Marking Fields for Encryption
Use the [Encrypted] attribute on any property that should be encrypted at rest:
[DynamoDbTable("customers")]
public partial class Customer
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
[Encrypted]
[Sensitive]
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;
[Encrypted]
[Sensitive]
[DynamoDbAttribute("creditCard")]
public string CreditCardNumber { get; set; } = string.Empty;
}
Transparent Encryption and Decryption
Once configured, encryption and decryption happen automatically during Put and Get operations:
// Data is encrypted before writing to DynamoDB
await table.Customers.PutAsync(customer);
// Data is decrypted after reading from DynamoDB
var loaded = await table.Customers.GetAsync("customer123");
Console.WriteLine(loaded.SocialSecurityNumber); // Decrypted value
Decryption Failure Modes
By default, any decryption failure throws an exception. You can configure the library to skip encrypted fields that fail to decrypt, which is useful for STS downscoping scenarios where a service has reduced KMS permissions.
DecryptionFailureMode Enum
| Value | Behavior |
|---|---|
Throw (default) | Any decryption failure throws an exception |
SkipFields | Recoverable failures (no encryptor, access denied) leave the property at its CLR default and log a warning. Integrity failures (corrupted ciphertext, wrong key) always throw. |
Configuration
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.Providers.Encryption;
// Skip encrypted fields when decryption fails (e.g., access denied)
var options = new FluentDynamoDbOptions()
.WithEncryption(encryptor)
.WithDecryptionFailureMode(DecryptionFailureMode.SkipFields);
var table = new CustomersTable(client, "customers", options);
Read-Only Access Without Encryptor
When a service only needs non-encrypted fields:
// No encryptor configured — encrypted fields are skipped
var readOnlyOptions = new FluentDynamoDbOptions()
.WithDecryptionFailureMode(DecryptionFailureMode.SkipFields);
var table = new CustomersTable(client, "customers", readOnlyOptions);
// Non-encrypted fields load normally; encrypted fields stay at CLR default
var customer = await table.Customers.GetAsync("customer123");
Console.WriteLine(customer.Name); // Populated normally
Console.WriteLine(customer.SocialSecurityNumber); // "" (CLR default)
Write Behavior
Write operations (ToDynamoDbAsync) are never affected by this setting — they always throw on failure to prevent silent data loss.
Failure Classification
| Category | Examples | Behavior in SkipFields Mode |
|---|---|---|
| Recoverable | No encryptor, KMS access denied | Field skipped, warning logged |
| Integrity | Invalid ciphertext, wrong key, context validation failed | Always throws |
Combining with Other Attributes
The [Encrypted] attribute works alongside other attributes:
| Combination | Behavior |
|---|---|
[Encrypted] + [Sensitive] | Encrypted at rest, redacted in logs |
[Encrypted] + [BlobStorage] | Encrypted before uploading to S3 |
[Encrypted] + [JsonBlob] | JSON serialized, then encrypted |
Cache TTL
Control how long decrypted data keys are cached:
[Encrypted(CacheTtlSeconds = 600)]
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;
Per-Property Key Aliases
By default, all encrypted properties on an entity use the same KMS key. With the KeyAlias property on [Encrypted], you can route different properties to different KMS keys based on data classification:
[DynamoDbTable("customers")]
public partial class Customer
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
[Encrypted(KeyAlias = "pii")]
[Sensitive]
[DynamoDbAttribute("ssn")]
public string Ssn { get; set; } = string.Empty;
[Encrypted(KeyAlias = "financial")]
[Sensitive]
[DynamoDbAttribute("accountNumber")]
public string AccountNumber { get; set; } = string.Empty;
}
When KeyAlias is specified, the value is passed through the encryption pipeline to ResolveKeyIdAsync on your IKmsKeyResolver implementation. When omitted (null), the resolver falls through to context-based or default key resolution.
Configuring Alias-to-Key Mappings
Use the aliasKeyMap parameter on DefaultKmsKeyResolver to map aliases to KMS key ARNs:
var resolver = new DefaultKmsKeyResolver(
defaultKeyId: "arn:aws:kms:us-east-1:123456789012:key/default-key",
contextKeyMap: new Dictionary<string, string>
{
["tenant-a"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-a-key",
["tenant-b"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-b-key"
},
aliasKeyMap: new Dictionary<string, string>
{
["pii"] = "arn:aws:kms:us-east-1:123456789012:key/pii-key",
["financial"] = "arn:aws:kms:us-east-1:123456789012:key/financial-key"
});
Resolution priority: aliasKeyMap → contextKeyMap → defaultKeyId. Both maps use case-sensitive lookups.
Async Key Resolution (Breaking Change)
The IKmsKeyResolver interface has been converted from synchronous to asynchronous in v1.1.0. All custom implementations must be updated.
Old Interface (v1.0.0)
public interface IKmsKeyResolver
{
string ResolveKeyId(string? contextId);
}
New Interface (v1.1.0)
public interface IKmsKeyResolver
{
Task<string> ResolveKeyIdAsync(
string? contextId,
string? keyAlias = null,
CancellationToken cancellationToken = default);
}
Benefits
- True async resolution — Key lookups can now call databases, secrets managers, or external APIs without blocking
- Per-property key aliases — The
keyAliasparameter carries the alias from[Encrypted(KeyAlias = "...")]to the resolver - Cooperative cancellation —
CancellationTokensupport enables graceful shutdown and timeout handling
DefaultKmsKeyResolver Updates
The DefaultKmsKeyResolver constructor now accepts an optional aliasKeyMap parameter:
var resolver = new DefaultKmsKeyResolver(
defaultKeyId: "arn:aws:kms:us-east-1:123456789012:key/default-key",
contextKeyMap: new Dictionary<string, string>
{
["tenant-a"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-a-key",
["tenant-b"] = "arn:aws:kms:us-east-1:123456789012:key/tenant-b-key"
},
aliasKeyMap: new Dictionary<string, string>
{
["pii"] = "arn:aws:kms:us-east-1:123456789012:key/pii-key",
["financial"] = "arn:aws:kms:us-east-1:123456789012:key/financial-key"
});
The aliasKeyMap enables mapping key aliases declared on [Encrypted(KeyAlias = "...")] to specific KMS key ARNs. Resolution priority is: aliasKeyMap → contextKeyMap → defaultKeyId.
Migration Guide
All IKmsKeyResolver implementations must migrate from ResolveKeyId to ResolveKeyIdAsync.
Before (v1.0.0)
public class MyTenantKeyResolver : IKmsKeyResolver
{
public string ResolveKeyId(string? contextId)
{
// Synchronous lookup
return _tenantKeyMap[contextId ?? "default"];
}
}
After (v1.1.0)
public class MyTenantKeyResolver : IKmsKeyResolver
{
public async Task<string> ResolveKeyIdAsync(
string? contextId,
string? keyAlias = null,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
// Can now use async operations (database lookups, vault calls, etc.)
if (keyAlias is not null && _aliasMap.TryGetValue(keyAlias, out var aliasKey))
return aliasKey;
return await _tenantKeyService.GetKeyAsync(
contextId ?? "default", cancellationToken);
}
}
Migration Steps
- Rename
ResolveKeyId→ResolveKeyIdAsync - Change return type from
stringtoTask<string> - Add
string? keyAlias = nullandCancellationToken cancellationToken = defaultparameters - Optionally handle the
keyAliasparameter for per-property key routing - Replace any blocking I/O with async equivalents
- Pass
cancellationTokento downstream async calls
See Also
- KMS Setup - Configure AWS KMS keys
- Envelope Encryption - How the encryption pattern works
- Logging with Redaction - Redacting sensitive data in logs