Documentation Index Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Try it Live Run AES-GCM examples in the interactive playground
Overview
AES-GCM encryption combines the AES block cipher in Counter mode (CTR) with Galois mode authentication (GMAC) to provide authenticated encryption. This single operation ensures both confidentiality (data secrecy) and integrity (tamper detection).
Encryption Operation
Basic Encryption
import * as AesGcm from '@tevm/voltaire/AesGcm' ;
// Generate key and nonce
const key = await AesGcm . generateKey ( 256 );
const nonce = AesGcm . generateNonce ();
// Encrypt data
const plaintext = new TextEncoder (). encode ( 'Secret message' );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Output format: [encrypted_data][16-byte_authentication_tag]
console . log ( 'Ciphertext length:' , ciphertext . length ); // plaintext.length + 16
How It Works
AES-GCM encryption involves three main steps:
Counter Mode Encryption (CTR)
Generates keystream by encrypting counter blocks
XORs keystream with plaintext to produce ciphertext
Counter starts from nonce and increments
Authentication Tag Generation (GMAC)
Processes ciphertext and AAD through GHASH
Produces 128-bit authentication tag
Tag ensures data hasn’t been tampered with
Output
Ciphertext (same length as plaintext)
Authentication tag (16 bytes)
Combined output: ciphertext || tag
Parameters
Key (Required)
The AES encryption key determines cipher strength:
// AES-128 (16 bytes = 128 bits)
const key128 = await AesGcm . generateKey ( 128 );
// AES-256 (32 bytes = 256 bits) - RECOMMENDED
const key256 = await AesGcm . generateKey ( 256 );
Key strength:
AES-128: ~2¹²⁸ operations to break (quantum: ~2⁶⁴)
AES-256: ~2²⁵⁶ operations to break (quantum: ~2¹²⁸)
Recommendation: Use AES-256 for sensitive data and long-term security.
Nonce/IV (Required)
The nonce (number used once) or IV (initialization vector) must be unique for each encryption with the same key:
// Generate random nonce (12 bytes = 96 bits)
const nonce = AesGcm . generateNonce ();
console . log ( nonce . length ); // 12
CRITICAL: Nonce reuse catastrophically breaks security!
// DANGEROUS - Never do this
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 ); // SECURITY FAILURE!
// Attacker can XOR ciphertexts to reveal plaintext relationship:
// ct1 XOR ct2 = (msg1 XOR keystream) XOR (msg2 XOR keystream)
// = msg1 XOR msg2
Why 12 bytes (96 bits)?
Standard size for GCM (NIST SP 800-38D)
Efficiently processed (no padding needed)
Large enough for random generation (2⁹⁶ possible values)
Small collision probability until ~2⁴⁸ encryptions
Plaintext (Required)
Data to encrypt can be any length:
// Empty plaintext (valid)
const empty = new Uint8Array ( 0 );
const ct1 = await AesGcm . encrypt ( empty , key , nonce );
console . log ( ct1 . length ); // 16 (just the tag)
// Small plaintext
const small = new TextEncoder (). encode ( 'Hi' );
const ct2 = await AesGcm . encrypt ( small , key , nonce2 );
console . log ( ct2 . length ); // 2 + 16 = 18
// Large plaintext (10 MB)
const large = new Uint8Array ( 10 * 1024 * 1024 );
crypto . getRandomValues ( large );
const ct3 = await AesGcm . encrypt ( large , key , nonce3 );
console . log ( ct3 . length ); // 10485760 + 16
Maximum plaintext length: 2³⁹ - 256 bits (~68 GB) per NIST SP 800-38D
Additional Authenticated Data (Optional)
AAD is authenticated but not encrypted - useful for metadata:
// Encrypt payment with metadata
const payment = new TextEncoder (). encode ( 'Transfer $100 to Alice' );
const metadata = new TextEncoder (). encode ( JSON . stringify ({
timestamp: Date . now (),
transactionId: 'tx-12345' ,
version: '1.0'
}));
const ciphertext = await AesGcm . encrypt ( payment , key , nonce , metadata );
// Metadata is:
// ✓ Authenticated (tampering detected during decryption)
// ✗ Not encrypted (readable in plaintext)
// ✓ Must match during decryption
Use cases for AAD:
Protocol headers
Database row IDs
Version numbers
Timestamps
User IDs
Packet sequence numbers
The encryption output combines ciphertext and authentication tag:
┌─────────────────┬──────────────────┐
│ Ciphertext │ Auth Tag (16B) │
└─────────────────┴──────────────────┘
Same as plaintext 128 bits (fixed)
const plaintext = new TextEncoder (). encode ( 'Hello' ); // 5 bytes
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
console . log ( ciphertext . length ); // 21 bytes (5 + 16)
// Extract components (for illustration - don't do this manually)
const encryptedData = ciphertext . slice ( 0 , plaintext . length );
const authTag = ciphertext . slice ( plaintext . length );
console . log ( encryptedData . length ); // 5
console . log ( authTag . length ); // 16
Store nonce with ciphertext (nonce is not secret, but must be available for decryption):
// Encrypt
const key = await AesGcm . generateKey ( 256 );
const nonce = AesGcm . generateNonce ();
const plaintext = new TextEncoder (). encode ( 'Secret data' );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Common storage format: nonce || ciphertext
const stored = new Uint8Array ( nonce . length + ciphertext . length );
stored . set ( nonce , 0 ); // Bytes 0-11: nonce
stored . set ( ciphertext , nonce . length ); // Bytes 12+: ciphertext + tag
// 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 );
Alternative: Store separately
// Store as JSON (less efficient, more readable)
const encrypted = {
nonce: Array ( nonce ),
ciphertext: Array ( ciphertext ),
algorithm: 'AES-256-GCM' ,
timestamp: Date . now ()
};
localStorage . setItem ( 'data' , JSON . stringify ( encrypted ));
// Later: parse and decrypt
const stored = JSON . parse ( localStorage . getItem ( 'data' ));
const decrypted = await AesGcm . decrypt (
new Uint8Array ( stored . ciphertext ),
key ,
new Uint8Array ( stored . nonce )
);
Nonce Generation Strategies
Random Nonces (Default)
Generate random nonce for each encryption:
const nonce = AesGcm . generateNonce ();
Pros:
Simple
No state to track
Works for distributed systems
Cons:
Collision probability after ~2⁴⁸ encryptions (birthday paradox)
Not suitable for high-volume scenarios
Safe for: Up to ~2³² encryptions per key (~4 billion)
Counter-Based Nonces
Increment counter for each encryption:
class NonceCounter {
constructor () {
this . counter = 0 n ;
}
next () {
const nonce = new Uint8Array ( 12 );
const view = new DataView ( nonce . buffer );
// Store counter in first 8 bytes (big-endian)
view . setBigUint64 ( 0 , this . counter , false );
// Last 4 bytes can be random or zeros
this . counter ++ ;
if ( this . counter >= ( 1 n << 64 n )) {
throw new Error ( 'Counter exhausted - rotate key' );
}
return nonce ;
}
}
const counter = new NonceCounter ();
const nonce1 = counter . next (); // 0
const nonce2 = counter . next (); // 1
const nonce3 = counter . next (); // 2
Pros:
No collisions (guaranteed unique)
Suitable for high-volume scenarios
Can encrypt up to 2⁶⁴ messages per key
Cons:
Must maintain state
Complex in distributed systems
Counter must never reset with same key
Hybrid Approach
Combine random and counter:
class HybridNonceGenerator {
constructor () {
// Generate random prefix once
this . prefix = crypto . getRandomValues ( Bytes4 ());
this . counter = 0 n ;
}
next () {
const nonce = new Uint8Array ( 12 );
// First 4 bytes: random prefix (unique per instance)
nonce . set ( this . prefix , 0 );
// Last 8 bytes: counter
const view = new DataView ( nonce . buffer , 4 );
view . setBigUint64 ( 0 , this . counter , false );
this . counter ++ ;
return nonce ;
}
}
Pros:
Works in distributed systems (different random prefixes)
No collisions within single instance
High throughput
Cons:
Requires coordination to avoid prefix collisions
Advanced Usage
Streaming Large Files
For files too large to fit in memory:
async function encryptFileStream ( fileStream , key ) {
const nonce = AesGcm . generateNonce ();
// Read file in chunks
const chunks = [];
for await ( const chunk of fileStream ) {
chunks . push ( chunk );
}
// Combine chunks
const plaintext = new Uint8Array (
chunks . reduce (( acc , chunk ) => acc + chunk . length , 0 )
);
let offset = 0 ;
for ( const chunk of chunks ) {
plaintext . set ( chunk , offset );
offset += chunk . length ;
}
// Encrypt entire file
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
return { nonce , ciphertext };
}
Note: AES-GCM requires entire plaintext for authentication. For true streaming encryption, use chunked encryption with separate tags per chunk.
Parallel Encryption
Encrypt multiple messages in parallel:
async function encryptBatch ( messages , key ) {
const encrypted = await Promise . all (
messages . map ( async ( message ) => {
const nonce = AesGcm . generateNonce ();
const plaintext = new TextEncoder (). encode ( message );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
return { nonce , ciphertext };
})
);
return encrypted ;
}
// Usage
const messages = [ 'Message 1' , 'Message 2' , 'Message 3' ];
const key = await AesGcm . generateKey ( 256 );
const encrypted = await encryptBatch ( messages , key );
Security Considerations
Critical Requirements
Unique nonces: Never reuse nonce with same key
Random nonces: Use cryptographically secure random (crypto.getRandomValues)
Key strength: Use 256-bit keys for sensitive data
Key rotation: Rotate keys before 2³² encryptions
Common Mistakes
// WRONG: Reusing nonce
const nonce = AesGcm . generateNonce ();
await AesGcm . encrypt ( msg1 , key , nonce );
await AesGcm . encrypt ( msg2 , key , nonce ); // BREAKS SECURITY!
// WRONG: Predictable nonce
const badNonce = new Uint8Array ( 12 );
for ( let i = 0 ; i < 12 ; i ++ ) {
badNonce [ i ] = i ; // Predictable!
}
// WRONG: Math.random() for nonce
const terribleNonce = new Uint8Array ( 12 );
for ( let i = 0 ; i < 12 ; i ++ ) {
terribleNonce [ i ] = Math . floor ( Math . random () * 256 ); // Not cryptographic!
}
// CORRECT: Use generateNonce()
const goodNonce = AesGcm . generateNonce ();
Hardware Acceleration
Modern CPUs with AES-NI instructions:
AES-128-GCM: ~3-5 GB/s
AES-256-GCM: ~2-4 GB/s
Without hardware acceleration:
Software-only: ~50-200 MB/s
Benchmarks
// Measure encryption speed
const key = await AesGcm . generateKey ( 256 );
const plaintext = new Uint8Array ( 1024 * 1024 ); // 1 MB
crypto . getRandomValues ( plaintext );
const iterations = 100 ;
const start = performance . now ();
for ( let i = 0 ; i < iterations ; i ++ ) {
const nonce = AesGcm . generateNonce ();
await AesGcm . encrypt ( plaintext , key , nonce );
}
const end = performance . now ();
const totalMB = ( plaintext . length * iterations ) / ( 1024 * 1024 );
const seconds = ( end - start ) / 1000 ;
const throughput = totalMB / seconds ;
console . log ( `Throughput: ${ throughput . toFixed ( 2 ) } MB/s` );
Examples
Wallet Encryption
import * as AesGcm from '@tevm/voltaire/AesGcm' ;
async function encryptWallet ( privateKey , password ) {
// Derive key from password
const salt = crypto . getRandomValues ( Bytes16 ());
const key = await AesGcm . deriveKey ( password , salt , 600000 , 256 );
// Encrypt private key
const nonce = AesGcm . generateNonce ();
const ciphertext = await AesGcm . encrypt ( privateKey , key , nonce );
// Return encrypted wallet
return {
salt: Array ( salt ),
nonce: Array ( nonce ),
ciphertext: Array ( ciphertext ),
algorithm: 'AES-256-GCM' ,
pbkdf2Iterations: 600000
};
}
async function decryptWallet ( encryptedWallet , password ) {
// Derive same key from password
const salt = new Uint8Array ( encryptedWallet . salt );
const key = await AesGcm . deriveKey (
password ,
salt ,
encryptedWallet . pbkdf2Iterations ,
256
);
// Decrypt private key
const nonce = new Uint8Array ( encryptedWallet . nonce );
const ciphertext = new Uint8Array ( encryptedWallet . ciphertext );
return await AesGcm . decrypt ( ciphertext , key , nonce );
}
Encrypted Database
class EncryptedField {
constructor ( key ) {
this . key = key ;
this . nonceCounter = new NonceCounter ();
}
async encrypt ( value ) {
const plaintext = new TextEncoder (). encode ( JSON . stringify ( value ));
const nonce = this . nonceCounter . next ();
const ciphertext = await AesGcm . encrypt ( plaintext , this . key , nonce );
return {
nonce: Array ( nonce ),
ciphertext: Array ( ciphertext )
};
}
async decrypt ( 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 ));
}
}
// Usage
const key = await AesGcm . generateKey ( 256 );
const field = new EncryptedField ( key );
// Encrypt sensitive data
const encrypted = await field . encrypt ({ ssn: '123-45-6789' });
await db . insert ({ id: 1 , data: encrypted });
// Decrypt
const row = await db . select ({ id: 1 });
const decrypted = await field . decrypt ( row . data );
console . log ( decrypted . ssn ); // '123-45-6789'
References