Skip to main content

Try it Live

Run AES-GCM examples in the interactive playground

Overview

AES-GCM is an authenticated encryption algorithm combining AES (Advanced Encryption Standard) in Galois/Counter Mode, providing both confidentiality and authenticity with a single key. Ethereum context: Not on Ethereum - Used for encrypted wallet storage (e.g., UTC/JSON keystore format) and secure messaging. Not part of Ethereum protocol. Key features:
  • Authenticated encryption: Confidentiality + integrity in one operation
  • Performance: Hardware-accelerated on modern CPUs
  • Parallelizable: Can encrypt/decrypt blocks in parallel
  • Additional data: Authenticate without encrypting (AAD)
  • Standards-compliant: NIST approved, widely used
  • Key sizes: 128, 192, or 256 bits
  • Implementations: Native Zig (16KB), NO WASM (not in browser crypto standard libs)

Quick Start

import * as AesGcm from '@tevm/voltaire/AesGcm';

// 1. Generate key (256-bit recommended)
const key = await AesGcm.generateKey(256);

// 2. Generate nonce (12 bytes)
const nonce = AesGcm.generateNonce();

// 3. Encrypt data
const plaintext = new TextEncoder().encode('Secret message');
const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

// 4. Decrypt data
const decrypted = await AesGcm.decrypt(ciphertext, key, nonce);
const message = new TextDecoder().decode(decrypted);
console.log(message); // "Secret message"

// 5. With additional authenticated data (AAD)
const aad = new TextEncoder().encode('metadata');
const ciphertextWithAAD = await AesGcm.encrypt(plaintext, key, nonce, aad);
const decryptedWithAAD = await AesGcm.decrypt(ciphertextWithAAD, key, nonce, aad);

API Reference

Key Management

generateKey(bits: 128 | 256): Promise<CryptoKey>

Generates a cryptographically secure AES key. Parameters:
  • bits - Key size (128 or 256 bits)
    • 128-bit: Faster, still very secure
    • 256-bit: Maximum security, recommended for sensitive data
// AES-256 (recommended)
const key256 = await AesGcm.generateKey(256);

// AES-128 (faster)
const key128 = await AesGcm.generateKey(128);

deriveKey(password: string | Uint8Array, salt: Uint8Array, iterations: number, bits: 128 | 256): Promise<CryptoKey>

Derives key from password using PBKDF2-HMAC-SHA256. Parameters:
  • password - User password (string or bytes)
  • salt - Salt for key derivation (≥16 bytes recommended)
  • iterations - PBKDF2 iterations (≥100,000 recommended)
  • bits - Key size (128 or 256)
// Generate salt (store with encrypted data)
import * as Hex from '@tevm/voltaire/Hex';
const salt = Hex('0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d');

// Derive key from password
const key = await AesGcm.deriveKey(
  'user-password',
  salt,
  100000,    // Iterations (adjust for security/performance)
  256        // 256-bit key
);

// Use derived key for encryption
const nonce = AesGcm.generateNonce();
const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

importKey(keyData: Uint8Array): Promise<CryptoKey>

Imports raw key bytes as CryptoKey.
import * as Hex from '@tevm/voltaire/Hex';

// Import 32-byte key (256-bit)
const rawKey = Hex('0xf8e9a72d4c3b1a6e7d5f2c8b9e1a4d7f3c6b9e2a5d8f1c4b7e0a3d6f9c2b5e8a');

const key = await AesGcm.importKey(rawKey);

exportKey(key: CryptoKey): Promise<Uint8Array>

Exports CryptoKey to raw bytes.
const key = await AesGcm.generateKey(256);
const rawBytes = await AesGcm.exportKey(key);

console.log(rawBytes); // Uint8Array(32)
// Store securely (encrypted, not plaintext!)

Encryption/Decryption

encrypt(plaintext: Uint8Array, key: CryptoKey, nonce: Uint8Array, additionalData?: Uint8Array): Promise<Uint8Array>

Encrypts data with AES-GCM, returns ciphertext with authentication tag appended. Parameters:
  • plaintext - Data to encrypt
  • key - AES key (from generateKey or deriveKey)
  • nonce - 12-byte nonce/IV (must be unique per encryption)
  • additionalData - Optional AAD (authenticated but not encrypted)
const plaintext = new TextEncoder().encode('Secret data');
const key = await AesGcm.generateKey(256);
const nonce = AesGcm.generateNonce();

// Basic encryption
const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

// With additional authenticated data (AAD)
const metadata = new TextEncoder().encode('version:1.0');
const ciphertextWithAAD = await AesGcm.encrypt(plaintext, key, nonce, metadata);
Output format:
[encrypted_data][16-byte_authentication_tag]

decrypt(ciphertext: Uint8Array, key: CryptoKey, nonce: Uint8Array, additionalData?: Uint8Array): Promise<Uint8Array>

Decrypts AES-GCM ciphertext, verifies authentication tag. Parameters:
  • ciphertext - Encrypted data with tag
  • key - Same key used for encryption
  • nonce - Same nonce used for encryption
  • additionalData - Same AAD used for encryption (if any)
try {
  const decrypted = await AesGcm.decrypt(ciphertext, key, nonce);
  const message = new TextDecoder().decode(decrypted);
  console.log(message);
} catch (error) {
  console.error('Decryption failed:', error);
  // Could be: wrong key, tampered data, or corrupted ciphertext
}

// With AAD (must match encryption)
const decryptedWithAAD = await AesGcm.decrypt(
  ciphertextWithAAD,
  key,
  nonce,
  metadata
);
Throws:
  • InvalidNonceError - Nonce not 12 bytes
  • DecryptionError - Authentication tag verification fails (data tampered), wrong key/nonce/AAD used, or corrupted ciphertext

Nonce Generation

generateNonce(): Uint8Array

Generates cryptographically secure 12-byte nonce.
const nonce = AesGcm.generateNonce();
console.log(nonce); // Uint8Array(12)

// Store nonce with ciphertext (not secret, but must be unique)

Constants

AesGcm.AES128_KEY_SIZE  // 16 bytes (128 bits)
AesGcm.AES256_KEY_SIZE  // 32 bytes (256 bits)
AesGcm.NONCE_SIZE       // 12 bytes (96 bits)
AesGcm.TAG_SIZE         // 16 bytes (128 bits)

Nonce Management

Critical: Never reuse a nonce with the same key!

Safe Nonce Usage

// Generate new nonce for each encryption
const key = await AesGcm.generateKey(256);

const msg1 = new TextEncoder().encode('First message');
const nonce1 = AesGcm.generateNonce();
const ct1 = await AesGcm.encrypt(msg1, key, nonce1);

const msg2 = new TextEncoder().encode('Second message');
const nonce2 = AesGcm.generateNonce(); // New nonce!
const ct2 = await AesGcm.encrypt(msg2, key, nonce2);

Storage Format

Store nonce with ciphertext (nonce is not secret):
// Encrypt
const key = await AesGcm.generateKey(256);
const nonce = AesGcm.generateNonce();
const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

// Store together (common format: nonce + ciphertext)
const stored = new Uint8Array(nonce.length + ciphertext.length);
stored.set(nonce, 0);
stored.set(ciphertext, nonce.length);

// Later: Extract and decrypt
const extractedNonce = stored.slice(0, AesGcm.NONCE_SIZE);
const extractedCiphertext = stored.slice(AesGcm.NONCE_SIZE);
const decrypted = await AesGcm.decrypt(extractedCiphertext, key, extractedNonce);

Nonce Collision Risk

With random nonces (12 bytes), collision probability:
  • After 2³² encryptions: ~0.005% chance
  • After 2⁴⁸ encryptions: 50% chance (birthday paradox)
Recommendations:
  • Random nonces: Safe for up to ~2³² encryptions per key
  • Counter-based: Increment counter for each encryption (no collisions)
  • Key rotation: Generate new key periodically to reset nonce space
// Counter-based nonce (for high-volume scenarios)
import * as Hex from '@tevm/voltaire/Hex';

class NonceCounter {
  constructor() {
    this.counter = 0n;
  }

  next() {
    const counterHex = this.counter.toString(16).padStart(24, '0');
    this.counter++;
    return Hex('0x' + counterHex);
  }
}

const counter = new NonceCounter();
const nonce1 = counter.next();
const nonce2 = counter.next(); // Guaranteed unique

Additional Authenticated Data (AAD)

AAD is authenticated but not encrypted - useful for metadata:
// Encrypt message with metadata
const message = new TextEncoder().encode('Transfer $100 to Alice');
const metadata = new TextEncoder().encode(JSON.stringify({
  timestamp: Date.now(),
  version: '1.0',
  sender: 'Bob'
}));

const key = await AesGcm.generateKey(256);
const nonce = AesGcm.generateNonce();

const ciphertext = await AesGcm.encrypt(message, key, nonce, metadata);

// Metadata is authenticated (tampering will fail decryption)
// But metadata itself is not encrypted (can be read)
Use cases:
  • Protocol version numbers
  • Timestamps
  • User IDs
  • Packet headers
  • Database row IDs
Security:
  • AAD is authenticated (tampering detected)
  • AAD is NOT encrypted (readable by anyone)
  • Must provide same AAD for decryption

Password-Based Encryption

Derive key from user password using PBKDF2:
import * as AesGcm from '@tevm/voltaire/AesGcm';

async function encryptWithPassword(plaintext, password) {
  // 1. Generate random salt
  import * as Hex from '@tevm/voltaire/Hex';
  const salt = Hex('0x3e4d5c6b7a8910293847566574839201');

  // 2. Derive key from password
  const key = await AesGcm.deriveKey(password, salt, 100000, 256);

  // 3. Encrypt
  const nonce = AesGcm.generateNonce();
  const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

  // 4. Return salt + nonce + ciphertext
  return {
    salt,
    nonce,
    ciphertext
  };
}

async function decryptWithPassword(encrypted, password) {
  // 1. Derive same key from password + salt
  const key = await AesGcm.deriveKey(
    password,
    encrypted.salt,
    100000,
    256
  );

  // 2. Decrypt
  return await AesGcm.decrypt(encrypted.ciphertext, key, encrypted.nonce);
}

// Usage
const data = new TextEncoder().encode('Secret data');
const encrypted = await encryptWithPassword(data, 'user-password');

// Store: encrypted.salt, encrypted.nonce, encrypted.ciphertext
// Later:
const decrypted = await decryptWithPassword(encrypted, 'user-password');

Key Storage

Secure Storage Patterns

1. Environment variables (server-side)
// Store base64-encoded key
const key = await AesGcm.generateKey(256);
const keyBytes = await AesGcm.exportKey(key);
const keyBase64 = btoa(String.fromCharCode(...keyBytes));

// Store in .env (keep out of version control!)
// ENCRYPTION_KEY=rK7J...

// Load and use
const storedKeyBytes = Uint8Array(
  atob(process.env.ENCRYPTION_KEY),
  c => c.charCodeAt(0)
);
const storedKey = await AesGcm.importKey(storedKeyBytes);
2. Browser (encrypted with password)
import * as Hex from '@tevm/voltaire/Hex';

// Encrypt master key with user password
async function storeEncryptedKey(masterKey, userPassword) {
  const salt = Hex('0x7f8e9d0c1b2a3948576e8d9c0b1a2938');
  const passwordKey = await AesGcm.deriveKey(userPassword, salt, 100000, 256);

  const masterKeyBytes = await AesGcm.exportKey(masterKey);
  const nonce = AesGcm.generateNonce();
  const encryptedKey = await AesGcm.encrypt(masterKeyBytes, passwordKey, nonce);

  // Store in localStorage
  localStorage.setItem('encryptedKey', JSON.stringify({
    salt: Array(salt),
    nonce: Array(nonce),
    ciphertext: Array(encryptedKey)
  }));
}

async function loadEncryptedKey(userPassword) {
  const stored = JSON.parse(localStorage.getItem('encryptedKey'));
  const salt = new Uint8Array(stored.salt);
  const nonce = new Uint8Array(stored.nonce);
  const ciphertext = new Uint8Array(stored.ciphertext);

  const passwordKey = await AesGcm.deriveKey(userPassword, salt, 100000, 256);
  const masterKeyBytes = await AesGcm.decrypt(ciphertext, passwordKey, nonce);

  return await AesGcm.importKey(masterKeyBytes);
}
3. Hardware Security Modules (HSM)
// Keys never leave secure hardware
// Use HSM APIs to encrypt/decrypt without exposing key

Security

Critical Warnings

1. Never reuse nonce with same key
// DANGEROUS - Same nonce with same key
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!

// Attacker can XOR ciphertexts to reveal plaintext relationship
2. Use cryptographically secure random
// CORRECT - Uses crypto.getRandomValues()
const nonce = AesGcm.generateNonce();

// WRONG - Never use Math.random() for cryptographic values
// Math.random() is predictable and NOT cryptographically secure!
3. Verify authentication tag (automatic)
// decrypt() verifies tag automatically
try {
  const plaintext = await AesGcm.decrypt(ciphertext, key, nonce);
  // If we reach here, authentication passed
} catch (error) {
  // Authentication failed - data was tampered or wrong key
  console.error('Tampering detected!');
}
4. Protect keys at rest
// WRONG - Store raw key
localStorage.setItem('key', JSON.stringify(keyBytes));

// RIGHT - Encrypt key with password or use secure storage
const encryptedKey = await encryptWithPassword(keyBytes, userPassword);
localStorage.setItem('key', JSON.stringify(encryptedKey));
5. Use strong passwords for derivation
// Weak password = weak encryption
const weakKey = await AesGcm.deriveKey('12345', salt, 100000, 256);

// Strong password = strong encryption
const strongKey = await AesGcm.deriveKey(
  'correct-horse-battery-staple-2024',
  salt,
  100000,
  256
);

Best Practices

1. Key size: Use 256-bit keys for sensitive data
const key = await AesGcm.generateKey(256); // Recommended
2. PBKDF2 iterations: Balance security vs performance
// Minimum: 100,000 iterations
// Recommended: 600,000+ iterations (OWASP 2023)
const key = await AesGcm.deriveKey(password, salt, 600000, 256);
3. Salt randomness: Use 16+ byte random salt
import * as Hex from '@tevm/voltaire/Hex';

const salt = Hex('0xa7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2');
4. Key rotation: Periodically generate new keys
// Rotate keys every N encryptions or time period
if (encryptionCount > 1000000 || Date.now() - keyCreatedTime > 30 * 86400000) {
  key = await AesGcm.generateKey(256);
  encryptionCount = 0;
  keyCreatedTime = Date.now();
}
5. Clear sensitive memory (when possible)
// After use, zero out key bytes
const keyBytes = await AesGcm.exportKey(key);
// Use key...
keyBytes.fill(0); // Clear memory

Common Attacks

Nonce Reuse Attack:
  • Same nonce + key reveals XOR of plaintexts
  • Protection: Always generate new nonce
Key Exhaustion:
  • Too many encryptions with same key increases collision risk
  • Protection: Rotate keys periodically
Weak Password:
  • Brute-force PBKDF2-derived keys
  • Protection: Strong passwords + high iteration count
Timing Attacks:
  • Constant-time operations in WebCrypto API
  • Protection: Use native crypto.subtle (not hand-rolled crypto)
Padding Oracle:
  • Not applicable to GCM (no padding)
  • GCM uses stream cipher mode

Performance

Benchmarks (typical)

Encryption speed (AES-256-GCM):
  • Modern CPU with AES-NI: 1-5 GB/s
  • Without hardware acceleration: 50-200 MB/s
Key derivation (PBKDF2):
  • 100,000 iterations: ~50-100ms
  • 600,000 iterations: ~300-600ms

Optimization Tips

1. Batch operations when possible
// Encrypt multiple messages
const messages = [...];
const encrypted = await Promise.all(
  messages.map(async msg => {
    const nonce = AesGcm.generateNonce();
    const ct = await AesGcm.encrypt(msg, key, nonce);
    return { nonce, ciphertext: ct };
  })
);
2. Reuse keys (but rotate periodically)
// Generate key once
const key = await AesGcm.generateKey(256);

// Reuse for multiple encryptions (with different nonces!)
for (const message of messages) {
  const nonce = AesGcm.generateNonce();
  await AesGcm.encrypt(message, key, nonce);
}
3. Adjust PBKDF2 iterations for use case
// High security (cold storage)
const key = await AesGcm.deriveKey(password, salt, 1000000, 256);

// Moderate security (frequent decryption)
const key = await AesGcm.deriveKey(password, salt, 100000, 256);

Use Cases

File Encryption

import * as Hex from '@tevm/voltaire/Hex';

async function encryptFile(fileData, password) {
  const salt = Hex('0x9e8d7c6b5a4938271605948372615049');
  const key = await AesGcm.deriveKey(password, salt, 600000, 256);
  const nonce = AesGcm.generateNonce();

  const ciphertext = await AesGcm.encrypt(fileData, key, nonce);

  // Format: salt (16) + nonce (12) + ciphertext + tag (16)
  const encrypted = new Uint8Array(salt.length + nonce.length + ciphertext.length);
  encrypted.set(salt, 0);
  encrypted.set(nonce, 16);
  encrypted.set(ciphertext, 28);

  return encrypted;
}

async function decryptFile(encryptedFile, password) {
  const salt = encryptedFile.slice(0, 16);
  const nonce = encryptedFile.slice(16, 28);
  const ciphertext = encryptedFile.slice(28);

  const key = await AesGcm.deriveKey(password, salt, 600000, 256);
  return await AesGcm.decrypt(ciphertext, key, nonce);
}

Database Field Encryption

class EncryptedDatabase {
  constructor(key) {
    this.key = key;
  }

  async encryptField(value) {
    const plaintext = new TextEncoder().encode(JSON.stringify(value));
    const nonce = AesGcm.generateNonce();
    const ciphertext = await AesGcm.encrypt(plaintext, this.key, nonce);

    return {
      nonce: Array(nonce),
      ciphertext: Array(ciphertext)
    };
  }

  async decryptField(encrypted) {
    const nonce = new Uint8Array(encrypted.nonce);
    const ciphertext = new Uint8Array(encrypted.ciphertext);

    const plaintext = await AesGcm.decrypt(ciphertext, this.key, nonce);
    return JSON.parse(new TextDecoder().decode(plaintext));
  }
}

Secure Messaging

async function sendEncryptedMessage(message, recipientPublicKey, senderPrivateKey) {
  // 1. Derive shared secret (ECDH)
  const sharedSecret = await deriveSharedSecret(senderPrivateKey, recipientPublicKey);

  // 2. Use shared secret as key
  const key = await AesGcm.importKey(sharedSecret);

  // 3. Encrypt message
  const plaintext = new TextEncoder().encode(message);
  const nonce = AesGcm.generateNonce();
  const ciphertext = await AesGcm.encrypt(plaintext, key, nonce);

  return { nonce, ciphertext };
}

Error Handling

All AesGcm functions throw typed errors that extend CryptoError:
ErrorCodeWhen
InvalidKeyErrorINVALID_KEYKey not 16 or 32 bytes on import
InvalidNonceErrorINVALID_NONCENonce not 12 bytes
DecryptionErrorDECRYPTION_FAILEDAuth tag verification fails, wrong key/nonce/AAD, or ciphertext too short
AesGcmErrorAES_GCM_ERRORGeneric encryption failure
import * as AesGcm from '@tevm/voltaire/AesGcm';
import { DecryptionError, InvalidNonceError, InvalidKeyError } from '@tevm/voltaire/AesGcm';

try {
  const decrypted = await AesGcm.decrypt(ciphertext, key, nonce);
} catch (e) {
  if (e instanceof DecryptionError) {
    console.error('Authentication failed:', e.message);
    console.error('Code:', e.code); // "DECRYPTION_FAILED"
  } else if (e instanceof InvalidNonceError) {
    console.error('Invalid nonce:', e.message);
  }
}
All error classes have:
  • name - Error class name (e.g., "DecryptionError")
  • code - Machine-readable error code
  • message - Human-readable description
  • docsPath - Link to relevant documentation

Implementation Notes

  • Uses native WebCrypto API (crypto.subtle)
  • Hardware-accelerated on modern CPUs (AES-NI)
  • Constant-time operations (timing attack resistant)
  • NIST SP 800-38D compliant
  • 128-bit authentication tag (maximum security)
  • 96-bit nonce (12 bytes, standard for GCM)

References