Cryptography, AES-GCM, and AWS KMS
Part 2: AES-GCM and AWS KMS
Continuing off the series from Part 1: Cryptography and Block Cipher Modes, this part will cover AES and AES in GCM mode at a high level, then show code examples of AES-GCM in Node.js and envelop encryption with AWS KMS which uses AES-GCM.
Advanced Encryption Standard (AES)
AES is a symmetric block cipher that operates on 128-bit (16‑byte) blocks and is one of the most widely used encryption algorithm in the world. It powers HTTPS, Wi‑Fi (WPA2/WPA3), VPNs, SSH, disk encryption tools like FileVault and BitLocker, secure messaging apps, password managers, cloud storage, and countless other systems that rely on confidentiality and data protection.
As a symmetric cipher, AES uses a single secret key for both encryption and decryption. It supports three key sizes:
- AES‑128 (128‑bit key)
- AES‑192 (192‑bit key)
- AES‑256 (256‑bit key)

Larger keys increase brute‑force resistance, but all variants remain secure in practice.
Why is AES so widely used?
AES became the successor to DES (which dominated the space over 20 years) and it has remained the standard for several reasons:
- Large block size (128 bits) eliminates structural weaknesses seen in older ciphers such as DES (64 bits).
- Strong resistance to known cryptanalytic attacks, including differential and linear cryptanalysis.
- A clean mathematical structure consisting of SubBytes, ShiftRows, MixColumns and AddRoundKey makes the algorithm efficient to implement and analyse unlike older designs such as DES which relied on irregular bit-level permutations and hardware-specific quirks.
- Hardware acceleration on processors like Intel, AMD, and ARMv8 makes AES extremely fast on modern CPUs.
- Works with many modern modes of operation, including CTR and GCM, which bring additional security guarantees.
AES‑GCM
AES‑GCM is one of the most widely used authenticated encryption modes today, examples including TLS (for HTTPS), AWS envelope encryption, WebRTC, OpenVPN, DynamoDB client-side encryption, and as an encryption option for storage engines such as WiredTiger (used in MongoDB). As you might recall from part 1, GCM is the mode of operation that combines two things:
- Confidentiality by encrypting data with the block cipher (AES in this case) in CTR mode.
- Integrity and Authenticity by producing an authentication tag using the GHASH function.
This makes AES‑GCM what is called an AEAD (Authenticated Encryption with Associated Data) mode.
How AES‑GCM Works
- A 128-bit block consisting of the nonce and counter bits are encrypted with the AES block cipher to generate a keystream. The ratio of bits between the nonce and the counter is arbitrary, but it’s typically 96 bits (12 bytes) for the nonce and 32 bits (4 bytes) for the counter. AES only requires that the combined size is 128 bits. However, GCM recommends a 96-bit nonce because it simplifies the GHASH computation and avoids extra processing steps.
# Typical composition of a block for constructing a keystream
block = [ nonce (96 bits) || counter (32 bits) ]
# Example
block_1 = [ nonce || 1 ]
block_2 = [ nonce || 2 ]
block_3 = [ nonce || 3 ]
...
# Encrypt each block with AES and concatenate to construct the keystream
keystream = AES(k, block_1) || AES(k, block_2) || AES(k, block_3) || ...
- The plaintext is XORed with this keystream to produce the ciphertext.
# Construct ciphertext from plaintext using the keystream
ciphertext = plaintext XOR keystream
- Simultaneously, GHASH processes the ciphertext and optional associated data (AAD) to compute a 128‑bit authentication tag.
# GHASH takes the hash subkey H and processes AAD, ciphertext,
# and their lengths to produce a 128-bit authentication value.
auth = GHASH( H, AAD || ciphertext || len(AAD) || len(ciphertext) )
- The output is the ciphertext, nonce, and authentication tag. The nonce is needed to recreate the keystream; ciphertext can be XORed with the keystream to extract the original plaintext; and the authentication tag is used to detect if the data was tampered with.
# Recreate the keystream
block_1 = [ nonce || 1 ]
block_2 = [ nonce || 2 ]
block_3 = [ nonce || 3 ]
...
keystream = AES(k, block_1) || AES(k, block_2) || AES(k, block_3) || ...
# Extract plaintext
plaintext = ciphertext XOR keystream
Critical Rule: Never Reuse a Nonce
Nonce reuse under the same key leads to catastrophic failures like Two-Time Pad, which can lead to plaintext recovery:
# Suppose that a nonce is reused and keystream `K`
# is constructed and used to encrypt two plaintexts:
C1 = P1 XOR K
C2 = P2 XOR K
# then the attacker can compute:
C1 XOR C2 = (P1 XOR K) XOR (P2 XOR K)
# and since K XOR K = 0, then:
C1 XOR C2 = P1 XOR P2
This is considered a fatal leak. Even though only the XOR of the plaintexts are known to the attackers, it can be cryptanalysed by looking at its structure. For example, English, JSON, HTML, and various data formats has predictable byte patterns that can be easily picked up and revealed.
Suppose that the attacker guessed the first 8 bytes of P1:
P1[0..8] = "HTTP/1.1 "
Then, the corresponding 8 bytes of P2 can be computed:
P2[0..8] = (P1 XOR P2)[0..8] XOR P1[0..8]
Once you begin to reveal parts of one plaintext, the corresponding bytes of the other plaintext are revealed automatically, and it grows like a zipper where every known bit of P1 reveals P2 and every known bit of P2 reveals P1.
Key take away: Do Not Reuse a Nonce.
AES-GCM with crypto Module in Node.js
Since we have theoretical knowledge of AES-GCM and its components like the nonce and AAD, the code examples should make sense. This section shows how to encrypt and decrypt data securely with AES‑GCM using Node’s built‑in crypto module.
Encrypting
import crypto from "crypto";
export function encrypt(key, plaintext, aad) {
// Create a nonce
const nonce = crypto.randomBytes(12); // 96 bits; recommended GCM nonce size
// Create the AES-256 GCM cipher with the key and nonce
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
// Set AAD metadata
if (aad) cipher.setAAD(Buffer.from(aad));
// Combine both outputs into the full ciphertext
const ciphertext = Buffer.concat([
// Encrypt available plaintext data and returns partial ciphertext
cipher.update(plaintext, "utf8"),
// Finish encryption, returns remaining ciphertext
cipher.final(),
]);
// Compute the auth tag to enable integrity check
const tag = cipher.getAuthTag();
// Recall that the output of AES-GCM is the
// ciphertext, nonce, and auth tag.
return { nonce, ciphertext, tag };
}
Decrypting
export function decrypt(key, { nonce, ciphertext, tag }, aad) {
// Create the decipher with the same key and nonce
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
// Enter the same metadata used during encryption
if (aad) decipher.setAAD(Buffer.from(aad));
// Provide the auth tag to check integrity at `decipher.final()`
decipher.setAuthTag(tag);
// Combine both outputs into the full plaintext
const plaintext = Buffer.concat([
// Decrypt available ciphertext and output plaintext chunk
decipher.update(ciphertext),
// Verify authenticity (GCM), finish decryption
decipher.final(),
]);
return plaintext.toString("utf8");
}
Usage
// Generate 256-bit key
const key = crypto.randomBytes(32);
// Encrypting some text and attaching metadata (aad)
const encrypted = encrypt(
key,
"Super secret message from John Doe",
"user=john.doe",
);
// Decrypting ciphertext and verifying metadata (aad)
const decrypted = decrypt(key, encrypted, "user=john.doe");
console.log(decrypted); // "Super secret message from John Doe"
AWS KMS Envelope Encryption using AES‑GCM
When you encrypt your data with a symmetric cipher like AES, you need to ensure that the encryption key is kept securely. Otherwise, encrypting your data is pointless. Services like AWS KMS solves this problem. It’s designed to securely manage the master keys to encrypt your data keys. Simply put, AWS KMS manages the keys that encrypt your keys. It does not encrypt large data directly. This is the essence of envelop encryption:
- When an application wants to encrypt some data, it requests KMS for a DEK (data encryption key).
- KMS generates a DEK, encrypts it using a master key that it manages, and responds with both the plaintext and encrypted DEK.
- The app then uses the plaintext DEK locally with AES‑GCM to encrypt the data and then discards the plaintext DEK. Only the encrypted DEK is stored alongside the ciphertext.
- When the app wants to decrypt the data, it sends the encrypted DEK back to KMS requesting it to be decrypted.
- AWS KMS decrypts it using its managed master key and sends back the decrypted key. The app then uses this key to decrypt the ciphertext.
Let’s look at the code examples.
1. Request for a DEK
import { KMSClient, GenerateDataKeyCommand } from "@aws-sdk/client-kms";
const kms = new KMSClient();
const { Plaintext: dek, CiphertextBlob: encryptedDEK } = await kms.send(
new GenerateDataKeyCommand({
KeyId: process.env.KMS_KEY_ID,
KeySpec: "AES_256",
}),
);
2. Encrypt Data with AES‑GCM
// Using the same `encrypt` function we defined in the
// earlier example but with the DEK from AWS KMS this time
const { nonce, ciphertext, tag } = encrypt(dek, "hello world");
3. Store the Encrypted Form
# In JSON format, binary format, etc. entirely up to you.
{
"ciphertext": "…",
"tag": "…",
"nonce": "…",
"encryptedDEK": "…"
}
4. Request for Decryption of Key
import { DecryptCommand } from "@aws-sdk/client-kms";
const { Plaintext: dek } = await kms.send(
new DecryptCommand({ CiphertextBlob: encryptedDEK }),
);
5. Use the Decrypted Key to Decrypt Ciphertext
const plaintext = decrypt(dek, { nonce, ciphertext, tag });
This pattern is used by AWS services such as S3, DynamoDB (client‑side encryption), and Secrets Manager. It allows you to securely encrypt large objects without exposing long‑term encryption keys.
Summary
This part of the series introduced AES and explained why it has become the modern standard for symmetric encryption. We covered how AES-GCM combines CTR-style encryption with GHASH to provide confidentiality, integrity, and authenticity in a single, efficient mode. We looked at how the keystream is constructed, why nonce reuse is dangerous, and how AES-GCM is implemented using Node.js’s crypto module. Finally, we explored how AWS KMS uses envelope encryption: instead of encrypting data directly, it securely generates and protects the keys that your application uses with AES-GCM. This pattern allows you to encrypt any amount of data safely while keeping long-term key material managed by AWS.
References
- Schneier, Bruce. Applied Cryptography: Protocols, Algorithms, and Source Code in C (20th Anniversary Edition). Wiley, 2015.
- Computerphile. “AES Explained (Advanced Encryption Standard)”. https://www.youtube.com/watch?v=O4xNJsjtN6E&utm_source=chatgpt.com (accessed 14 Nov 2025)
- Wikipedia. “Advanced Encryption Standard.” Wikipedia, The Free Encyclopedia, https://en.wikipedia.org/wiki/Advanced_Encryption_Standard (accessed 15 Nov 2025).
- AWS SDK for JavaScript v3: “@aws-sdk/client-kms”. npm package. https://www.npmjs.com/package/@aws-sdk/client-kms (accessed 16 Nov 2025).
- Amazon Web Services. “AWS Key Management Service (KMS) — Developer Guide: Overview.” AWS Documentation. https://docs.aws.amazon.com/kms/latest/developerguide/overview.html (accessed 16 Nov 2025).