Skip to main content

Try it Live

Run AES-GCM examples in the interactive playground

Overview

AES-GCM is a NIST-approved authenticated encryption mode providing both confidentiality and integrity. When used correctly, it offers strong security guarantees. However, nonce reuse is catastrophic and several other pitfalls exist.

Security Properties

Confidentiality

Semantic Security (IND-CPA):
  • Ciphertext reveals no information about plaintext
  • Identical plaintexts produce different ciphertexts (with different nonces)
  • Requires unique nonces for each encryption
Key Strength:
  • AES-128: ~2¹²⁸ operations to break (~340 undecillion)
  • AES-256: ~2²⁵⁶ operations to break
  • Post-quantum: AES-128 reduced to ~2⁶⁴, AES-256 to ~2¹²⁸ (Grover’s algorithm)
Recommendation: Use AES-256 for long-term security and post-quantum resistance.

Integrity and Authentication

Unforgeable (INT-CTXT):
  • Cannot create valid ciphertext without the key
  • Authentication tag is 128 bits (2¹²⁸ possible tags)
  • Brute-force forgery: ~2¹²⁸ attempts
Tag Properties:
  • Computed over ciphertext AND additional authenticated data
  • Verified in constant time (timing-attack resistant)
  • Any modification (ciphertext, tag, or AAD) causes decryption failure

Authenticated Encryption with Associated Data (AEAD)

AES-GCM provides all three security properties simultaneously:
  1. Confidentiality: Plaintext secrecy
  2. Integrity: Tampering detection
  3. Authenticity: Proof of origin (with correct key)

Critical Security Requirements

1. NEVER Reuse Nonces

CATASTROPHIC SECURITY FAILURE Nonce reuse with the same key completely breaks security:
// DANGEROUS - Nonce reuse
const key = await AesGcm.generateKey(256);
const nonce = AesGcm.generateNonce();

const ct1 = await AesGcm.encrypt(msg1, key, nonce); // OK
const ct2 = await AesGcm.encrypt(msg2, key, nonce); // BREAKS SECURITY!
What an attacker can do with reused nonces:
  1. Recover XOR of plaintexts:
    ct1 XOR ct2 = (msg1 XOR keystream) XOR (msg2 XOR keystream)
                 = msg1 XOR msg2
    
  2. Recover authentication key (H):
    • With two ciphertexts using same nonce
    • Can forge arbitrary ciphertexts
    • Complete authentication bypass
  3. Recover plaintext:
    • If one plaintext is known or guessable
    • XOR attack reveals other plaintext
Example Attack:
// Attacker intercepts two ciphertexts with same nonce
const ct1 = await AesGcm.encrypt(
  new TextEncoder().encode('Transfer $100 to Alice'),
  key,
  nonce
);

const ct2 = await AesGcm.encrypt(
  new TextEncoder().encode('Transfer $999 to Alice'),
  key,
  nonce // Same nonce!
);

// Attacker can XOR ciphertexts to learn plaintext differences
// And forge new valid ciphertexts!
Prevention:
// CORRECT: New nonce for each encryption
const key = await AesGcm.generateKey(256);

const nonce1 = AesGcm.generateNonce();
const ct1 = await AesGcm.encrypt(msg1, key, nonce1);

const nonce2 = AesGcm.generateNonce(); // Different nonce!
const ct2 = await AesGcm.encrypt(msg2, key, nonce2);

2. Use Cryptographically Secure Random

REQUIRED: Use crypto.getRandomValues() for nonces and keys
// CORRECT
const nonce = AesGcm.generateNonce(); // Uses crypto.getRandomValues()

// WRONG - Never do this
const badNonce = new Uint8Array(12);
for (let i = 0; i < 12; i++) {
  badNonce[i] = Math.floor(Math.random() * 256); // NOT SECURE!
}
Why Math.random() is insecure:
  • Predictable pseudorandom (not cryptographic)
  • Seeded from system time (guessable)
  • Attacker can predict future nonces
  • Leads to nonce collisions

3. Protect Keys at Rest

Never store keys in plaintext:
// WRONG - Store raw key
localStorage.setItem('key', JSON.stringify(keyBytes));

// RIGHT - Encrypt key with password
const salt = crypto.getRandomValues(Bytes16());
const passwordKey = await AesGcm.deriveKey(password, salt, 600000, 256);
const encryptedKey = await AesGcm.encrypt(keyBytes, passwordKey, nonce);

localStorage.setItem('encryptedKey', JSON.stringify({
  salt: Array(salt),
  nonce: Array(nonce),
  ciphertext: Array(encryptedKey)
}));
Key storage best practices:
  • Server: Use HSM, key management service (KMS), or environment variables
  • Browser: Encrypt with user password before storing
  • Mobile: Use secure enclave (iOS) or keystore (Android)
  • Never commit keys to version control

4. Rotate Keys Periodically

Limit encryptions per key:
class KeyRotation {
  constructor() {
    this.key = null;
    this.encryptionCount = 0;
    this.keyCreatedTime = 0;
    this.MAX_ENCRYPTIONS = 2 ** 32; // ~4 billion
    this.MAX_KEY_AGE = 30 * 86400000; // 30 days
  }

  async rotateIfNeeded() {
    const needsRotation =
      this.key === null ||
      this.encryptionCount >= this.MAX_ENCRYPTIONS ||
      Date.now() - this.keyCreatedTime >= this.MAX_KEY_AGE;

    if (needsRotation) {
      this.key = await AesGcm.generateKey(256);
      this.encryptionCount = 0;
      this.keyCreatedTime = Date.now();
      console.log('Key rotated');
    }
  }

  async encrypt(plaintext) {
    await this.rotateIfNeeded();
    this.encryptionCount++;

    const nonce = AesGcm.generateNonce();
    return await AesGcm.encrypt(plaintext, this.key, nonce);
  }
}

5. Use Strong Passwords for Key Derivation

Weak password = Weak encryption
// WEAK - Easily brute-forced
const weakKey = await AesGcm.deriveKey('12345', salt, 100000, 256);

// STRONG - High entropy
const strongKey = await AesGcm.deriveKey(
  'correct-horse-battery-staple-2024!',
  salt,
  600000, // High iteration count
  256
);
Password recommendations:
  • Minimum: 12 characters
  • Recommended: 16+ characters or passphrase
  • Include: Uppercase, lowercase, numbers, symbols
  • Avoid: Dictionary words, personal information, common patterns
PBKDF2 iterations:
  • Minimum: 100,000 (legacy)
  • Recommended: 600,000+ (OWASP 2023)
  • High security: 1,000,000+
Trade-off: Higher iterations = slower but more resistant to brute-force.

Attack Scenarios and Mitigations

Nonce Collision (Birthday Paradox)

Problem: Random nonces eventually collide Collision probability:
  • After 2³² encryptions: ~0.005% (acceptable)
  • After 2⁴⁸ encryptions: 50% (dangerous)
Mitigation:
// Option 1: Limit encryptions per key
if (encryptionCount > 2 ** 32) {
  key = await AesGcm.generateKey(256);
  encryptionCount = 0;
}

// Option 2: Use counter-based nonces
class NonceCounter {
  constructor() {
    this.counter = 0n;
  }

  next() {
    const nonce = new Uint8Array(12);
    const view = new DataView(nonce.buffer);
    view.setBigUint64(0, this.counter, false);
    this.counter++;
    return nonce;
  }
}

Key Exhaustion

Problem: Too many encryptions with same key NIST Recommendation: Max 2³² encryptions per key for random nonces Mitigation: Implement automatic key rotation

Weak Password Attacks

Problem: PBKDF2-derived keys vulnerable to dictionary attacks Attack: Offline brute-force of common passwords Mitigation:
  1. Enforce strong password policy
  2. Use high iteration count (600,000+)
  3. Consider additional key derivation (scrypt, Argon2)
  4. Use hardware-based key storage when possible

Side-Channel Attacks

Timing Attacks:
  • Risk: Tag comparison reveals information
  • Mitigation: Constant-time verification (built into WebCrypto)
Cache-Timing Attacks:
  • Risk: AES table lookups leak key information
  • Mitigation: Use AES-NI (hardware acceleration)
Power Analysis:
  • Risk: Power consumption reveals operations
  • Mitigation: Use hardware security modules (HSM)

Chosen-Ciphertext Attacks

Problem: Attacker modifies ciphertext to learn about plaintext Protection: Authentication tag prevents this
  • Any modification causes decryption failure
  • No partial plaintext revealed
  • All-or-nothing decryption

Key Compromise

Problem: Attacker obtains encryption key Impact:
  • All past ciphertexts can be decrypted
  • Future encryptions can be forged
Mitigation:
  • Use forward secrecy (ephemeral keys)
  • Rotate keys regularly
  • Limit key access with least privilege
  • Use HSM/KMS for key protection

Common Vulnerabilities

1. Storing Nonce with Ciphertext (Acceptable)

Acceptable - Nonce is not secret:
// Store nonce with ciphertext (common pattern)
const stored = new Uint8Array(nonce.length + ciphertext.length);
stored.set(nonce, 0);
stored.set(ciphertext, nonce.length);

// This is SAFE - nonce doesn't need to be secret
// Only requirement: unique per encryption

2. Reusing AAD (Safe)

Safe - AAD can be reused:
const aad = new TextEncoder().encode('version:1.0');

// OK to use same AAD for multiple encryptions
const ct1 = await AesGcm.encrypt(msg1, key, nonce1, aad);
const ct2 = await AesGcm.encrypt(msg2, key, nonce2, aad);

3. Short Authentication Tags (Avoid)

Not applicable - AES-GCM uses 128-bit tags Voltaire always uses full 128-bit tags (maximum security). Some implementations allow truncated tags (96, 104, 112 bits) - this weakens authentication.

Best Practices Summary

DO

✓ Generate new nonce for each encryption ✓ Use AesGcm.generateNonce() (cryptographically secure) ✓ Use AES-256 for sensitive data ✓ Store nonce with ciphertext (it’s not secret) ✓ Rotate keys periodically ✓ Use strong passwords (≥16 chars, high entropy) ✓ Use high PBKDF2 iterations (≥600,000) ✓ Handle decryption errors gracefully ✓ Clear sensitive data from memory when done ✓ Use hardware security modules (HSM) for keys

DON’T

✗ Never reuse nonces with the same key ✗ Never use Math.random() for nonces ✗ Never store keys in plaintext ✗ Never ignore decryption errors ✗ Never exceed 2³² encryptions per key (random nonces) ✗ Never use weak passwords for key derivation ✗ Never commit keys to version control ✗ Never assume partial decryption on error ✗ Never use predictable nonces (e.g., timestamps alone)

Security Checklist

Before deploying AES-GCM encryption:
  • Nonces are unique for each encryption
  • Using cryptographically secure random (crypto.getRandomValues())
  • Using AES-256 (not AES-128) for sensitive data
  • Keys stored encrypted or in secure storage (HSM/KMS)
  • Key rotation implemented (< 2³² encryptions per key)
  • Strong password policy enforced (≥16 chars)
  • PBKDF2 iterations ≥ 600,000
  • Decryption errors handled properly
  • No keys in version control or logs
  • Authentication failures logged for monitoring
  • Key access follows least privilege principle
  • Backup/recovery procedures for encrypted data
  • Compliance with regulations (GDPR, HIPAA, etc.)

Compliance and Standards

NIST Approved

AES-GCM is approved by NIST for:
  • FIPS 140-2/140-3 compliance
  • Government use (classified data with AES-256)
  • Commercial applications
Standards:
  • NIST SP 800-38D (GCM specification)
  • FIPS 197 (AES algorithm)
  • RFC 5116 (AEAD algorithms)

Industry Compliance

PCI DSS: AES-256 required for cardholder data HIPAA: AES-256 recommended for PHI GDPR: Strong encryption required for personal data

Cryptographic Limits

NIST SP 800-38D Limits

Maximum plaintext length: 2³⁹ - 256 bits (~68 GB) Maximum invocations: 2³² per key (random nonces) Tag length: 128 bits (full security), minimum 96 bits (reduced) Nonce length: 96 bits (recommended), 1 to 2⁶⁴ bits (supported)

Practical Limits

// Maximum per key with random nonces
const MAX_ENCRYPTIONS = 2 ** 32; // ~4.3 billion

// Maximum plaintext size
const MAX_PLAINTEXT_SIZE = (2 ** 39 - 256) / 8; // ~68 GB

// Safe operation
if (encryptionCount >= MAX_ENCRYPTIONS) {
  throw new Error('Key exhausted - rotate key');
}

if (plaintext.length > MAX_PLAINTEXT_SIZE) {
  throw new Error('Plaintext too large for single encryption');
}

References