AWS Key Management Service (KMS) does not encrypt your files, your database rows, or your API payloads directly. It encrypts keys. Understanding that one distinction explains almost every design decision in the KMS API, and why the SDK calls look the way they do.
This page maps the model: what a customer master key actually is, why KMS hands you a "data key" instead of doing the bulk encryption itself, and where each SDK call fits in a real request.
KMS's core object is the customer master key (CMK) - a key that never leaves the service and is only ever used to wrap (encrypt) and unwrap (decrypt) other, smaller keys.
Insight: KMS is a key-wrapping service, not a bulk-encryption service. Encrypt/Decrypt calls exist for small payloads, but the standard pattern for anything bigger is envelope encryption via GenerateDataKey.
Key Concepts:CMK, data key, envelope encryption, ciphertext blob, encryption context.
When to Use: any time your application needs to encrypt data at rest or in transit and you want AWS to manage key custody, rotation, and access control.
Limitations/Trade-offs: direct Encrypt is capped near 4KB for symmetric keys; envelope encryption adds one extra KMS round trip per encrypt/decrypt operation but removes the size limit entirely.
Related Topics: symmetric vs asymmetric key types, generating and using data keys, grants and key policies, automatic key rotation.
A customer master key (CMK) is the root object in KMS. When you call CreateKey, KMS generates key material and stores it inside its own hardware security modules (HSMs). That key material is never exported in plaintext, never downloadable, and never visible to you or AWS operators - it only exists as an operation KMS performs on your behalf.
Every operation that "uses" a CMK is really KMS performing cryptography on your input and handing back the output. For encryption, that means: you send plaintext (or a data key) to KMS, KMS runs the CMK against it, and you get ciphertext back. Decrypt runs the reverse.
KeySpec: SYMMETRIC_DEFAULT produces an AES-256-GCM key used entirely inside KMS - you never see its bytes. Origin: AWS_KMS means KMS generated the material itself, as opposed to material you imported.
Because a CMK never leaves KMS, calling Encrypt directly means shipping your plaintext to the KMS API and getting ciphertext back. That works fine for small values like a config secret or a database credential, but KMS caps direct Encrypt/Decrypt payloads at roughly 4KB for symmetric keys. A file, an image, or a JSON document with any real size will not fit.
The fix is envelope encryption, and it is the standard pattern for anything larger than that limit. Instead of asking KMS to encrypt your data, you ask KMS to generate a data key - a smaller, one-time symmetric key. GenerateDataKey returns two things: the data key in plaintext, and the same key encrypted ("wrapped") under your CMK as a ciphertext blob.
You use the plaintext data key to encrypt your actual payload locally, using AES-GCM in your own process - fast, with no size limit and no per-byte KMS call. Then you discard the plaintext key immediately and store only the ciphertext blob alongside your encrypted data. The blob is small (a few hundred bytes) regardless of how large the original payload was.
# --- Python (boto3) ---import boto3kms = boto3.client("kms", region_name="us-east-1")resp = kms.generate_data_key(KeyId="alias/app-secrets-key", KeySpec="AES_256")plaintext_key = resp["Plaintext"] # use locally, then discardciphertext_blob = resp["CiphertextBlob"] # store next to your encrypted data
// --- TypeScript (AWS SDK v3) ---import { KMSClient, GenerateDataKeyCommand } from "@aws-sdk/client-kms";const kms = new KMSClient({ region: "us-east-1" });const resp = await kms.send(new GenerateDataKeyCommand({ KeyId: "alias/app-secrets-key", KeySpec: "AES_256",}));const plaintextKey = resp.Plaintext; // use locally, then discardconst ciphertextBlob = resp.CiphertextBlob; // store next to your encrypted data
To decrypt later, you send only the ciphertext blob back to KMS via Decrypt. KMS unwraps it using the CMK and returns the plaintext data key again, which you then use locally to decrypt the payload. Notice that Decrypt on a symmetric key does not require you to name the key ID - the blob itself carries that information.
The encryption context is an optional set of key-value pairs you can attach to Encrypt, Decrypt, and GenerateDataKey calls. KMS treats it as additional authenticated data (AAD): it is not encrypted, but it must match exactly on decrypt or the call fails. This lets you bind a ciphertext to metadata like a tenant ID or resource ARN without adding it to the encrypted payload itself, and it also shows up in CloudTrail logs for auditing.
Envelope encryption's cost model matters at scale. Each encrypt or decrypt operation of a large object costs exactly one KMS API call (for the data key), no matter how big the object is, versus one call per chunk if you mistakenly tried to force everything through direct Encrypt. This is why every AWS service that encrypts large objects at rest - S3 SSE-KMS, EBS, RDS - implements this same envelope pattern internally.
The model also explains why KMS scales the way it does: KMS request quotas govern how many GenerateDataKey/Decrypt calls per second you can make, not how much data you encrypt, because the bulk work happens entirely in your own process, off the KMS request path.
"KMS encrypts my file when I call it." - No. KMS only ever operates on keys (or small payloads under ~4KB). Bulk data encryption happens locally with a data key KMS gave you.
"The plaintext data key should be stored for later." - No, never. Only the ciphertext blob is safe to store; the plaintext key must be discarded right after use.
"Decrypt needs the same KeyId I used to encrypt." - Not for a single-region symmetric key; the ciphertext blob identifies its own CMK. You do need to name the key when decrypting with a multi-region replica in a different region.
"Envelope encryption is slower." - It is faster for anything beyond trivial payloads, since bulk work is local AES-GCM and KMS is only called once for the key.
"Encryption context is encrypted." - It is not. It is authenticated but visible, so never put secrets in it.
A customer master key is a logical key resource whose key material lives only inside KMS's HSMs. You never see the raw bytes - you can only ask KMS to perform operations (encrypt, decrypt, sign, wrap a data key) using it.
Why can't I just call Encrypt on my whole file?
Direct Encrypt on a symmetric CMK is capped near 4KB of plaintext. Anything larger needs envelope encryption: generate a data key, encrypt the file locally, store only the small ciphertext blob.
What does GenerateDataKey actually return?
Two values: Plaintext, the raw data key to use immediately for local encryption, and CiphertextBlob, that same key encrypted under your CMK, safe to store long-term.
Do I need to keep the plaintext data key around?
No - discard it as soon as you finish encrypting. Keep only the ciphertext blob; you regenerate the plaintext key later by calling Decrypt on the blob.
How does KMS know which key to use on Decrypt?
For a single-region key, the ciphertext blob embeds the CMK identity, so you don't pass a KeyId. For multi-region replica keys in a different region, you do specify the replica's key ID.
What is encryption context for?
An optional set of authenticated (but not encrypted) key-value pairs bound to a ciphertext. It must match exactly on decrypt, which lets you tie a ciphertext to context like a tenant ID and get that context logged in CloudTrail.
Is envelope encryption only for files?
No - it's the general pattern any time you encrypt something larger than a few kilobytes, including database columns, backups, and messages. AWS services like S3 SSE-KMS use it internally for exactly this reason.
Does using a data key avoid per-object KMS calls entirely?
Not entirely - you still call KMS once per object to generate the data key and once to decrypt it later. What you avoid is calling KMS for every chunk or byte range of that object.