SJ blog
security
A

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

KMSエンベロープ暗号化 — データキー・GenerateDataKey・パフォーマンスの仕組み

AWSサービスが内部で使うエンベロープ暗号化の仕組み、GenerateDataKeyとGenerateDataKeyWithoutPlaintext、データキーキャッシュによるKMSコスト削減、SDKでの実装方法を解説。

一言結論

大量データをKMSで暗号化する際はエンベロープ暗号化が基本で、データキーをローカルで使い暗号化済みキーだけを保存することでAPIコストとレイテンシを大幅に削減できる。

エンベロープ暗号化とは

KMSで大量のデータを暗号化する場合、データをKMSキー(CMK)で直接暗号化するのは非効率だ(KMSのAPIコール制限・コスト・レイテンシ)。その代わりに「データキー」という中間キーを使う仕組みがエンベロープ暗号化だ。

通常の暗号化:
  データ → KMSキーで暗号化 → 暗号化データ
  問題: 大量データをKMSに送るのはAPIコスト・レイテンシの問題あり

エンベロープ暗号化:
  1. KMSに「データキー(DEK: Data Encryption Key)」を生成させる
  2. データキー(平文)でデータをローカルで暗号化
  3. データキー(暗号文)を暗号化データと一緒に保存
  4. 復号時: 暗号化データキーをKMSで復号 → 平文データキーで復号

GenerateDataKeyの動作

import boto3
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

kms = boto3.client('kms')

# データキーを生成(平文と暗号文の両方を取得)
response = kms.generate_data_key(
    KeyId='alias/my-key',
    KeySpec='AES_256'  # or AES_128
)

plaintext_key = response['Plaintext']      # 使ったら必ずメモリから削除
encrypted_key = response['CiphertextBlob'] # S3等に保存(安全)

# データをローカルで暗号化(KMSは使わない)
nonce = os.urandom(12)
aesgcm = AESGCM(plaintext_key)
ciphertext = aesgcm.encrypt(nonce, b"Secret data", None)

# 暗号化データキーとnonceを暗号文と一緒に保存
import pickle
encrypted_object = {
    'encrypted_key': encrypted_key,
    'nonce': nonce,
    'ciphertext': ciphertext
}

# 平文データキーをメモリから削除(重要!)
del plaintext_key
# 復号
import boto3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

kms = boto3.client('kms')

# 暗号化データキーをKMSで復号(KMS APIは1回のみ)
response = kms.decrypt(
    CiphertextBlob=encrypted_object['encrypted_key']
)
plaintext_key = response['Plaintext']

# ローカルで復号
aesgcm = AESGCM(plaintext_key)
plaintext = aesgcm.decrypt(
    encrypted_object['nonce'],
    encrypted_object['ciphertext'],
    None
)

del plaintext_key

GenerateDataKeyWithoutPlaintext

平文データキーが不要な場合(将来使うためにデータキーだけ生成しておく)に使う。

# 暗号化データキーのみ生成(平文は返さない)
response = kms.generate_data_key_without_plaintext(
    KeyId='alias/my-key',
    KeySpec='AES_256'
)

encrypted_key = response['CiphertextBlob']
# → 後でkms.decryptで平文データキーを取り出す

データキーキャッシュ

同じデータキーを短時間・複数回使い回すことでKMS APIコールを減らせる。

from aws_encryption_sdk import EncryptionSDKClient, CommitmentPolicy
from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache
from aws_encryption_sdk.materials_managers.caching import (
    CachingCryptoMaterialsManager
)
import aws_encryption_sdk

enc_client = EncryptionSDKClient(
    commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT
)

# ローカルキャッシュの設定
cache = LocalCryptoMaterialsCache(capacity=100)

# キャッシュマネージャー(同じデータキーを最大300秒・100回再利用)
caching_manager = CachingCryptoMaterialsManager(
    master_key_provider=kms_key_provider,
    cache=cache,
    max_age=300.0,    # 最大キャッシュ期間(秒)
    max_messages_encrypted=100  # 最大使用回数
)

# 暗号化(キャッシュを活用)
ciphertext, _ = enc_client.encrypt(
    source=data,
    materials_manager=caching_manager
)

S3 Bucket KeyとのアナロジーWHAT

S3 Bucket Keyは本質的にエンベロープ暗号化のバケットレベル版だ。

通常のSSE-KMS:
  各オブジェクト → KMSにGenerateDataKey → データキーで暗号化
  → オブジェクト数万個なら数万回のKMS API呼び出し

S3 Bucket Key有効時:
  バケット用データキーをKMSが生成 → S3がキャッシュ
  → 各オブジェクトはバケットキーで暗号化
  → KMS API呼び出しを大幅削減(最大99%)

KMS API コールのコスト

東京リージョン(2026年時点参考):
  $0.03 / 10,000 API コール

例: 100万オブジェクトのS3バケット(アップロード時)
  通常のSSE-KMS: 100万 ÷ 10,000 × $0.03 = $3
  S3 Bucket Key: ほぼ$0(KMS呼び出しが激減)

エンベロープ暗号化のメリット

1. パフォーマンス: KMSへのAPIコールは1回だけ、大量データはローカルで処理
2. コスト削減:    KMSのAPIコール料金を最小化
3. サイズ制限回避: KMSの直接暗号化は4KBまで、エンベロープは無制限
4. 鍵の階層化:    CMK(マスターキー)とDEK(データキー)を分離

試験頻出ポイント

シナリオ回答
KMSで1MBのファイルを直接暗号化できるか不可(4KB上限、エンベロープ暗号化が必要)
S3 Bucket Keyの目的KMS APIコール削減(最大99%)によるコスト削減
GenerateDataKeyとDecryptの関係暗号化時にGenerateDataKey、復号時にDecrypt
データキーをKMSが管理するかしない(暗号化データキーはユーザーが管理)
エンベロープ暗号化でKMSにアクセスするタイミングデータキー生成時と復号時のみ

まとめ

エンベロープ暗号化はKMSの中核的な設計パターンだ。CMKでデータを直接暗号化するのではなく、CMKでデータキーを保護し、実際のデータはデータキーでローカルに暗号化する。この設計によりパフォーマンス・コスト・サイズ制限の問題をすべて解決している。