DEV Community

Cover image for Rethinking DynamoDB Encryption : From Attribute-Level Encryption to Beacons
ほうき星 for AWS Community Builders

Posted on Originally published at qiita.com

Rethinking DynamoDB Encryption : From Attribute-Level Encryption to Beacons

This article is a machine translation of the contents of the following URL, which I wrote in Japanese:

DynamoDB の暗号化を改めて考える:属性レベル暗号化から Beacons まで #AWS - Qiita

はじめに こんにちは、ほうき星 @H0ukiStar です。 皆さんは DynamoDB を暗号化して利用していますでしょうか? DynamoDB を暗号化なしで作成することはできませんので、この問いに「暗号化してなーい!」という返答は帰ってこないかと思います。 Dyna...

favicon qiita.com

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:
DynamoDB encryption overview

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_ONLY or SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT.

⚠️ Warning: SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT is 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.

DynamoDB table creation - encryption key selection

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.

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.usagenotes.html#dynamodb-kms

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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-id with 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));
    }
}
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

Looking at the raw (encrypted) item, we can observe the following:

  • email, name, age are stored as binary (B type) — they are encrypted
  • pk, sk, status remain as plaintext (S type) — signed only, not encrypted
  • aws_dbe_head and aws_dbe_foot metadata 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.

image.png

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
Enter fullscreen mode Exit fullscreen mode

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-id with 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()
Enter fullscreen mode Exit fullscreen mode
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'
Enter fullscreen mode Exit fullscreen mode

Looking at the raw (encrypted) item, we can observe the following:

  • email, name, age are stored as binary (Binary type) — they are encrypted
  • pk, sk, status remain 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());
Enter fullscreen mode Exit fullscreen mode
=== Scan with FilterExpression: email = 'taro@example.com' (String) ===
  Items found: 0
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode
  Items found: 1
Enter fullscreen mode Exit fullscreen mode

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-id with 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));
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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-id with 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));
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The sample writes three items:

Item status email
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)
Enter fullscreen mode Exit fullscreen mode

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).

https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/ddb-java-migrate.html#ddb-java-v2-to-v3

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)