2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

2
Last updated at Posted at 2026-08-25

はじめに

こんにちは、ほうき星 @H0ukiStar です。

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

DynamoDB はデフォルトで AWS が所有するキーによりデータ保管時に、サーバ側で暗号化されています。このサーバ側の暗号化では要件に応じ 3 つのキーで暗号化することが可能です。

また、DynamoDB はクライアント側の暗号化にも対応しており、このクライアント側の暗号化では属性レベルでの暗号化を行うことが可能です。

今回は、この DynamoDB に関する各暗号化方法について改めて考えてみます。
特にクライアント側の属性レベルの暗号化については、基本的な暗号化から Beacons(Searchable Encryption)まで実際に試してみます。

DynamoDB が対応している暗号化

はじめにでも触れたように DynamoDB はサーバ側の暗号化、クライアント側の暗号化、そして転送中の暗号化の 3 つに対応しています。

DynamoDB に関する暗号化の全体像は以下になります。
figure.png

サーバ側の暗号化

DynamoDB ではテーブル作成時に「暗号化しない」という選択肢はなく、サーバ側で保管時の暗号化は必ず行われます。
これは、テーブルがディスクに保存される際に DynamoDB がテーブルを透過的に暗号化し、ユーザーがテーブルデータにアクセスする際にテーブルを復号するものです。

このサーバ側の暗号化には以下の 3 つの KMS キーのいずれかを使用することができ、それぞれの特徴は以下の通りです。

キーの種類 管理者 コスト ユースケース
AWS が所有するキー(デフォルト) AWS 無料 特別な要件がない場合
AWS マネージドキー (aws/dynamodb) AWS(ユーザーが確認可能) KMS の API 料金 CloudTrail でキー使用を監査したい場合
カスタマーマネージドキー (CMK) ユーザー KMS キー料金 + API 料金 キーのローテーション制御、キーポリシーの管理、クロスアカウントアクセスが必要な場合

要件としては以下のように整理できます。

  • 特にキーの管理要件がない場合:AWS が所有するキー(デフォルト)
  • キーの使用状況を CloudTrail で監査したい場合:AWS マネージドキー
  • キーのライフサイクルやアクセスポリシーを自身で管理したい場合:カスタマーマネージドキー

クライアント側の暗号化

サーバ側での保管時の暗号化に加え、クライアント側での暗号化を行うことができます。
クライアント側の暗号化では、クライアントアプリケーションが DynamoDB に送るデータそのものを暗号化し、暗号化される前のデータは AWS を含む第三者に公開されることは無いのが特徴です。

このクライアント側の暗号化には AWS Database Encryption SDK for DynamoDB もしくは Amazon DynamoDB Encryption Client を用いて行い、暗号化する対象は属性ごとに選択することができます。

暗号化アクションの種類

AWS Database Encryption SDK for DynamoDB を用いた属性レベルの暗号化では、属性ごとにどのような暗号化・署名処理を行うかを「暗号化アクション(Crypto Action)」で指定します。
選択肢は以下の 4 つです。

アクション 暗号化 署名 用途
ENCRYPT_AND_SIGN 機密性と完全性の両方を保護したい属性(メールアドレス、住所など)
SIGN_ONLY × 暗号化は不要だが改ざん検知が必要な属性(パーティションキー、ソートキーなど)
SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT × 署名に加え、暗号化コンテキストにも含める属性(監査ログに残したい識別子など)
DO_NOTHING × × 暗号化も署名も不要な属性(後から自由に追加・変更したい属性)

パーティションキーとソートキーは暗号化することができません。
DynamoDB はこれらの値を使ってアイテムの格納先の決定やインデックスの構築を行うため、暗号化するとサーバ側でルーティングができなくなります。
これらの属性には SIGN_ONLY または SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT のみ指定することができます。

SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT は AWS Database Encryption SDK で追加されたアクションであり、Amazon DynamoDB Encryption Client では指定できません。
なお、2 つの SDK の違いや歴史については後述を参照してください。

キーリング(キープロバイダー)の選択肢

クライアント側暗号化で使用するキーリングは主に以下から選択できます。

  • AWS KMS キーリング:KMS のキー管理機能を活用でき、運用負荷が低い
  • Raw AES キーリング:KMS を使用せず、自前でキーを管理するケース
  • Raw RSA キーリング:非対称キーを使用するケース
  • AWS KMS Hierarchical キーリング:ブランチキーによるキャッシュでパフォーマンスを向上させるケース(Beacons 利用時は必須)

本記事では AWS KMS キーリングおよび Hierarchical キーリングを使用してサンプルを示します。

【補足】転送中の暗号化

DynamoDB はクライアント - サーバ間のデータ転送に HTTPS を使用して、転送中のデータを暗号化しています。

なお、暗号化のベストプラクティスでは、この転送中においても追加のセキュリティ対策が推奨されています。

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.

実際に試してみる

サーバ側の暗号化

DynamoDB のテーブル作成時に選択した「暗号化キー」によって、サーバ側で保管時に暗号化されます。

image.png

サーバ側の暗号化は DynamoDB が透過的に処理してくれるため、アプリケーション側での対応は不要です。テーブル作成時にキーを選択するだけで完了します。

また、テーブル作成後、暗号化に使用するキーをダウンタイム無しで変更することが可能です。

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.

属性レベル(クライアント側)の暗号化

ここからがこの記事の本題です。
AWS Database Encryption SDK for DynamoDB(Java)と DynamoDB Encryption Client(Python)を用いて、属性レベルの暗号化を行います。

DynamoDB テーブル構造と CloudFormation テンプレート

サンプルで利用する DynamoDB テーブルや KMS キーなどは以下の CloudFormation テンプレートとして定義しています。

なお、サンプルでは以下のようなユーザー情報を格納するテーブルとしました。

属性名 暗号化アクション 理由
pk String(パーティションキー) SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT キー属性は暗号化不可。暗号化コンテキストに含めて監査性を確保
sk String(ソートキー) SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT 同上
email String ENCRYPT_AND_SIGN 個人情報のため暗号化
name String ENCRYPT_AND_SIGN 個人情報のため暗号化
age Number ENCRYPT_AND_SIGN 個人情報のため暗号化
status String SIGN_ONLY 機密ではないが改ざんは防ぎたい
サンプルテンプレートはこちら
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:
        # Standard Beacon 用 GSI
        - IndexName: email-index
          KeySchema:
            - AttributeName: aws_dbe_b_email
              KeyType: HASH
          Projection:
            ProjectionType: ALL
        # Compound Beacon 用 GSI
        - 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

このテンプレートでは以下のリソースを作成します。

リソース 説明
EncryptionKey (KMS) クライアント側暗号化と KeyStore のブランチキー暗号化に使用する対称キー(自動ローテーション有効)
UsersTable サンプルアプリケーションのメインテーブル
KeyStoreTable Hierarchical Keyring のブランチキー管理テーブル、SDK が要求するスキーマ(branch-key-id / type)で作成

また、Users テーブルには Standard Beacon / Compound Beacon 検索用に GSI を 2 つ設定しています。

GSI 名 キー属性 用途
email-index aws_dbe_b_email Standard Beacon を使用した email 単体の等価検索
status-email-index aws_dbe_b_status_email Compound Beacon を使用した status + email の複合条件検索

基本的な put / get のサンプル

AWS Database Encryption SDK(Java)の場合

AWS Database Encryption SDK(Java)を用いた属性レベルの暗号化のサンプルを示します。

なお、筆者は以下の環境で動作を確認しました。

  • Amazon Corretto 21(Java 21)
  • 依存関係(Apache Maven 3.9):
    <dependency>
        <groupId>software.amazon.cryptography</groupId>
        <artifactId>aws-database-encryption-sdk-dynamodb</artifactId>
        <version>3.9.0</version>
    </dependency>
    

DynamoDbEncryptionInterceptor を使用して暗号化クライアントを設定し、アイテムの put と get を行います。

サンプルコードはこちら

arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id はご自身の環境に置き換えてください

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. キーリングの作成
        // =======================================================
        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. 属性ごとの暗号化アクション定義
        // =======================================================
        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. テーブル暗号化設定
        // =======================================================
        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. DynamoDbEncryptionInterceptor の作成
        // =======================================================
        final DynamoDbEncryptionInterceptor interceptor =
                DynamoDbEncryptionInterceptor.builder()
                        .config(DynamoDbTablesEncryptionConfig.builder()
                                .tableEncryptionConfigs(tableConfigs)
                                .build())
                        .build();

        // =======================================================
        // 5. 暗号化対応 DynamoDB クライアントの作成
        // =======================================================
        final DynamoDbClient ddb = DynamoDbClient.builder()
                .overrideConfiguration(
                        ClientOverrideConfiguration.builder()
                                .addExecutionInterceptor(interceptor)
                                .build())
                .build();

        // =======================================================
        // 6. アイテムの書き込み
        // =======================================================
        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. 通常クライアントで取得して暗号化状態を確認
        // =======================================================
        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. アイテムの読み取り(自動復号)
        // =======================================================
        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));
    }
}

暗号化されたアイテムの確認

上記のサンプルでは、put 後に通常の DynamoDB クライアント(Interceptor なし)で取得して暗号化状態を確認し、その後に暗号化クライアントで取得して復号結果を表示しています。
実行すると以下のような出力を確認することができます。

実行結果
=== 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=0x0376e53f884a0c4c35faa06837ef0cd432f7c26babe3d5f64638f18b82a04c8f893552b6d59a4691e33e03e378daacb130650230267fe06a6a245d402154d9bb7211250348b468fe8c9800b73c2eb3f5c1fe6ab948d5920ebed5739a3f47c760d21e2400023100b1288bb64e43941a53b9911da3c6e2b44e55e8f3e7907d140a0924f30867a11f61d0affa9b55f192c3f69f5373807a41))
  aws_dbe_head = AttributeValue(B=SdkBytes(bytes=0x02010fceda3534943bc81d3c30ba0f24878f324ab58aa7d2538f410a0569526df7320006636365656573000100156177732d63727970746f2d7075626c69632d6b6579004441796c566f36456e427443434f4c7139535657446369544543713172534143744a56507971476c68344858504c7030396f63437830302f4e414c495271626e6154413d3d0100076177732d6b6d73005061726e3a6177733a6b6d733a61702d6e6f727468656173742d313a3233393039343739393634363a6b65792f65353065653766322d363339642d343138322d613535302d39396134613735633433376100e8d07c1723394fe79e4533e4fd28c4f763325ff0aff289b03892d1dd7ca6228c8ee203bf618a9abae496f3fa7c3623c583010201007862e489ba68c2a77ef1e62a41c689994988cb2e55237ca7dd50b9efe0c95e9a9d01bd141f983ee84d775845a7bd5dbf93910000007e307c06092a864886f70d010706a06f306d020100306806092a864886f70d010701301e060960864801650304012e3011040c7b4998bd72b940006c3677d1020110803bd8b30b7618a86ee6ecad6198f8949a2e13f86f6b031792d33e3b5c7e04c771eff976c980ba28df32b7e4cb835840763c109c2e6e145da49c11c697ff4b453a229f9114b4000382865c4ee3eb029b879afdd2da55da59da06344f3c))
  pk = AttributeValue(S=USER#001)
  email = AttributeValue(B=SdkBytes(bytes=0x0001c341cfe3b9230d1e430fcfeccd9ce20164a7a7f6d12c1dfd17ed568bec214729))
  name = AttributeValue(B=SdkBytes(bytes=0x0001737cdc1b5cbbcb086ce000d1c8c1a05283ab3bf9ff9b))
  age = AttributeValue(B=SdkBytes(bytes=0x0002c79e2341e184e681675bf5119ee5e1a91efe))
  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)

出力された暗号化状態のアイテム(raw)を確認すると以下の状態であることが分かります。

  • emailnameage はバイナリ(B 型)で格納されており暗号化されている
  • pkskstatus は平文のまま(S 型)で、暗号化されず署名のみ
  • aws_dbe_headaws_dbe_foot というメタデータ属性が SDK によって自動的に追加されている

また、DynamoDB の「項目を探索」から該当のアイテムを確認しても、コンソールに出力された暗号化状態のアイテム(raw)と同じことが確認できます。

image.png

Amazon DynamoDB Encryption Client(Python)の場合

Amazon DynamoDB Encryption Client(Python)を用いた属性レベルの暗号化のサンプルを示します。

なお、筆者は以下の環境で動作を確認しました。

  • Python 3.14
  • 依存関係:
    dynamodb-encryption-sdk==3.3.0
    boto3==1.43.73
    

EncryptedTable クラスを使用して暗号化テーブルリソースを作成し、アイテムの put と get を行います。このクラスは内部で暗号化・復号を透過的に行うため、通常の boto3 の Table と同じ感覚で利用できます。

サンプルコードはこちら

arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id はご自身の環境に置き換えてください

basic_put_get_example.py
"""DynamoDB Client-Side Encryption - Basic Put/Get Example (Python)

属性レベルの暗号化を行い、アイテムの put と get を実行するサンプルです。
AWS KMS を使用して暗号化マテリアルを提供します。
"""

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. KMS CMP(暗号化マテリアルプロバイダー)の作成
    # =======================================================
    kms_cmp: AwsKmsCryptographicMaterialsProvider = (
        AwsKmsCryptographicMaterialsProvider(key_id=KMS_KEY_ARN)
    )

    # =======================================================
    # 2. 属性ごとの暗号化アクション定義
    # =======================================================
    actions: AttributeActions = AttributeActions(
        default_action=CryptoAction.ENCRYPT_AND_SIGN,
        attribute_actions={
            # パーティションキー・ソートキーは暗号化不可(EncryptedTable が自動で SIGN_ONLY にする)
            "pk": CryptoAction.SIGN_ONLY,
            "sk": CryptoAction.SIGN_ONLY,
            "status": CryptoAction.DO_NOTHING,  # 署名も暗号化もしない属性
        },
    )

    # =======================================================
    # 3. 通常の DynamoDB テーブルリソースの作成
    # =======================================================
    table = boto3.resource("dynamodb").Table(TABLE_NAME)

    # =======================================================
    # 4. 暗号化対応テーブルの作成
    # =======================================================
    encrypted_table: EncryptedTable = EncryptedTable(
        table=table,
        materials_provider=kms_cmp,
        attribute_actions=actions,
    )

    # =======================================================
    # 5. アイテムの書き込み
    # =======================================================
    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. 通常テーブルリソースで取得して暗号化状態を確認
    # =======================================================
    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. 暗号化テーブルリソースで取得(自動復号)
    # =======================================================
    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()

暗号化されたアイテムの確認

AWS Database Encryption SDK(Java)版と同様に、put 後に通常のテーブルリソース(暗号化なし)で取得して暗号化状態を確認し、その後に EncryptedTable で取得して復号結果を表示しています。
実行すると以下のような出力を確認することができます。

実行結果
=== 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\x00\x00\x00\x10amzn-ddb-env-alg\x00\x00\x00\x07AES/256\x00\x00\x00\x10amzn-ddb-env-key\x00\x00\x00\xf8AQIBAHhi5Im6aMKnfvHmKkHGiZlJiMsuVSN8p91Que/gyV6anQHASZ9RkA+ti6F8TusZyLDBAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMR/RQMiCZVj9GHfHNAgEQgDvM4I7T66PvwN/g3ieAH6CBcUOAYSejPJuHBEOZDDbFyviDC4xZhiKBPW/QQMh8ah77S8pRyezXVu1l3g==\x00\x00\x00\x17amzn-ddb-map-signingAlg\x00\x00\x00\nHmacSHA256\x00\x00\x00\x15amzn-ddb-map-sym-mode\x00\x00\x00\x11/CBC/PKCS5Padding\x00\x00\x00\x10amzn-ddb-sig-alg\x00\x00\x00\x0eHmacSHA256/256\x00\x00\x00\x11amzn-ddb-wrap-alg\x00\x00\x00\x03kms\x00\x00\x00\x0faws-kms-ec-attr\x00\x00\x00\x06*keys*')
  status = 'active'
  *amzn-ddb-map-sig* = Binary(b'\xa1\xef\x02]\xb7Z\x12\xdb]5\xb7\xd4\x9d\x05\xb2\xc3\xc8[\x1d\xf3]\xa2\xa8\x95\xa9E{\xdc7y\x89:')
  pk = 'USER#002'
  email = Binary(b'\x8e\x02\xd8V\xe8[\xdcr\x11\x9f\xf9\xcb\xe1\x884o\xb5?)\xc3\x1a\xea\x17,\xfa\x81&\xb7`\x02\xa3;w\x85(\xe1\x0f\xa7e\x8e\xdb\x88\x10[\x94\xed\xb4\x7f')
  name = Binary(b'\x04\x8ceI\xf9\xb1\xe6\xeb\x83\x8a\x90\xbf\\\x17`\x0e\x166\xb8\xef>\x91`:\xccQ\xc9\xec\x98\xe4\xb0J')
  age = Binary(b'\xec_x\xe7\xeb\x11\xf0/\xd3S\x0e\x9cG\xc1\xb8|o\xda\xb0\xc8}\xa3m\xb2\xbb\xf0\xc5\xdbjM\x14)')
  sk = 'PROFILE'

=== Get item (decrypted) ===
  status = 'active'
  pk = 'USER#002'
  email = 'jiro@example.com'
  name = '次郎'
  age = Decimal('26')
  sk = 'PROFILE'

出力された暗号化状態のアイテム(raw)を確認すると以下の状態であることが分かります。

  • emailnameage はバイナリ(Binary 型)で格納されており暗号化されている
  • pkskstatus は平文のまま
  • *amzn-ddb-map-desc**amzn-ddb-map-sig* というメタデータ属性が SDK によって自動的に追加されている

異なる SDK 間で相互に復号はできない

AWS Database Encryption SDK と DynamoDB Encryption Client は異なる暗号化形式を使用しており、メタデータ属性名も異なります(前者は aws_dbe_head / aws_dbe_foot、後者は *amzn-ddb-map-desc* / *amzn-ddb-map-sig*)。
そのため、異なる SDK 間で暗号化したアイテムを相互に復号することはできず、例えば DynamoDB Encryption Client(Python)で暗号化したアイテムを AWS Database Encryption SDK(Java)で復号しようとすると以下のようなエラーを確認できます。

[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
~省略~

同じ SDK 系列の異なる言語間であれば相互に復号可能です。
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

なお、2 つの SDK の違いや歴史については後述を参照してください。

属性レベルの暗号化時のクエリ

属性レベルの暗号化を導入すると、暗号化された属性に対するクエリに制約が生じます。

暗号化属性に対するフィルタ・条件式の制約

暗号化された属性は DynamoDB サーバ側では復号できないため、以下の操作が行えません。

  • フィルタ式(FilterExpression)での使用email = :val のような条件でフィルタしても、サーバ側では暗号化されたバイナリ値と比較されるためマッチしない
  • 条件式(ConditionExpression)での使用:同様の理由で条件付き書き込みも機能しない
  • GSI / LSI のキー属性:同様の理由で暗号化された属性をインデックスのキーに指定しても、意味のあるクエリができない
暗号化された属性に平文で Filter してもマッチしない例
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());
if (response1.count() > 0) {
    response1.items().forEach(item -> {
        System.out.println("  ---");
        item.forEach((k, v) -> System.out.println("    " + k + " = " + v));
    });
}
System.out.println();
実行結果
=== Scan with FilterExpression: email = 'taro@example.com' (String) ===
  Items found: 0

暗号化後のバイナリを知っていれば Filter でマッチするものの、以下の理由から暗号化後のバイナリを FilterExpression 組み立て時に導出できないため、事実上検索できません。

  • AES-GCMでは、一般的に暗号化時に毎回異なる nonce(IV)が使用されるため、同じ平文を同じ鍵で暗号化しても、暗号化のたびに異なる暗号文が生成される
  • そのため、検索したい平文値から「テーブルに格納されている暗号文」を事前に計算することはできない
暗号化後のバイナリを知っていれば Filter でマッチできる例
byte[] encryptedEmailBytes = Base64.getDecoder().decode(
        "AAHDQc/juSMNHkMPz+zNnOIBZKen9tEsHf0X7VaL7CFHKQ==");
System.out.println("=== Scan with FilterExpression: email = <encrypted binary> (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());
if (response2.count() > 0) {
    response2.items().forEach(item -> {
        System.out.println("  ---");
        item.forEach((k, v) -> System.out.println("    " + k + " = " + v));
    });
}
実行結果
  Items found: 1
  ---
    status = AttributeValue(S=active)
    aws_dbe_foot = AttributeValue(B=SdkBytes(bytes=0x0376e53f884a0c4c35faa06837ef0cd432f7c26babe3d5f64638f18b82a04c8f893552b6d59a4691e33e03e378daacb130650230267fe06a6a245d402154d9bb7211250348b468fe8c9800b73c2eb3f5c1fe6ab948d5920ebed5739a3f47c760d21e2400023100b1288bb64e43941a53b9911da3c6e2b44e55e8f3e7907d140a0924f30867a11f61d0affa9b55f192c3f69f5373807a41))
    sk = AttributeValue(S=PROFILE)
    aws_dbe_head = AttributeValue(B=SdkBytes(bytes=0x02010fceda3534943bc81d3c30ba0f24878f324ab58aa7d2538f410a0569526df7320006636365656573000100156177732d63727970746f2d7075626c69632d6b6579004441796c566f36456e427443434f4c7139535657446369544543713172534143744a56507971476c68344858504c7030396f63437830302f4e414c495271626e6154413d3d0100076177732d6b6d73005061726e3a6177733a6b6d733a61702d6e6f727468656173742d313a3233393039343739393634363a6b65792f65353065653766322d363339642d343138322d613535302d39396134613735633433376100e8d07c1723394fe79e4533e4fd28c4f763325ff0aff289b03892d1dd7ca6228c8ee203bf618a9abae496f3fa7c3623c583010201007862e489ba68c2a77ef1e62a41c689994988cb2e55237ca7dd50b9efe0c95e9a9d01bd141f983ee84d775845a7bd5dbf93910000007e307c06092a864886f70d010706a06f306d020100306806092a864886f70d010701301e060960864801650304012e3011040c7b4998bd72b940006c3677d1020110803bd8b30b7618a86ee6ecad6198f8949a2e13f86f6b031792d33e3b5c7e04c771eff976c980ba28df32b7e4cb835840763c109c2e6e145da49c11c697ff4b453a229f9114b4000382865c4ee3eb029b879afdd2da55da59da06344f3c))
    pk = AttributeValue(S=USER#001)
    email = AttributeValue(B=SdkBytes(bytes=0x0001c341cfe3b9230d1e430fcfeccd9ce20164a7a7f6d12c1dfd17ed568bec214729))
    name = AttributeValue(B=SdkBytes(bytes=0x0001737cdc1b5cbbcb086ce000d1c8c1a05283ab3bf9ff9b))
    age = AttributeValue(B=SdkBytes(bytes=0x0002c79e2341e184e681675bf5119ee5e1a91efe))

では、暗号化した属性で検索を行いたい場合はどうすれば良いのでしょうか?ここで Beacons(Searchable Encryption)の出番です。

Beacons(Searchable Encryption)

AWS Database Encryption SDK は、暗号化した属性に対してもクエリを可能にする仕組みとして Beacons を提供しています。

Amazon DynamoDB Encryption Client は Beacons に対応していません。

Beacons の仕組み

Beacons は、暗号化対象の属性の平文値から HMAC を計算し、その一部(truncated HMAC)を「ビーコン」として別の属性に格納する仕組みです。このビーコン値は元の平文を復元することはできませんが、同じ平文からは常に同じビーコン値が生成されるため、等価一致クエリ(equality search)に利用できます。

なお、平文そのものではなくハッシュ値を利用するため、以下の特徴(注意点)があります。

  • ビーコン値は平文のハッシュを切り詰めたもののため、異なる平文が同じビーコン値を持つ可能性がある(衝突)
  • ビーコン長を短くするほど衝突が増えるが、元の値の推測がより困難になる(セキュリティ向上)
  • ビーコン長を長くすると衝突が減りクエリ精度が向上するが、統計的な推測リスクが高まる

Beacons の種類

種類 用途
Standard Beacon 単一属性に対する等価一致検索
Compound Beacon 複数属性を組み合わせた検索

Standard Beacon のサンプル

以下は email 属性に対して Standard Beacon を設定し、暗号化された状態でもメールアドレスによる検索を可能にする例です。

Beacons を利用する場合は Hierarchical Keyring が必須であり、そのためには KeyStore(ブランチキー管理テーブル)を事前に作成しておく必要があります。

サンプルコードはこちら

arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id はご自身の環境に置き換えてください

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. KeyStore の作成
        // =======================================================
        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. ブランチキーの作成
        //    (既に作成済みの場合は、既存の branchKeyId を直接指定しても可)
        // =======================================================
        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. 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. Standard Beacon の定義
        // =======================================================
        List<StandardBeacon> standardBeacons = new ArrayList<>();
        standardBeacons.add(StandardBeacon.builder()
                .name("email")
                .length(30)  // ビーコン長(ビット数)
                .build());

        // =======================================================
        // 5. 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. 検索設定(SearchConfig)
        // =======================================================
        SearchConfig searchConfig = SearchConfig.builder()
                .versions(beaconVersions)
                .writeVersion(1)
                .build();

        // =======================================================
        // 7. 属性ごとの暗号化アクション定義
        // =======================================================
        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. テーブル暗号化設定(Beacons 有効)
        // =======================================================
        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. DynamoDbEncryptionInterceptor の作成
        // =======================================================
        final DynamoDbEncryptionInterceptor interceptor =
                DynamoDbEncryptionInterceptor.builder()
                        .config(DynamoDbTablesEncryptionConfig.builder()
                                .tableEncryptionConfigs(tableConfigs)
                                .build())
                        .build();

        // =======================================================
        // 10. 暗号化対応 DynamoDB クライアントの作成
        // =======================================================
        final DynamoDbClient ddb = DynamoDbClient.builder()
                .overrideConfiguration(
                        ClientOverrideConfiguration.builder()
                                .addExecutionInterceptor(interceptor)
                                .build())
                .build();

        // =======================================================
        // 11. アイテムの書き込み
        // =======================================================
        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 取得(ビーコン属性を確認)
        // =======================================================
        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. ビーコンを使った検索(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));
        });
    }
}

上記のサンプルを実行すると、以下のような出力を確認することができます。

実行結果
Creating branch key...
Branch key created: 7b700228-d086-4446-bf71-bb4e95e95907
=== Put item (plaintext) ===
  sk = AttributeValue(S=PROFILE)
  name = AttributeValue(S=花子)
  pk = AttributeValue(S=USER#100)
  email = AttributeValue(S=hanako@example.com)
  age = AttributeValue(N=25)
  status = AttributeValue(S=active)
Put item with beacon completed.

=== Get item (raw) - beacon attribute visible ===
  status = AttributeValue(S=active)
  aws_dbe_foot = AttributeValue(B=SdkBytes(bytes=0xcb042e45c04e35b9dbb7cb22350d8415161121a6b708d2a95ae6948df79de0cc7baa07fd8feb4d693cf22ac45d84f15c306502300d084c7a61c05c29dcf97212e2cdc3dafaadf6ff6dbbb923074dc7f8cd26fbf0d04d76d0c7e29836a40b0b944f06bca2023100bd80b3437c15ac3543ef98ac9299abdf7cc93f5888bb76d46ff0d7d356d55f4613c8c9e32557e0624f25d6084943361a))
  aws_dbe_head = AttributeValue(B=SdkBytes(bytes=0x02018b0ba5ebe5c792ddd28b8c936e1a4faa9e98bf6b9d66f26b9e8a308c31a05be60006636365656573000100156177732d63727970746f2d7075626c69632d6b6579004441684a6c7568753061505669583879707654344b5a454837487a466a7533524e307243774b7a716e67744a6c6947474a49796d5534587a686c7a2f5947777a6d61673d3d0100116177732d6b6d732d686965726172636879002437623730303232382d643038362d343434362d626637312d626234653935653935393037008cc4f3e256d86294f91f070720b278e05a75c413cc40ba2254302250568b461b59f8c21f43047670f0a695b493af28df5c1be6d148a26de1b4d6bad398a8803e87ecc98d5cc4684b3c9ee662c72cbe54e99c204f6e914287be98a248ebb1f617c24220a4cf220b6d4f2056d70d3b9df69d9741bf8fa2e5045bd07f84121bef236e0cf1146ae3a0046cdeeced24f6a514b14fcf619f1d582b5ea1c0dcfdd3e1daa5de629d77727bb5ede71f1b68))
  pk = AttributeValue(S=USER#100)
  email = AttributeValue(B=SdkBytes(bytes=0x00014789e99bb353dec03a55fb09ba94100caf7a95d7253b73a9dca3c1ff482ba5da8f36))
  name = AttributeValue(B=SdkBytes(bytes=0x0001d0687963a9a90e370322f961a34d9c381776e1b25178))
  aws_dbe_b_email = AttributeValue(S=176be958)
  age = AttributeValue(B=SdkBytes(bytes=0x0002514667c3c4ff996f3b69dcbe41b40ad40d09))
  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)

raw 取得の結果を見ると、基本的な暗号化のサンプルで確認した属性(aws_dbe_headaws_dbe_foot、暗号化されたバイナリ属性)に加え、aws_dbe_b_email という新しい属性が確認できます。これが SDK によって自動的に計算・付与されたビーコン属性です。値は 176be958 という短い文字列で、hanako@example.com の HMAC を切り詰めたものです。

GSI email-index はこの aws_dbe_b_email をパーティションキーとしており、クエリ時に SDK が平文の hanako@example.com からビーコン値を計算し、GSI に対して検索を行っています。結果として暗号化されたアイテムが検索でヒットし、SDK が自動的に復号したうえで平文の値が返ってきています。

Beacons を利用する際の注意点

Beacons を使用し検索可能にする場合の、主な注意点は以下です。

  • KeyStore テーブルの事前作成が必要:ビーコンキーを管理するための DynamoDB テーブルを別途作成し、ブランチキーを作成しておく必要がある
  • Hierarchical Keyring が必須:Beacons を利用する場合は Hierarchical Keyring を使用する必要がある
  • ビーコン長のトレードオフ:ビーコン長を決定する際は、テーブルの予想レコード数とセキュリティ要件を考慮する必要がある

    ※AWS のドキュメントではデータセットのサイズに基づく推奨値が提供されています
    https://docs.aws.amazon.com/ja_jp/database-encryption-sdk/latest/devguide/choosing-beacon-length.html#beacon-length-example

  • 等価一致のみ:Standard Beacon は等価一致(=)クエリのみをサポートする
  • 既存データへの適用:Beacons は書き込み時にビーコン値を計算するため、ビーコンを有効にしても既存データは検索対象にできない(再書き込みが必要)
  • DynamoDB Enhanced Client では非対応:Searchable Encryption は DynamoDbEncryptionInterceptor + low-level DynamoDB API でのみ使用可能

Compound Beacon のサンプル

複数の属性を組み合わせた検索が必要な場合は Compound Beacon を使用します。例えば status(SIGN_ONLY)と email(ENCRYPT_AND_SIGN)を組み合わせて、「ステータスが active かつ email が hanako@example.com」のような複合条件で検索したい場合に有効です。

Compound Beacon は、各パーツの値を split character で結合した1つのビーコン値を生成し、1回の GSI クエリで複合条件の等価検索を実現します。

サンプルコードはこちら

arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id はご自身の環境に置き換えてください

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 のサンプル。
 * status(SIGN_ONLY)と email(ENCRYPT_AND_SIGN)を組み合わせた Compound Beacon を使い、
 * "status = active かつ email = hanako@example.com" のような複合条件検索を行う。
 *
 * テーブル: Users(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. KeyStore の作成
        // =======================================================
        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. ブランチキーの作成
        // =======================================================
        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. 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. Standard Beacon の定義(Compound Beacon の encrypted part に必要)
        // =======================================================
        List<StandardBeacon> standardBeacons = new ArrayList<>();
        standardBeacons.add(StandardBeacon.builder()
                .name("email")
                .length(30)
                .build());

        // =======================================================
        // 5. Compound Beacon の定義
        //    status(SIGN_ONLY)と email(ENCRYPT_AND_SIGN)を組み合わせ
        // =======================================================
        List<CompoundBeacon> compoundBeacons = new ArrayList<>();
        compoundBeacons.add(CompoundBeacon.builder()
                .name("status_email")
                .split("#")  // パーツ間の区切り文字(パーツの値に含まれない文字を選ぶ)
                .encrypted(List.of(
                        EncryptedPart.builder()
                                .name("email")
                                .prefix("E-")
                                .build()))
                .signed(List.of(
                        SignedPart.builder()
                                .name("status")
                                .prefix("S-")
                                .build()))
                .build());

        // =======================================================
        // 6. 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. 検索設定(SearchConfig)
        // =======================================================
        SearchConfig searchConfig = SearchConfig.builder()
                .versions(beaconVersions)
                .writeVersion(1)
                .build();

        // =======================================================
        // 8. 属性ごとの暗号化アクション定義
        // =======================================================
        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. テーブル暗号化設定(Beacons 有効)
        // =======================================================
        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. DynamoDbEncryptionInterceptor の作成
        // =======================================================
        final DynamoDbEncryptionInterceptor interceptor =
                DynamoDbEncryptionInterceptor.builder()
                        .config(DynamoDbTablesEncryptionConfig.builder()
                                .tableEncryptionConfigs(tableConfigs)
                                .build())
                        .build();

        // =======================================================
        // 11. 暗号化対応 DynamoDB クライアントの作成
        // =======================================================
        final DynamoDbClient ddb = DynamoDbClient.builder()
                .overrideConfiguration(
                        ClientOverrideConfiguration.builder()
                                .addExecutionInterceptor(interceptor)
                                .build())
                .build();

        // =======================================================
        // 12. アイテムの書き込み(複数アイテム)
        // =======================================================
        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 取得(Compound Beacon 属性を確認)
        // =======================================================
        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. Compound Beacon を使った検索
        //     status = "active" かつ 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));
        });
    }
}

上記のサンプルでは3つのアイテムを書き込んでいます。

アイテム status email
USER#201 active hanako@example.com
USER#202 active taro@example.com
USER#203 inactive hanako@example.com

実行結果:

実行結果
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=0xb1402471d587f92e691c085eefe67bb0081fface02a5bb537580db220413050f435457283cba2fbbd470c82b7fbb3af63065023100f069afacd4fa409d0ccbf172906e4963e485fb4ca266a9a173fab1901174bf8da212cc2a6b4ed03db1f00d2fd91217b102305400344fe72155e066499a4c6da66e0208bc066aca206b62f1096cbac82acc206f45b406c4c25ca7922e2275aa087861))
  aws_dbe_head = AttributeValue(B=SdkBytes(bytes=0x020148790b3b13d36575014eb1720dc6450039218202047b66eedcc3142ff5ca35e90006636365656573000100156177732d63727970746f2d7075626c69632d6b6579004441767836334f70554f71306f454e7075596b6c2f786c7a447837594a6a5a724267494f6a6a3044587869743775726a516b4e4e5a69347a4f4c494d2b5a2b6e5766673d3d0100116177732d6b6d732d686965726172636879002437383161656661302d356432352d343430662d626265372d313036666138316265653134008cf29f499598ab17308bd82fe5a5524bba4fa6443919b0b19bd94d5e39ec9ef82670ddb2b7392595b41947ca58a2bc605ff6a55bdfc6d54d5172461ba2f0543049e72983f971b7d0ee3da113691810631641a2456885a3fa3a2ae652a2082d5d08cf453ab3b7c9cc2a29b1e36c3ad8ee641d1ff39db7ba150dc5643400c7aa371fcaaff6119565ce37f6c9d04e2f451b1e7ffae8df4316815a2e6a9e77a67da4f5ee767efa73aae5947f453d90))
  pk = AttributeValue(S=USER#201)
  email = AttributeValue(B=SdkBytes(bytes=0x000111ff3c6324ddcebd2eae229e3f816a1279e8e321988cb95b604016ff6c1d68b346e9))
  name = AttributeValue(B=SdkBytes(bytes=0x000198974676a2574c8d24f2280c6eec5c92d3023f4bc7f2))
  aws_dbe_b_email = AttributeValue(S=3d042b97)
  age = AttributeValue(B=SdkBytes(bytes=0x0002b1dc32863d7f9c02ebe17676812321d6d268))
  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)

raw 取得の結果から、Standard Beacon(aws_dbe_b_email = 3d042b97)に加え、Compound Beacon(aws_dbe_b_status_email = S-active#E-3d042b97)が確認できます。Compound Beacon の値は S-active(status の平文に prefix S- を付けたもの)と E-3d042b97(email のビーコン値に prefix E- を付けたもの)を split character # で結合した文字列です。

クエリの結果は、3件のうち status = "active" かつ email = "hanako@example.com" に合致する USER#201 のみが返されています。SDK が検索値 S-active#E-hanako@example.com を自動的に S-active#E-3d042b97 に変換し、GSI に対して1回のクエリで複合条件の検索を実現しています。

なお、split character にはパーツの平文値に含まれない文字を選ぶ必要があります。
今回は # を使用していますが、平文中に該当の文字列が含まれる可能性がある場合には別の文字を検討する必要があります。

特に注意が必要なポイント

クライアント側暗号化を導入する際に特に注意が必要なポイントを以下にまとめます。

暗号化属性に対するフィルタ・条件式の使用

暗号化された属性に対する FilterExpression や ConditionExpression は機能しません。暗号化属性で検索が必要な場合は Beacons を検討することになります。

署名検証エラー

アイテムに属性を追加や削除、あるいは値を変更したりすると、署名が合わなくなり取得時にエラーが発生します(改ざん検知)。
署名対象の属性構成を変更する場合は注意が必要です。

例えば、マネジメントコンソールからアイテム中の署名対象の属性の削除や変更を行うと、以下のエラーが発生します。

署名対象の属性を削除した場合:

StructuredEncryptionException: Schema changed : something that was signed is now unsigned.

署名対象の属性値を変更した場合:

StructuredEncryptionException: Signature of record does not match the signature computed when the record was encrypted.

アイテムサイズの増加

暗号化により以下の理由でアイテムサイズが増加します。

  • 暗号化された値はバイナリ型で格納され、パディングにより元の値より大きくなる
  • メタデータ属性(aws_dbe_headaws_dbe_foot)が追加される
  • Beacons を使用する場合、ビーコン属性も追加される

そのため、DynamoDB のアイテムサイズ上限(400KB)に対して余裕を持った設計が必要となります。

コンソールや CLI でデータが確認できない

暗号化された属性はバイナリ値として格納されるため、マネジメントコンソールや AWS CLI で中身を確認することができません。

ビーコンの後からの追加・変更が困難

Beacons を利用する場合、検索パターン(どの属性で検索するか)を事前に決定しておくことが非常に重要です。
通常の DynamoDB でもアクセスパターンの事前整理が求められますが、Beacons ではその重要性がさらに増します。

後から新しいビーコンを追加したい場合、以下の対応が必要になります。

  • 新しいビーコン定義の追加と対応する GSI の作成
  • 既存データの全件再書き込み(既存アイテムには新しいビーコン属性が付与されておらず検索対象にならない)

また、ブランチキーのローテーションを行った場合も、ビーコン値の導出に使われる HMAC キーが変わるため、全アイテムのビーコン値を再計算(再書き込み)する必要があります。

通常の DynamoDB であれば GSI を追加するだけで既存データも自動的にインデックスされますが、Beacons ではそうはいかない点を認識しておく必要があります。

PartiQL(ExecuteStatement)は非対応

AWS Database Encryption SDK の DynamoDbEncryptionInterceptor は PartiQL(ExecuteStatement / BatchExecuteStatement)をサポートしていません。
既存のアプリケーションが PartiQL を使用して DynamoDB にアクセスしている場合、属性レベルの暗号化を導入する前に通常の DynamoDB API(PutItem / GetItem / Query 等)を利用した処理に書き換える必要があります。

SDK の歴史と移行

DynamoDB のクライアント側暗号化ライブラリは、名称変更や世代交代を経て現在の形になっています。

経緯

主な歴史は以下です。

時期 出来事
2018 年 5 月 Amazon DynamoDB Encryption Client として Java 版・Python 版がリリース
2022 年 7 月 DynamoDB Encryption Client Java 版:v1.x、Python 版:v1.x / v2.x がサポート終了フェーズに移行
2023 年 6 月 ライブラリが AWS Database Encryption SDK に改称。新しい暗号化形式と Beacons(Searchable Encryption)を導入した SDK として Java 版がリリース
現在 AWS Database Encryption SDK は Java、.NET、Rust で提供。Python 向けは旧来の DynamoDB Encryption Client v3.x が引き続き利用可能

2 つの SDK 系列

前述の通り、名称変更などに伴って現在、DynamoDB のクライアント側暗号化には 2 つの SDK 系列が共存しています。

AWS Database Encryption SDK(新世代) Amazon DynamoDB Encryption Client(レガシー)
対応言語 Java, .NET, Rust Java, Python
メタデータ属性 aws_dbe_head / aws_dbe_foot *amzn-ddb-map-desc* / *amzn-ddb-map-sig*
Searchable Encryption 対応(Beacons) 非対応
相互運用 同じ SDK 系列内で相互復号可能 同じ SDK 系列内で相互復号可能

移行について

2 つの SDK 系列間では、相互に暗号化 / 復号化できません。
そのため、レガシーの DynamoDB Encryption Client から AWS Database Encryption SDK への移行には段階的なアプローチ(読み取り時に両形式を処理→書き込みを新形式に切り替え)が推奨されています。

さいごに

DynamoDB の暗号化について、サーバ側暗号化からクライアント側の属性レベル暗号化まで整理しました。

サーバ側の暗号化は DynamoDB が透過的に行ってくれるため普段意識することは少ないですが、キーの選択肢と監査要件を押さえておくことは重要です。

一方、クライアント側の属性レベル暗号化は、AWS を含む第三者からデータを保護できる強力な手段ですが、クエリの制約やアイテムサイズの増加といったトレードオフがあります。特に暗号化した属性で検索を行いたい場合は、Beacons の導入が必要になり、設計の複雑さが増す点は事前に考慮しておくべきでしょう。

なお、AWS Database Encryption SDK for DynamoDB は現時点で Java、.NET、Rust のみの対応となっています。Python で DynamoDB のクライアント側暗号化を行いたい場合は、旧来の DynamoDB Encryption Client(v3.x)を利用することになりますが、Beacons 等の新機能は利用できない点に注意してください。

導入を検討される際は、まず「どの属性が機密で、どの属性で検索が必要か」を整理したうえで、暗号化アクションと Beacons の要否を決定することをおすすめします。

本記事が、DynamoDB の暗号化に関する理解の一助になれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?