This article is a machine translation of the contents of the following URL, which I wrote in Japanese:
Introduction
Hello, I'm @H0ukiStar.
Are you encrypting your DynamoDB data? Since you cannot create a DynamoDB table without encryption, the answer is always "yes" — whether you realize it or not.
By default, DynamoDB encrypts data at rest using an AWS owned key on the server side. You can choose from three different KMS keys for this server-side encryption depending on your requirements.
Additionally, DynamoDB supports client-side encryption, which allows you to encrypt data at the attribute level before sending it to DynamoDB.
In this article, I revisit the various encryption approaches available for DynamoDB. In particular, I walk through client-side attribute-level encryption hands-on — from basic encryption to Beacons (Searchable Encryption).
Encryption Options Supported by DynamoDB
As mentioned above, DynamoDB supports three types of encryption: server-side encryption, client-side encryption, and encryption in transit.
The overall picture of DynamoDB encryption is shown below:

Server-Side Encryption
When creating a DynamoDB table, there is no option to disable encryption — server-side encryption at rest is always applied. DynamoDB transparently encrypts the table when it is persisted to disk and decrypts it when you access the data.
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html
You can choose from three KMS keys for server-side encryption:
| Key Type | Manager | Cost | Use Case |
|---|---|---|---|
| AWS owned key (default) | AWS | Free | No special requirements |
AWS managed key (aws/dynamodb) |
AWS (user can view) | KMS API call charges | Need to audit key usage via CloudTrail |
| Customer managed key (CMK) | User | KMS key charges + API charges | Need rotation control, key policy management, or cross-account access |
The selection criteria can be summarized as follows:
- No special key management requirements: AWS owned key (default)
- Need to audit key usage via CloudTrail: AWS managed key
- Need to manage key lifecycle and access policies yourself: Customer managed key
Client-Side Encryption
In addition to server-side encryption at rest, you can perform client-side encryption. With client-side encryption, the client application encrypts the data itself before sending it to DynamoDB, ensuring that unencrypted data is never exposed to any third party, including AWS.
https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/client-server-side.html
Client-side encryption is performed using either the AWS Database Encryption SDK for DynamoDB or the Amazon DynamoDB Encryption Client, and you can select encryption targets on a per-attribute basis.
Crypto Action Types
With attribute-level encryption using the AWS Database Encryption SDK for DynamoDB, you specify a "Crypto Action" for each attribute to determine how it should be encrypted and/or signed. There are four options:
| Action | Encrypt | Sign | Use Case |
|---|---|---|---|
ENCRYPT_AND_SIGN |
Yes | Yes | Attributes requiring both confidentiality and integrity (e.g., email, address) |
SIGN_ONLY |
No | Yes | Attributes not requiring encryption but needing tamper detection (e.g., partition key, sort key) |
SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT |
No | Yes | Signed and included in the encryption context (e.g., identifiers for audit logs) |
DO_NOTHING |
No | No | Attributes requiring neither encryption nor signing (attributes you may freely add or change later) |
⚠️ Warning: Partition keys and sort keys cannot be encrypted. DynamoDB uses these values to determine item placement and build indexes, so encrypting them would prevent server-side routing. These attributes can only be assigned
SIGN_ONLYorSIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT.⚠️ Warning:
SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXTis an action added in the AWS Database Encryption SDK and is not available in the Amazon DynamoDB Encryption Client. For details on the differences between the two SDKs, see SDK History and Migration below.
Keyring (Key Provider) Options
The following keyrings are primarily available for client-side encryption:
- AWS KMS Keyring: Leverages KMS key management capabilities with low operational overhead
- Raw AES Keyring: For cases where you manage keys yourself without KMS
- Raw RSA Keyring: For cases using asymmetric keys
- AWS KMS Hierarchical Keyring: Improves performance through branch key caching (required when using Beacons)
This article demonstrates samples using the AWS KMS Keyring and the Hierarchical Keyring.
Supplement: Encryption in Transit
DynamoDB encrypts data in transit between client and server using HTTPS.
The encryption best practices recommend additional security measures for data in transit:
While DynamoDB encrypts data in transit by using HTTPS by default, additional security controls are recommended. You can use any of the following options:
- AWS Site-to-Site VPN connection using IPsec for encryption.
- AWS Direct Connect connection to establish a private connection.
- AWS Direct Connect connection with AWS Site-to-Site VPN connection for an IPsec-encrypted private connection.
- If access to DynamoDB is required only from within a virtual private cloud (VPC), you can use a VPC gateway endpoint and allow only resources in the VPC to access it. This prevents the traffic from traversing the public internet.
https://docs.aws.amazon.com/prescriptive-guidance/latest/encryption-best-practices/dynamodb.html
Hands-On
Server-Side Encryption
Server-side encryption at rest is determined by the "encryption key" you select when creating a DynamoDB table.
Since DynamoDB handles server-side encryption transparently, no application-level changes are required. Simply select a key when creating the table and you're done.
Additionally, you can change the encryption key after table creation without any downtime.
You select the KMS key for a table when you create or update the table. You can change the KMS key for a table at any time, either in the DynamoDB console or by using the UpdateTable operation. The process of switching keys is seamless and does not require downtime or degrade service.
Attribute-Level (Client-Side) Encryption
This is the main topic of this article. We perform attribute-level encryption using the AWS Database Encryption SDK for DynamoDB (Java) and the DynamoDB Encryption Client (Python).
DynamoDB Table Structure and CloudFormation Template
The DynamoDB tables, KMS key, and other resources used in the samples are defined in the following CloudFormation template.
The sample uses a table storing user information with the following attributes:
| Attribute | Type | Crypto Action | Reason |
|---|---|---|---|
pk |
String (Partition Key) | SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT |
Key attributes cannot be encrypted. Included in encryption context for auditability |
sk |
String (Sort Key) | SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT |
Same as above |
email |
String | ENCRYPT_AND_SIGN |
PII — encrypt |
name |
String | ENCRYPT_AND_SIGN |
PII — encrypt |
age |
Number | ENCRYPT_AND_SIGN |
PII — encrypt |
status |
String | SIGN_ONLY |
Not sensitive, but tamper protection needed |
sample.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
DynamoDB Client-Side Encryption sample resources.
Creates a Users table (with GSIs for Standard/Compound Beacons),
a KeyStore table for Hierarchical Keyring, and a KMS key.
Parameters:
UsersTableName:
Type: String
Default: Users
Description: Name of the DynamoDB table for the sample application.
KeyStoreTableName:
Type: String
Default: KeyStore
Description: Name of the DynamoDB table used as the KeyStore for Beacons (branch key management).
Resources:
# ===========================================================================
# KMS Key - Client-Side Encryption & KeyStore branch key wrapping
# ===========================================================================
EncryptionKey:
Type: AWS::KMS::Key
Properties:
Description: KMS key for DynamoDB client-side encryption sample
KeyUsage: ENCRYPT_DECRYPT
KeySpec: SYMMETRIC_DEFAULT
EnableKeyRotation: true
KeyPolicy:
Version: "2012-10-17"
Statement:
- Sid: AllowRootAccountFullAccess
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
EncryptionKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/ddb-cse-sample
TargetKeyId: !Ref EncryptionKey
# ===========================================================================
# Users Table - Sample application table
# ===========================================================================
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref UsersTableName
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
# Standard Beacon: email
- AttributeName: aws_dbe_b_email
AttributeType: S
# Compound Beacon: status + email
- AttributeName: aws_dbe_b_status_email
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
GlobalSecondaryIndexes:
# GSI for Standard Beacon
- IndexName: email-index
KeySchema:
- AttributeName: aws_dbe_b_email
KeyType: HASH
Projection:
ProjectionType: ALL
# GSI for Compound Beacon
- IndexName: status-email-index
KeySchema:
- AttributeName: aws_dbe_b_status_email
KeyType: HASH
Projection:
ProjectionType: ALL
# ===========================================================================
# KeyStore Table - Branch key management for Hierarchical Keyring / Beacons
# ===========================================================================
# The AWS Database Encryption SDK expects the KeyStore table to have
# a specific schema:
# - Partition key: "branch-key-id" (String)
# - Sort key: "type" (String)
KeyStoreTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref KeyStoreTableName
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: branch-key-id
AttributeType: S
- AttributeName: type
AttributeType: S
KeySchema:
- AttributeName: branch-key-id
KeyType: HASH
- AttributeName: type
KeyType: RANGE
Outputs:
KmsKeyArn:
Description: ARN of the KMS key for client-side encryption
Value: !GetAtt EncryptionKey.Arn
KmsKeyAlias:
Description: Alias of the KMS key
Value: !Ref EncryptionKeyAlias
UsersTableName:
Description: Name of the Users DynamoDB table
Value: !Ref UsersTable
UsersTableArn:
Description: ARN of the Users DynamoDB table
Value: !GetAtt UsersTable.Arn
KeyStoreTableName:
Description: Name of the KeyStore DynamoDB table
Value: !Ref KeyStoreTable
KeyStoreTableArn:
Description: ARN of the KeyStore DynamoDB table
Value: !GetAtt KeyStoreTable.Arn
This template creates the following resources:
| Resource | Description |
|---|---|
EncryptionKey (KMS) |
Symmetric key used for client-side encryption and KeyStore branch key wrapping (automatic rotation enabled) |
UsersTable |
Main table for the sample application |
KeyStoreTable |
Branch key management table for Hierarchical Keyring, created with the schema required by the SDK (branch-key-id / type) |
The Users table also has two GSIs for Standard Beacon / Compound Beacon searches:
| GSI Name | Key Attribute | Purpose |
|---|---|---|
email-index |
aws_dbe_b_email |
Equality search on email using Standard Beacon |
status-email-index |
aws_dbe_b_status_email |
Compound condition search on status + email using Compound Beacon |
Basic Put / Get Sample
AWS Database Encryption SDK (Java)
Here is a sample of attribute-level encryption using the AWS Database Encryption SDK (Java).
The author verified this with the following environment:
- Amazon Corretto 21 (Java 21)
- Dependency (Apache Maven 3.9):
<dependency>
<groupId>software.amazon.cryptography</groupId>
<artifactId>aws-database-encryption-sdk-dynamodb</artifactId>
<version>3.9.0</version>
</dependency>
We use DynamoDbEncryptionInterceptor to configure an encryption client and perform item put and get operations.
⚠️ Warning: Replace
arn:aws:kms:ap-northeast-1:123456789012:key/your-key-idwith your own KMS key ARN.
Sample Code : BasicPutGetExample.java
package com.example.ddbcse;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
import software.amazon.cryptography.materialproviders.IKeyring;
import software.amazon.cryptography.materialproviders.MaterialProviders;
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkMultiKeyringInput;
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
import java.util.HashMap;
import java.util.Map;
public class BasicPutGetExample {
private static final String TABLE_NAME = "Users";
private static final String KMS_KEY_ARN =
"arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id";
public static void main(String[] args) {
// =======================================================
// 1. Create the keyring
// =======================================================
final MaterialProviders matProv = MaterialProviders.builder()
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
.build();
final CreateAwsKmsMrkMultiKeyringInput keyringInput =
CreateAwsKmsMrkMultiKeyringInput.builder()
.generator(KMS_KEY_ARN)
.build();
final IKeyring kmsKeyring = matProv.CreateAwsKmsMrkMultiKeyring(keyringInput);
// =======================================================
// 2. Define crypto actions per attribute
// =======================================================
final Map<String, CryptoAction> attributeActions = new HashMap<>();
attributeActions.put("pk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("sk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("email", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("name", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("age", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("status", CryptoAction.SIGN_ONLY);
// =======================================================
// 3. Table encryption configuration
// =======================================================
final DynamoDbTableEncryptionConfig tableConfig =
DynamoDbTableEncryptionConfig.builder()
.logicalTableName(TABLE_NAME)
.partitionKeyName("pk")
.sortKeyName("sk")
.attributeActionsOnEncrypt(attributeActions)
.keyring(kmsKeyring)
.build();
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
tableConfigs.put(TABLE_NAME, tableConfig);
// =======================================================
// 4. Create DynamoDbEncryptionInterceptor
// =======================================================
final DynamoDbEncryptionInterceptor interceptor =
DynamoDbEncryptionInterceptor.builder()
.config(DynamoDbTablesEncryptionConfig.builder()
.tableEncryptionConfigs(tableConfigs)
.build())
.build();
// =======================================================
// 5. Create encryption-enabled DynamoDB client
// =======================================================
final DynamoDbClient ddb = DynamoDbClient.builder()
.overrideConfiguration(
ClientOverrideConfiguration.builder()
.addExecutionInterceptor(interceptor)
.build())
.build();
// =======================================================
// 6. Write an item
// =======================================================
final HashMap<String, AttributeValue> item = new HashMap<>();
item.put("pk", AttributeValue.builder().s("USER#001").build());
item.put("sk", AttributeValue.builder().s("PROFILE").build());
item.put("email", AttributeValue.builder().s("taro@example.com").build());
item.put("name", AttributeValue.builder().s("太郎").build());
item.put("age", AttributeValue.builder().n("30").build());
item.put("status", AttributeValue.builder().s("active").build());
System.out.println("=== Put item (plaintext) ===");
item.forEach((k, v) -> System.out.println(" " + k + " = " + v));
ddb.putItem(PutItemRequest.builder()
.tableName(TABLE_NAME)
.item(item)
.build());
System.out.println("Put item completed.\n");
// =======================================================
// 7. Get item with plain client to verify encrypted state
// =======================================================
final Map<String, AttributeValue> key = new HashMap<>();
key.put("pk", AttributeValue.builder().s("USER#001").build());
key.put("sk", AttributeValue.builder().s("PROFILE").build());
final DynamoDbClient plainClient = DynamoDbClient.builder().build();
GetItemResponse rawResponse = plainClient.getItem(GetItemRequest.builder()
.tableName(TABLE_NAME)
.key(key)
.build());
System.out.println("=== Get item (raw / encrypted) ===");
rawResponse.item().forEach((k, v) -> System.out.println(" " + k + " = " + v));
System.out.println();
// =======================================================
// 8. Read item (automatic decryption)
// =======================================================
GetItemResponse response = ddb.getItem(GetItemRequest.builder()
.tableName(TABLE_NAME)
.key(key)
.build());
System.out.println("=== Get item (decrypted) ===");
response.item().forEach((k, v) -> System.out.println(" " + k + " = " + v));
}
}
Verifying the Encrypted Item
The sample above puts an item, retrieves it with a plain DynamoDB client (no Interceptor) to verify the encrypted state, and then retrieves it with the encryption client to show the decrypted result. Running the sample produces output like the following:
=== Put item (plaintext) ===
sk = AttributeValue(S=PROFILE)
name = AttributeValue(S=太郎)
pk = AttributeValue(S=USER#001)
email = AttributeValue(S=taro@example.com)
age = AttributeValue(N=30)
status = AttributeValue(S=active)
Put item completed.
=== Get item (raw / encrypted) ===
status = AttributeValue(S=active)
aws_dbe_foot = AttributeValue(B=SdkBytes(bytes=0x0376e53f...))
aws_dbe_head = AttributeValue(B=SdkBytes(bytes=0x02010fce...))
pk = AttributeValue(S=USER#001)
email = AttributeValue(B=SdkBytes(bytes=0x0001c341...))
name = AttributeValue(B=SdkBytes(bytes=0x0001737c...))
age = AttributeValue(B=SdkBytes(bytes=0x0002c79e...))
sk = AttributeValue(S=PROFILE)
=== Get item (decrypted) ===
name = AttributeValue(S=太郎)
sk = AttributeValue(S=PROFILE)
pk = AttributeValue(S=USER#001)
email = AttributeValue(S=taro@example.com)
age = AttributeValue(N=30)
status = AttributeValue(S=active)
Looking at the raw (encrypted) item, we can observe the following:
-
email,name,ageare stored as binary (Btype) — they are encrypted -
pk,sk,statusremain as plaintext (Stype) — signed only, not encrypted -
aws_dbe_headandaws_dbe_footmetadata attributes have been automatically added by the SDK
You can also verify this from the DynamoDB console — the "Explore table items" view shows the same encrypted state.
Amazon DynamoDB Encryption Client (Python)
Here is a sample of attribute-level encryption using the Amazon DynamoDB Encryption Client (Python).
The author verified this with the following environment:
- Python 3.14
- Dependencies:
dynamodb-encryption-sdk==3.3.0
boto3==1.43.73
We use the EncryptedTable class to create an encryption-enabled table resource and perform put and get operations. This class transparently handles encryption and decryption internally, so it can be used just like a regular boto3 Table.
⚠️ Warning: Replace
arn:aws:kms:ap-northeast-1:123456789012:key/your-key-idwith your own KMS key ARN.
Sample Code : basic_put_get_example.py
"""DynamoDB Client-Side Encryption - Basic Put/Get Example (Python)
Performs attribute-level encryption and executes item put and get.
Uses AWS KMS to provide cryptographic materials.
"""
from typing import Any
import boto3
from dynamodb_encryption_sdk.encrypted.table import EncryptedTable
from dynamodb_encryption_sdk.identifiers import CryptoAction
from dynamodb_encryption_sdk.material_providers.aws_kms import (
AwsKmsCryptographicMaterialsProvider,
)
from dynamodb_encryption_sdk.structures import AttributeActions
TABLE_NAME: str = "Users"
KMS_KEY_ARN: str = (
"arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
)
def main() -> None:
# =======================================================
# 1. Create KMS CMP (Cryptographic Materials Provider)
# =======================================================
kms_cmp: AwsKmsCryptographicMaterialsProvider = (
AwsKmsCryptographicMaterialsProvider(key_id=KMS_KEY_ARN)
)
# =======================================================
# 2. Define crypto actions per attribute
# =======================================================
actions: AttributeActions = AttributeActions(
default_action=CryptoAction.ENCRYPT_AND_SIGN,
attribute_actions={
# Partition key and sort key cannot be encrypted (EncryptedTable auto-sets SIGN_ONLY)
"pk": CryptoAction.SIGN_ONLY,
"sk": CryptoAction.SIGN_ONLY,
"status": CryptoAction.DO_NOTHING, # Neither signed nor encrypted
},
)
# =======================================================
# 3. Create regular DynamoDB table resource
# =======================================================
table = boto3.resource("dynamodb").Table(TABLE_NAME)
# =======================================================
# 4. Create encryption-enabled table
# =======================================================
encrypted_table: EncryptedTable = EncryptedTable(
table=table,
materials_provider=kms_cmp,
attribute_actions=actions,
)
# =======================================================
# 5. Write an item
# =======================================================
item: dict[str, Any] = {
"pk": "USER#002",
"sk": "PROFILE",
"email": "jiro@example.com",
"name": "次郎",
"age": 26,
"status": "active",
}
print("=== Put item (plaintext) ===")
for k, v in item.items():
print(f" {k} = {v}")
encrypted_table.put_item(Item=item)
print("Put item completed.\n")
# =======================================================
# 6. Get with plain table resource to verify encrypted state
# =======================================================
key: dict[str, Any] = {"pk": "USER#002", "sk": "PROFILE"}
raw_response: dict[str, Any] = table.get_item(Key=key)
raw_item: dict[str, Any] = raw_response["Item"]
print("=== Get item (raw / encrypted) ===")
for k, v in raw_item.items():
print(f" {k} = {v!r}")
print()
# =======================================================
# 7. Get with encrypted table resource (automatic decryption)
# =======================================================
decrypted_response: dict[str, Any] = encrypted_table.get_item(Key=key)
decrypted_item: dict[str, Any] = decrypted_response["Item"]
print("=== Get item (decrypted) ===")
for k, v in decrypted_item.items():
print(f" {k} = {v!r}")
if __name__ == "__main__":
main()
Verifying the Encrypted Item
Similar to the AWS Database Encryption SDK (Java) version, we retrieve the item with a plain table resource (no encryption) to verify the encrypted state, then retrieve it with EncryptedTable to show the decrypted result. Running the sample produces output like the following:
=== Put item (plaintext) ===
pk = USER#002
sk = PROFILE
email = jiro@example.com
name = 次郎
age = 26
status = active
Put item completed.
=== Get item (raw / encrypted) ===
*amzn-ddb-map-desc* = Binary(b'\x00\x00\x00\x00...')
status = 'active'
*amzn-ddb-map-sig* = Binary(b'\xa1\xef\x02]...')
pk = 'USER#002'
email = Binary(b'\x8e\x02\xd8V...')
name = Binary(b'\x04\x8ceI...')
age = Binary(b'\xec_x\xe7...')
sk = 'PROFILE'
=== Get item (decrypted) ===
status = 'active'
pk = 'USER#002'
email = 'jiro@example.com'
name = '次郎'
age = Decimal('26')
sk = 'PROFILE'
Looking at the raw (encrypted) item, we can observe the following:
-
email,name,ageare stored as binary (Binarytype) — they are encrypted -
pk,sk,statusremain as plaintext -
*amzn-ddb-map-desc*and*amzn-ddb-map-sig*metadata attributes have been automatically added by the SDK
Cross-SDK Decryption Is Not Possible
The AWS Database Encryption SDK and the DynamoDB Encryption Client use different encryption formats with different metadata attribute names (aws_dbe_head / aws_dbe_foot vs. *amzn-ddb-map-desc* / *amzn-ddb-map-sig*).
Therefore, items encrypted by one SDK cannot be decrypted by the other. For example, attempting to decrypt an item encrypted by the DynamoDB Encryption Client (Python) using the AWS Database Encryption SDK (Java) results in the following error:
[WARNING]
software.amazon.awssdk.core.exception.SdkClientException: Unable to unmarshall response (Encrypted item missing expected header and footer attributes). Response Code: 200, Response Text: OK (SDK Attempt Count: 1)
...
Caused by: software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DynamoDbItemEncryptorException: Encrypted item missing expected header and footer attributesℹ️ Note: Cross-language decryption within the same SDK family is supported.
AWS Database Encryption SDK:
The AWS Database Encryption SDK for DynamoDB is available in multiple programming languages. The language implementations are designed to be fully interoperable and to offer the same features, although they might be implemented in different ways. Typically, you use the library that is compatible with your application.
https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/configure.html
Amazon DynamoDB Encryption Client:
The Amazon DynamoDB Encryption Client is available for the following programming languages. The language-specific libraries vary, but the resulting implementations are interoperable. For example, you can encrypt (and sign) an item with the Java client and decrypt the item with the Python client.
https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/programming-languages.html
For details on the differences and history of the two SDKs, see SDK History and Migration below.
Querying Encrypted Attributes
When attribute-level encryption is introduced, there are constraints on querying encrypted attributes.
Constraints on Filter and Condition Expressions for Encrypted Attributes
Since encrypted attributes cannot be decrypted on the DynamoDB server side, the following operations do not work:
-
FilterExpression: Even if you filter with a condition like
email = :val, the server compares against the encrypted binary value, so it won't match - ConditionExpression: For the same reason, conditional writes don't work either
- GSI / LSI key attributes: For the same reason, specifying an encrypted attribute as an index key produces no meaningful query results
Example: Filtering on an encrypted attribute with plaintext (no match)
System.out.println("=== Scan with FilterExpression: email = 'taro@example.com' (String) ===");
ScanResponse response1 = ddb.scan(ScanRequest.builder()
.tableName(TABLE_NAME)
.filterExpression("email = :val")
.expressionAttributeValues(Map.of(
":val", AttributeValue.builder().s("taro@example.com").build()))
.build());
System.out.println(" Items found: " + response1.count());
=== Scan with FilterExpression: email = 'taro@example.com' (String) ===
Items found: 0
If you know the exact encrypted binary, a filter can match. However, you cannot derive the encrypted binary at FilterExpression construction time for the following reasons, making search effectively impossible:
- In AES-GCM, a different nonce (IV) is generally used for each encryption operation, so even encrypting the same plaintext with the same key produces a different ciphertext each time
- Therefore, it is impossible to pre-compute the ciphertext stored in the table from a plaintext search value
Example: Filtering with the known encrypted binary (matches)
byte[] encryptedEmailBytes = Base64.getDecoder().decode(
"AAHDQc/juSMNHkMPz+zNnOIBZKen9tEsHf0X7VaL7CFHKQ==");
ScanResponse response2 = ddb.scan(ScanRequest.builder()
.tableName(TABLE_NAME)
.filterExpression("email = :val")
.expressionAttributeValues(Map.of(
":val", AttributeValue.builder()
.b(SdkBytes.fromByteArray(encryptedEmailBytes))
.build()))
.build());
System.out.println(" Items found: " + response2.count());
Items found: 1
So how can you search on encrypted attributes? This is where Searchable Encryption (Beacons) comes in.
Beacons (Searchable Encryption)
The AWS Database Encryption SDK provides Beacons as a mechanism to enable querying on encrypted attributes.
⚠️ Warning: The Amazon DynamoDB Encryption Client does not support Beacons.
How Beacons Work
Beacons compute an HMAC from the plaintext value of an encrypted attribute and store a portion (truncated HMAC) as a "beacon" in a separate attribute. This beacon value cannot be used to recover the original plaintext, but since the same plaintext always produces the same beacon value, it can be used for equality queries.
Since a hash value is used rather than the plaintext itself, there are the following characteristics (caveats):
- Because beacon values are truncated hashes, different plaintexts may produce the same beacon value (collisions)
- Shorter beacon lengths increase collisions but make it harder to guess the original value (better security)
- Longer beacon lengths reduce collisions and improve query precision but increase the risk of statistical inference
Beacon Types
| Type | Purpose |
|---|---|
| Standard Beacon | Equality search on a single attribute |
| Compound Beacon | Search combining multiple attributes |
Standard Beacon Sample
The following example configures a Standard Beacon for the email attribute, enabling email-based searches even in an encrypted state.
Using Beacons requires a Hierarchical Keyring, which in turn requires a pre-created KeyStore (branch key management table).
⚠️ Warning: Replace
arn:aws:kms:ap-northeast-1:123456789012:key/your-key-idwith your own KMS key ARN.
Sample Code : StandardBeaconExample.java
package com.example.ddbcse;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.*;
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
import software.amazon.cryptography.keystore.KeyStore;
import software.amazon.cryptography.keystore.model.CreateKeyInput;
import software.amazon.cryptography.keystore.model.CreateKeyOutput;
import software.amazon.cryptography.keystore.model.KeyStoreConfig;
import software.amazon.cryptography.keystore.model.KMSConfiguration;
import software.amazon.cryptography.materialproviders.IKeyring;
import software.amazon.cryptography.materialproviders.MaterialProviders;
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsHierarchicalKeyringInput;
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
import java.util.*;
public class StandardBeaconExample {
private static final String TABLE_NAME = "Users";
private static final String KMS_KEY_ARN =
"arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id";
private static final String KEYSTORE_TABLE_NAME = "KeyStore";
public static void main(String[] args) {
// =======================================================
// 1. Create KeyStore client
// =======================================================
final KeyStore keyStore = KeyStore.builder()
.KeyStoreConfig(KeyStoreConfig.builder()
.ddbTableName(KEYSTORE_TABLE_NAME)
.kmsConfiguration(KMSConfiguration.builder()
.kmsKeyArn(KMS_KEY_ARN)
.build())
.logicalKeyStoreName(KEYSTORE_TABLE_NAME)
.ddbClient(DynamoDbClient.builder().build())
.kmsClient(KmsClient.builder().build())
.build())
.build();
// =======================================================
// 2. Create branch key
// (If already created, you can specify the existing branchKeyId directly)
// =======================================================
System.out.println("Creating branch key...");
final CreateKeyOutput branchKeyOutput = keyStore.CreateKey(
CreateKeyInput.builder().build());
final String branchKeyId = branchKeyOutput.branchKeyIdentifier();
System.out.println("Branch key created: " + branchKeyId);
// =======================================================
// 3. Create Hierarchical Keyring
// =======================================================
final MaterialProviders matProv = MaterialProviders.builder()
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
.build();
final IKeyring hierarchicalKeyring =
matProv.CreateAwsKmsHierarchicalKeyring(
CreateAwsKmsHierarchicalKeyringInput.builder()
.keyStore(keyStore)
.branchKeyId(branchKeyId)
.ttlSeconds(600L)
.build());
// =======================================================
// 4. Define Standard Beacon
// =======================================================
List<StandardBeacon> standardBeacons = new ArrayList<>();
standardBeacons.add(StandardBeacon.builder()
.name("email")
.length(30) // Beacon length (in bits)
.build());
// =======================================================
// 5. Configure Beacon Version
// =======================================================
List<BeaconVersion> beaconVersions = new ArrayList<>();
beaconVersions.add(BeaconVersion.builder()
.version(1)
.keyStore(keyStore)
.keySource(BeaconKeySource.builder()
.single(SingleKeyStore.builder()
.keyId(branchKeyId)
.cacheTTL(600)
.build())
.build())
.standardBeacons(standardBeacons)
.build());
// =======================================================
// 6. Search configuration (SearchConfig)
// =======================================================
SearchConfig searchConfig = SearchConfig.builder()
.versions(beaconVersions)
.writeVersion(1)
.build();
// =======================================================
// 7. Define crypto actions per attribute
// =======================================================
final Map<String, CryptoAction> attributeActions = new HashMap<>();
attributeActions.put("pk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("sk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("email", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("name", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("age", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("status", CryptoAction.SIGN_ONLY);
// =======================================================
// 8. Table encryption config (Beacons enabled)
// =======================================================
final DynamoDbTableEncryptionConfig tableConfig =
DynamoDbTableEncryptionConfig.builder()
.logicalTableName(TABLE_NAME)
.partitionKeyName("pk")
.sortKeyName("sk")
.attributeActionsOnEncrypt(attributeActions)
.keyring(hierarchicalKeyring)
.search(searchConfig)
.build();
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
tableConfigs.put(TABLE_NAME, tableConfig);
// =======================================================
// 9. Create DynamoDbEncryptionInterceptor
// =======================================================
final DynamoDbEncryptionInterceptor interceptor =
DynamoDbEncryptionInterceptor.builder()
.config(DynamoDbTablesEncryptionConfig.builder()
.tableEncryptionConfigs(tableConfigs)
.build())
.build();
// =======================================================
// 10. Create encryption-enabled DynamoDB client
// =======================================================
final DynamoDbClient ddb = DynamoDbClient.builder()
.overrideConfiguration(
ClientOverrideConfiguration.builder()
.addExecutionInterceptor(interceptor)
.build())
.build();
// =======================================================
// 11. Write an item
// =======================================================
final HashMap<String, AttributeValue> item = new HashMap<>();
item.put("pk", AttributeValue.builder().s("USER#100").build());
item.put("sk", AttributeValue.builder().s("PROFILE").build());
item.put("email", AttributeValue.builder().s("hanako@example.com").build());
item.put("name", AttributeValue.builder().s("花子").build());
item.put("age", AttributeValue.builder().n("25").build());
item.put("status", AttributeValue.builder().s("active").build());
System.out.println("=== Put item (plaintext) ===");
item.forEach((k, v) -> System.out.println(" " + k + " = " + v));
ddb.putItem(PutItemRequest.builder()
.tableName(TABLE_NAME)
.item(item)
.build());
System.out.println("Put item with beacon completed.\n");
// =======================================================
// 12. Raw get with plain client (verify beacon attribute)
// =======================================================
final DynamoDbClient plainClient = DynamoDbClient.builder().build();
final Map<String, AttributeValue> key = new HashMap<>();
key.put("pk", AttributeValue.builder().s("USER#100").build());
key.put("sk", AttributeValue.builder().s("PROFILE").build());
GetItemResponse rawResponse = plainClient.getItem(GetItemRequest.builder()
.tableName(TABLE_NAME)
.key(key)
.build());
System.out.println("=== Get item (raw) - beacon attribute visible ===");
rawResponse.item().forEach((k, v) -> System.out.println(" " + k + " = " + v));
System.out.println();
// =======================================================
// 13. Query using beacon (GSI: email-index)
// =======================================================
QueryResponse queryResponse = ddb.query(QueryRequest.builder()
.tableName(TABLE_NAME)
.indexName("email-index")
.keyConditionExpression("aws_dbe_b_email = :emailVal")
.expressionAttributeValues(Map.of(
":emailVal",
AttributeValue.builder().s("hanako@example.com").build()))
.build());
System.out.println("=== Query via beacon (email-index) ===");
System.out.println(" Items found: " + queryResponse.count());
queryResponse.items().forEach(i -> {
System.out.println(" ---");
i.forEach((k, v) -> System.out.println(" " + k + " = " + v));
});
}
}
Running the sample above produces output like the following:
Creating branch key...
Branch key created: 7b700228-d086-4446-bf71-bb4e95e95907
=== Put item (plaintext) ===
...
Put item with beacon completed.
=== Get item (raw) - beacon attribute visible ===
status = AttributeValue(S=active)
aws_dbe_foot = AttributeValue(B=SdkBytes(bytes=...))
aws_dbe_head = AttributeValue(B=SdkBytes(bytes=...))
pk = AttributeValue(S=USER#100)
email = AttributeValue(B=SdkBytes(bytes=...))
name = AttributeValue(B=SdkBytes(bytes=...))
aws_dbe_b_email = AttributeValue(S=176be958)
age = AttributeValue(B=SdkBytes(bytes=...))
sk = AttributeValue(S=PROFILE)
=== Query via beacon (email-index) ===
Items found: 1
---
sk = AttributeValue(S=PROFILE)
name = AttributeValue(S=花子)
pk = AttributeValue(S=USER#100)
age = AttributeValue(N=25)
email = AttributeValue(S=hanako@example.com)
status = AttributeValue(S=active)
Looking at the raw get result, in addition to the attributes we saw in the basic encryption sample (aws_dbe_head, aws_dbe_foot, encrypted binary attributes), there is a new attribute aws_dbe_b_email. This is the beacon attribute automatically computed and stored by the SDK. Its value 176be958 is a short string — the truncated HMAC of hanako@example.com.
The GSI email-index uses aws_dbe_b_email as its partition key. At query time, the SDK computes the beacon value from the plaintext hanako@example.com and searches the GSI. As a result, the encrypted item is found, and the SDK automatically decrypts it, returning the plaintext values.
Important Considerations When Using Beacons
Key considerations when making data searchable with Beacons:
- KeyStore table must be created in advance: A separate DynamoDB table for managing beacon keys must be created, along with a branch key
- Hierarchical Keyring is required: Beacons require the use of a Hierarchical Keyring
- Beacon length tradeoff: When determining beacon length, consider the expected record count and security requirements > AWS documentation provides recommended values based on dataset size: > https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html#beacon-length-example
-
Equality matches only: Standard Beacons only support equality (
=) queries - Existing data not searchable: Beacons compute beacon values at write time, so enabling beacons does not make existing data searchable (re-writing is required)
-
Not supported with DynamoDB Enhanced Client: Searchable Encryption can only be used with
DynamoDbEncryptionInterceptor+ the low-level DynamoDB API
Compound Beacon Sample
When you need searches combining multiple attributes, use Compound Beacons. For example, combining status (SIGN_ONLY) and email (ENCRYPT_AND_SIGN) to search for "status is active AND email is hanako@example.com".
A Compound Beacon generates a single beacon value by concatenating each part's value with a split character, enabling compound condition equality searches with a single GSI query.
⚠️ Warning: Replace
arn:aws:kms:ap-northeast-1:123456789012:key/your-key-idwith your own KMS key ARN.
Sample Code : CompoundBeaconExample.java
package com.example.ddbcse;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.*;
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
import software.amazon.cryptography.keystore.KeyStore;
import software.amazon.cryptography.keystore.model.CreateKeyInput;
import software.amazon.cryptography.keystore.model.CreateKeyOutput;
import software.amazon.cryptography.keystore.model.KeyStoreConfig;
import software.amazon.cryptography.keystore.model.KMSConfiguration;
import software.amazon.cryptography.materialproviders.IKeyring;
import software.amazon.cryptography.materialproviders.MaterialProviders;
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsHierarchicalKeyringInput;
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
import java.util.*;
/**
* Compound Beacon sample.
* Uses a Compound Beacon combining status (SIGN_ONLY) and email (ENCRYPT_AND_SIGN)
* to perform compound condition searches like "status = active AND email = hanako@example.com".
*
* Table: Users (deployed via sample.yaml)
* GSI: status-email-index (aws_dbe_b_status_email)
*/
public class CompoundBeaconExample {
private static final String TABLE_NAME = "Users";
private static final String KMS_KEY_ARN =
"arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id";
private static final String KEYSTORE_TABLE_NAME = "KeyStore";
public static void main(String[] args) {
// =======================================================
// 1. Create KeyStore client
// =======================================================
final KeyStore keyStore = KeyStore.builder()
.KeyStoreConfig(KeyStoreConfig.builder()
.ddbTableName(KEYSTORE_TABLE_NAME)
.kmsConfiguration(KMSConfiguration.builder()
.kmsKeyArn(KMS_KEY_ARN)
.build())
.logicalKeyStoreName(KEYSTORE_TABLE_NAME)
.ddbClient(DynamoDbClient.builder().build())
.kmsClient(KmsClient.builder().build())
.build())
.build();
// =======================================================
// 2. Create branch key
// =======================================================
System.out.println("Creating branch key...");
final CreateKeyOutput branchKeyOutput = keyStore.CreateKey(
CreateKeyInput.builder().build());
final String branchKeyId = branchKeyOutput.branchKeyIdentifier();
System.out.println("Branch key created: " + branchKeyId);
// =======================================================
// 3. Create Hierarchical Keyring
// =======================================================
final MaterialProviders matProv = MaterialProviders.builder()
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
.build();
final IKeyring hierarchicalKeyring =
matProv.CreateAwsKmsHierarchicalKeyring(
CreateAwsKmsHierarchicalKeyringInput.builder()
.keyStore(keyStore)
.branchKeyId(branchKeyId)
.ttlSeconds(600L)
.build());
// =======================================================
// 4. Define Standard Beacon (required for Compound Beacon's encrypted part)
// =======================================================
List<StandardBeacon> standardBeacons = new ArrayList<>();
standardBeacons.add(StandardBeacon.builder()
.name("email")
.length(30)
.build());
// =======================================================
// 5. Define Compound Beacon
// Combining status (SIGN_ONLY) and email (ENCRYPT_AND_SIGN)
// =======================================================
List<CompoundBeacon> compoundBeacons = new ArrayList<>();
compoundBeacons.add(CompoundBeacon.builder()
.name("status_email")
.split("#") // Split character (must not appear in any part's plaintext)
.encrypted(List.of(
EncryptedPart.builder()
.name("email")
.prefix("E-")
.build()))
.signed(List.of(
SignedPart.builder()
.name("status")
.prefix("S-")
.build()))
.build());
// =======================================================
// 6. Configure Beacon Version
// =======================================================
List<BeaconVersion> beaconVersions = new ArrayList<>();
beaconVersions.add(BeaconVersion.builder()
.version(1)
.keyStore(keyStore)
.keySource(BeaconKeySource.builder()
.single(SingleKeyStore.builder()
.keyId(branchKeyId)
.cacheTTL(600)
.build())
.build())
.standardBeacons(standardBeacons)
.compoundBeacons(compoundBeacons)
.build());
// =======================================================
// 7. Search configuration (SearchConfig)
// =======================================================
SearchConfig searchConfig = SearchConfig.builder()
.versions(beaconVersions)
.writeVersion(1)
.build();
// =======================================================
// 8. Define crypto actions per attribute
// =======================================================
final Map<String, CryptoAction> attributeActions = new HashMap<>();
attributeActions.put("pk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("sk", CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
attributeActions.put("email", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("name", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("age", CryptoAction.ENCRYPT_AND_SIGN);
attributeActions.put("status", CryptoAction.SIGN_ONLY);
// =======================================================
// 9. Table encryption config (Beacons enabled)
// =======================================================
final DynamoDbTableEncryptionConfig tableConfig =
DynamoDbTableEncryptionConfig.builder()
.logicalTableName(TABLE_NAME)
.partitionKeyName("pk")
.sortKeyName("sk")
.attributeActionsOnEncrypt(attributeActions)
.keyring(hierarchicalKeyring)
.search(searchConfig)
.build();
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
tableConfigs.put(TABLE_NAME, tableConfig);
// =======================================================
// 10. Create DynamoDbEncryptionInterceptor
// =======================================================
final DynamoDbEncryptionInterceptor interceptor =
DynamoDbEncryptionInterceptor.builder()
.config(DynamoDbTablesEncryptionConfig.builder()
.tableEncryptionConfigs(tableConfigs)
.build())
.build();
// =======================================================
// 11. Create encryption-enabled DynamoDB client
// =======================================================
final DynamoDbClient ddb = DynamoDbClient.builder()
.overrideConfiguration(
ClientOverrideConfiguration.builder()
.addExecutionInterceptor(interceptor)
.build())
.build();
// =======================================================
// 12. Write multiple items
// =======================================================
List<Map<String, AttributeValue>> items = List.of(
Map.of(
"pk", AttributeValue.builder().s("USER#201").build(),
"sk", AttributeValue.builder().s("PROFILE").build(),
"email", AttributeValue.builder().s("hanako@example.com").build(),
"name", AttributeValue.builder().s("花子").build(),
"age", AttributeValue.builder().n("25").build(),
"status", AttributeValue.builder().s("active").build()),
Map.of(
"pk", AttributeValue.builder().s("USER#202").build(),
"sk", AttributeValue.builder().s("PROFILE").build(),
"email", AttributeValue.builder().s("taro@example.com").build(),
"name", AttributeValue.builder().s("太郎").build(),
"age", AttributeValue.builder().n("30").build(),
"status", AttributeValue.builder().s("active").build()),
Map.of(
"pk", AttributeValue.builder().s("USER#203").build(),
"sk", AttributeValue.builder().s("PROFILE").build(),
"email", AttributeValue.builder().s("hanako@example.com").build(),
"name", AttributeValue.builder().s("花子(退会済み)").build(),
"age", AttributeValue.builder().n("25").build(),
"status", AttributeValue.builder().s("inactive").build()));
for (Map<String, AttributeValue> item : items) {
ddb.putItem(PutItemRequest.builder()
.tableName(TABLE_NAME)
.item(item)
.build());
System.out.println("Put: " + item.get("pk").s() + " / status=" + item.get("status").s()
+ " / email=" + item.get("email").s());
}
System.out.println();
// =======================================================
// 13. Raw get with plain client (verify Compound Beacon attribute)
// =======================================================
final DynamoDbClient plainClient = DynamoDbClient.builder().build();
final Map<String, AttributeValue> key = Map.of(
"pk", AttributeValue.builder().s("USER#201").build(),
"sk", AttributeValue.builder().s("PROFILE").build());
GetItemResponse rawResponse = plainClient.getItem(GetItemRequest.builder()
.tableName(TABLE_NAME)
.key(key)
.build());
System.out.println("=== Get item (raw) - beacon attributes visible ===");
rawResponse.item().forEach((k, v) -> System.out.println(" " + k + " = " + v));
System.out.println();
// =======================================================
// 14. Query using Compound Beacon
// status = "active" AND email = "hanako@example.com"
// =======================================================
System.out.println("=== Query via compound beacon (status-email-index) ===");
System.out.println(" Condition: status=active AND email=hanako@example.com");
QueryResponse queryResponse = ddb.query(QueryRequest.builder()
.tableName(TABLE_NAME)
.indexName("status-email-index")
.keyConditionExpression("aws_dbe_b_status_email = :val")
.expressionAttributeValues(Map.of(
":val",
AttributeValue.builder().s("S-active#E-hanako@example.com").build()))
.build());
System.out.println(" Items found: " + queryResponse.count());
queryResponse.items().forEach(i -> {
System.out.println(" ---");
i.forEach((k, v) -> System.out.println(" " + k + " = " + v));
});
}
}
The sample writes three items:
| Item | status | |
|---|---|---|
| USER#201 | active | hanako@example.com |
| USER#202 | active | taro@example.com |
| USER#203 | inactive | hanako@example.com |
Running the sample produces output like the following:
Creating branch key...
Branch key created: 781aefa0-5d25-440f-bbe7-106fa81bee14
Put: USER#201 / status=active / email=hanako@example.com
Put: USER#202 / status=active / email=taro@example.com
Put: USER#203 / status=inactive / email=hanako@example.com
=== Get item (raw) - beacon attributes visible ===
aws_dbe_b_status_email = AttributeValue(S=S-active#E-3d042b97)
status = AttributeValue(S=active)
aws_dbe_foot = AttributeValue(B=SdkBytes(bytes=...))
aws_dbe_head = AttributeValue(B=SdkBytes(bytes=...))
pk = AttributeValue(S=USER#201)
email = AttributeValue(B=SdkBytes(bytes=...))
name = AttributeValue(B=SdkBytes(bytes=...))
aws_dbe_b_email = AttributeValue(S=3d042b97)
age = AttributeValue(B=SdkBytes(bytes=...))
sk = AttributeValue(S=PROFILE)
=== Query via compound beacon (status-email-index) ===
Condition: status=active AND email=hanako@example.com
Items found: 1
---
name = AttributeValue(S=花子)
sk = AttributeValue(S=PROFILE)
pk = AttributeValue(S=USER#201)
email = AttributeValue(S=hanako@example.com)
age = AttributeValue(N=25)
status = AttributeValue(S=active)
From the raw get result, we can see the Standard Beacon (aws_dbe_b_email = 3d042b97) and the Compound Beacon (aws_dbe_b_status_email = S-active#E-3d042b97). The Compound Beacon value is a concatenation of S-active (status plaintext with prefix S-) and E-3d042b97 (email beacon value with prefix E-), joined by the split character #.
The query result returns only USER#201, which matches both status = "active" and email = "hanako@example.com". The SDK automatically converts the search value S-active#E-hanako@example.com to S-active#E-3d042b97 and executes a single query against the GSI.
Note that the split character must be a character that does not appear in any part's plaintext value. In this example, # is used, but a different character should be chosen if # could appear in the plaintext.
Key Considerations
Here is a summary of important points to be aware of when introducing client-side encryption.
Using Filter/Condition Expressions on Encrypted Attributes
FilterExpression and ConditionExpression do not work on encrypted attributes. If you need to search on encrypted attributes, consider using Beacons.
Signature Verification Errors
Adding, deleting, or modifying attributes in an item will cause the signature to no longer match, resulting in an error on retrieval (tamper detection). Care must be taken when changing the attribute structure of signed items.
For example, deleting or modifying a signed attribute via the management console produces the following errors:
When a signed attribute is deleted:
StructuredEncryptionException: Schema changed : something that was signed is now unsigned.
When a signed attribute value is modified:
StructuredEncryptionException: Signature of record does not match the signature computed when the record was encrypted.
Increased Item Size
Encryption increases item size for the following reasons:
- Encrypted values are stored as binary type and become larger than the original due to padding
- Metadata attributes (
aws_dbe_head,aws_dbe_foot) are added - When using Beacons, beacon attributes are also added
Therefore, design with margin against DynamoDB's item size limit (400KB).
Data Not Visible in Console or CLI
Encrypted attributes are stored as binary values, so their contents cannot be viewed in the management console or AWS CLI.
Difficulty Adding/Changing Beacons After the Fact
When using Beacons, it is extremely important to determine search patterns (which attributes to search on) in advance. While DynamoDB always requires upfront access pattern planning, Beacons make this even more critical.
If you want to add a new beacon later, the following steps are required:
- Add the new beacon definition and create a corresponding GSI
- Re-write all existing data (existing items don't have the new beacon attribute and won't be searchable)
Additionally, if you rotate the branch key, the HMAC key used for beacon value derivation changes, requiring all items' beacon values to be recalculated (re-written).
With regular DynamoDB, simply adding a GSI automatically indexes existing data, but this is not the case with Beacons.
PartiQL (ExecuteStatement) Is Not Supported
The DynamoDbEncryptionInterceptor in the AWS Database Encryption SDK does not support PartiQL (ExecuteStatement / BatchExecuteStatement). If your existing application accesses DynamoDB using PartiQL, you need to refactor those calls to use the standard DynamoDB API (PutItem / GetItem / Query, etc.) before introducing attribute-level encryption.
SDK History and Migration
The DynamoDB client-side encryption library has undergone name changes and generational transitions to reach its current form.
Timeline
| Date | Event |
|---|---|
| May 2018 | Amazon DynamoDB Encryption Client released for Java and Python |
| July 2022 | DynamoDB Encryption Client Java v1.x, Python v1.x/v2.x entered end-of-support phase |
| June 2023 | Library renamed to AWS Database Encryption SDK. Released as a new-generation SDK with a new encryption format and Searchable Encryption (Beacons) for Java |
| Present | AWS Database Encryption SDK available for Java, .NET, Rust. Python continues with the legacy DynamoDB Encryption Client v3.x |
https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/document-history.html
Two SDK Families
As described above, two SDK families currently coexist for DynamoDB client-side encryption:
| AWS Database Encryption SDK (New Generation) | Amazon DynamoDB Encryption Client (Legacy) | |
|---|---|---|
| Supported Languages | Java, .NET, Rust | Java, Python |
| Metadata Attributes |
aws_dbe_head / aws_dbe_foot
|
*amzn-ddb-map-desc* / *amzn-ddb-map-sig*
|
| Searchable Encryption | Supported (Beacons) | Not supported |
| Interoperability | Cross-language decryption within the same SDK family | Cross-language decryption within the same SDK family |
Migration
The two SDK families cannot encrypt/decrypt each other's data. Therefore, migrating from the legacy DynamoDB Encryption Client to the AWS Database Encryption SDK follows a phased approach (handle both formats on read → switch writes to the new format).
Conclusion
This article covered DynamoDB encryption from server-side encryption to client-side attribute-level encryption.
Server-side encryption is handled transparently by DynamoDB, so you rarely need to think about it in day-to-day development. However, understanding the key options and audit requirements remains important.
On the other hand, client-side attribute-level encryption is a powerful way to protect data from third parties including AWS, but it comes with tradeoffs such as query constraints and increased item size. In particular, if you need to search on encrypted attributes, Beacons must be introduced, and the added design complexity should be considered upfront.
Note that the AWS Database Encryption SDK for DynamoDB currently supports only Java, .NET, and Rust. If you want to perform client-side encryption with Python, you'll need to use the legacy DynamoDB Encryption Client (v3.x), but be aware that newer features like Beacons are not available.
When considering adoption, I recommend first identifying "which attributes are sensitive and which attributes need to be searchable," then deciding on crypto actions and whether Beacons are needed.
I hope this article helps improve your understanding of DynamoDB encryption.


Top comments (0)