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 decryption reverses the encryption process while verifying the authentication tag. This ensures that:
Ciphertext hasn’t been tampered with (integrity)
Correct key and nonce were used (authentication)
AAD matches encryption (if used)
Decryption fails completely if authentication fails - no partial plaintext is returned.
Decryption Operation
Basic Decryption
import * as AesGcm from '@tevm/voltaire/AesGcm' ;
// Decrypt data (using same key and nonce from encryption)
try {
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce );
const message = new TextDecoder (). decode ( decrypted );
console . log ( 'Decrypted:' , message );
} catch ( error ) {
console . error ( 'Decryption failed:' , error );
// Could be: wrong key, wrong nonce, tampered data, or corrupted ciphertext
}
How It Works
AES-GCM decryption involves three main steps:
Separate Components
Extract authentication tag (last 16 bytes)
Extract ciphertext (remaining bytes)
Verify Authentication Tag
Recompute tag using ciphertext, AAD, nonce, and key
Compare computed tag with provided tag (constant-time)
Fail immediately if tags don’t match
Decrypt Ciphertext (only if authentication passes)
Generate keystream using counter mode
XOR keystream with ciphertext to produce plaintext
Return plaintext
Critical: Authentication is verified BEFORE decryption to prevent timing attacks and ensure tampered data is never returned.
Parameters
Ciphertext (Required)
The encrypted data including the 16-byte authentication tag:
// Minimum length: 16 bytes (just the tag, for empty plaintext)
const ciphertext = new Uint8Array ([
/* encrypted data */ ,
/* 16-byte tag */
]);
// Decrypt
const plaintext = await AesGcm . decrypt ( ciphertext , key , nonce );
Format:
┌─────────────────┬──────────────────┐
│ Ciphertext │ Auth Tag (16B) │
└─────────────────┴──────────────────┘
Error if too short:
const tooShort = new Uint8Array ( 15 ); // Less than 16 bytes
try {
await AesGcm . decrypt ( tooShort , key , nonce );
} catch ( error ) {
console . error ( error ); // "Ciphertext too short to contain authentication tag"
}
Key (Required)
Must be the same key used for encryption:
const key = await AesGcm . generateKey ( 256 );
// Encrypt
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Decrypt with SAME key
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce );
// WRONG: Different key
const wrongKey = await AesGcm . generateKey ( 256 );
await AesGcm . decrypt ( ciphertext , wrongKey , nonce ); // Throws DecryptionError
Nonce (Required)
Must be the same nonce used for encryption:
// Encrypt
const nonce = AesGcm . generateNonce ();
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Store nonce with ciphertext
const stored = new Uint8Array ( nonce . length + ciphertext . length );
stored . set ( nonce , 0 );
stored . set ( ciphertext , nonce . length );
// Later: Extract nonce 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 );
CRITICAL: Nonce must be exactly 12 bytes (96 bits)
const wrongNonce = Bytes16 (); // Wrong size!
try {
await AesGcm . decrypt ( ciphertext , key , wrongNonce );
} catch ( error ) {
console . error ( error ); // "Nonce must be 12 bytes, got 16"
}
Additional Authenticated Data (Optional)
If AAD was used during encryption, the exact same AAD must be provided for decryption:
// Encrypt with AAD
const aad = new TextEncoder (). encode ( 'metadata' );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce , aad );
// Decrypt with SAME AAD
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce , aad );
// WRONG: Different AAD
const wrongAAD = new TextEncoder (). encode ( 'different' );
await AesGcm . decrypt ( ciphertext , key , nonce , wrongAAD ); // Throws DecryptionError
// WRONG: Missing AAD
await AesGcm . decrypt ( ciphertext , key , nonce ); // Throws DecryptionError
AAD is part of authentication - any change (including omission) causes decryption to fail.
Error Handling
Authentication Failures
Decryption throws DecryptionError if authentication fails:
try {
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce );
// Success - data is authentic
} catch ( error ) {
if ( error . name === 'DecryptionError' ) {
console . error ( 'Authentication failed:' , error . message );
// Possible causes:
// - Wrong key
// - Wrong nonce
// - Wrong AAD
// - Tampered ciphertext
// - Corrupted data
}
}
Common Failure Scenarios
1. Wrong key:
const key1 = await AesGcm . generateKey ( 256 );
const key2 = await AesGcm . generateKey ( 256 );
const ciphertext = await AesGcm . encrypt ( plaintext , key1 , nonce );
await AesGcm . decrypt ( ciphertext , key2 , nonce ); // Throws DecryptionError
2. Wrong nonce:
const nonce1 = AesGcm . generateNonce ();
const nonce2 = AesGcm . generateNonce ();
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce1 );
await AesGcm . decrypt ( ciphertext , key , nonce2 ); // Throws DecryptionError
3. Tampered ciphertext:
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Modify ciphertext
const tampered = new Uint8Array ( ciphertext );
tampered [ 0 ] ^= 1 ; // Flip one bit
await AesGcm . decrypt ( tampered , key , nonce ); // Throws DecryptionError
4. Tampered authentication tag:
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Modify tag (last 16 bytes)
const tampered = new Uint8Array ( ciphertext );
tampered [ ciphertext . length - 1 ] ^= 1 ; // Flip one bit in tag
await AesGcm . decrypt ( tampered , key , nonce ); // Throws DecryptionError
5. Wrong AAD:
const aad1 = new TextEncoder (). encode ( 'metadata1' );
const aad2 = new TextEncoder (). encode ( 'metadata2' );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce , aad1 );
await AesGcm . decrypt ( ciphertext , key , nonce , aad2 ); // Throws DecryptionError
Error Messages
// Ciphertext too short
"Ciphertext too short to contain authentication tag"
// Wrong nonce length
"Nonce must be 12 bytes, got {length}"
// Authentication failed
"Decryption failed (invalid key, nonce, or corrupted data): ..."
Security Properties
Constant-Time Verification
Tag verification is performed in constant time to prevent timing attacks:
// WebCrypto API implements constant-time comparison
// Attackers cannot learn anything from execution time
const tampered1 = new Uint8Array ( ciphertext );
tampered1 [ 0 ] ^= 1 ; // First byte of ciphertext
const tampered2 = new Uint8Array ( ciphertext );
tampered2 [ ciphertext . length - 1 ] ^= 1 ; // Last byte of tag
// Both fail in approximately the same time
await AesGcm . decrypt ( tampered1 , key , nonce ); // Throws
await AesGcm . decrypt ( tampered2 , key , nonce ); // Throws
All-or-Nothing Decryption
If authentication fails, no plaintext is returned - not even partial data:
const plaintext = new TextEncoder (). encode ( 'Secret message with sensitive data' );
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Tamper with one byte
const tampered = new Uint8Array ( ciphertext );
tampered [ 5 ] ^= 1 ;
try {
const decrypted = await AesGcm . decrypt ( tampered , key , nonce );
// Never reached
} catch ( error ) {
// No partial plaintext available
// Attacker learns nothing about the message
}
This prevents padding oracle attacks and ensures data integrity.
Advanced Usage
Batch Decryption
Decrypt multiple messages in parallel:
async function decryptBatch ( encrypted , key ) {
const decrypted = await Promise . all (
encrypted . map ( async ({ nonce , ciphertext }) => {
try {
const n = new Uint8Array ( nonce );
const ct = new Uint8Array ( ciphertext );
const plaintext = await AesGcm . decrypt ( ct , key , n );
return {
success: true ,
plaintext: new TextDecoder (). decode ( plaintext )
};
} catch ( error ) {
return {
success: false ,
error: error . message
};
}
})
);
return decrypted ;
}
// Usage
const results = await decryptBatch ( encryptedMessages , key );
results . forEach (( result , i ) => {
if ( result . success ) {
console . log ( `Message ${ i } :` , result . plaintext );
} else {
console . error ( `Message ${ i } failed:` , result . error );
}
});
Extract and Decrypt
Parse stored format and decrypt:
// Stored format: [12-byte nonce][ciphertext][16-byte tag]
async function extractAndDecrypt ( stored , key ) {
// Validate minimum length
if ( stored . length < AesGcm . NONCE_SIZE + AesGcm . TAG_SIZE ) {
throw new Error ( 'Invalid stored data: too short' );
}
// Extract components
const nonce = stored . slice ( 0 , AesGcm . NONCE_SIZE );
const ciphertext = stored . slice ( AesGcm . NONCE_SIZE );
// Decrypt
return await AesGcm . decrypt ( ciphertext , key , nonce );
}
// Usage
const stored = loadFromDatabase ();
const plaintext = await extractAndDecrypt ( stored , key );
Verify Without Decrypting
Check if decryption would succeed without actually decrypting:
async function verifyAuthenticity ( ciphertext , key , nonce , aad ) {
try {
await AesGcm . decrypt ( ciphertext , key , nonce , aad );
return true ; // Authentic
} catch ( error ) {
return false ; // Not authentic
}
}
// Usage
const isValid = await verifyAuthenticity ( ciphertext , key , nonce , aad );
if ( isValid ) {
console . log ( 'Data is authentic' );
} else {
console . log ( 'Data has been tampered with' );
}
Note: This still performs decryption internally. GCM doesn’t support tag verification without decryption.
Decryption Speed
Similar to encryption (hardware-accelerated):
With AES-NI: ~2-5 GB/s
Software-only: ~50-200 MB/s
Benchmarks
const key = await AesGcm . generateKey ( 256 );
const plaintext = new Uint8Array ( 1024 * 1024 ); // 1 MB
crypto . getRandomValues ( plaintext );
// Encrypt once
const nonce = AesGcm . generateNonce ();
const ciphertext = await AesGcm . encrypt ( plaintext , key , nonce );
// Benchmark decryption
const iterations = 100 ;
const start = performance . now ();
for ( let i = 0 ; i < iterations ; i ++ ) {
await AesGcm . decrypt ( ciphertext , key , nonce );
}
const end = performance . now ();
const totalMB = ( plaintext . length * iterations ) / ( 1024 * 1024 );
const seconds = ( end - start ) / 1000 ;
const throughput = totalMB / seconds ;
console . log ( `Decryption throughput: ${ throughput . toFixed ( 2 ) } MB/s` );
Examples
Wallet Decryption
import * as AesGcm from '@tevm/voltaire/AesGcm' ;
async function unlockWallet ( encryptedWallet , password ) {
try {
// Derive 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 );
const privateKey = await AesGcm . decrypt ( ciphertext , key , nonce );
return {
success: true ,
privateKey
};
} catch ( error ) {
return {
success: false ,
error: 'Invalid password or corrupted wallet'
};
}
}
// Usage
const result = await unlockWallet ( encryptedWallet , userPassword );
if ( result . success ) {
console . log ( 'Wallet unlocked' );
// Use result.privateKey
} else {
console . error ( result . error );
}
Database Field Decryption
class EncryptedDatabase {
constructor ( key ) {
this . key = key ;
}
async decryptField ( encrypted ) {
try {
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 ));
} catch ( error ) {
console . error ( 'Field decryption failed:' , error );
throw new Error ( 'Data corruption detected' );
}
}
async queryDecrypted ( query ) {
const rows = await this . db . query ( query );
return await Promise . all (
rows . map ( async ( row ) => ({
id: row . id ,
data: await this . decryptField ( row . encrypted_data )
}))
);
}
}
// Usage
const db = new EncryptedDatabase ( key );
const results = await db . queryDecrypted ( 'SELECT * FROM users' );
results . forEach (( row ) => {
console . log ( `User ${ row . id } :` , row . data );
});
Authenticated Message Decryption
async function receiveEncryptedMessage ( encrypted , sharedSecret ) {
const key = await AesGcm . importKey ( sharedSecret );
const nonce = new Uint8Array ( encrypted . nonce );
const ciphertext = new Uint8Array ( encrypted . ciphertext );
const aad = new Uint8Array ( encrypted . metadata );
try {
const plaintext = await AesGcm . decrypt ( ciphertext , key , nonce , aad );
const message = new TextDecoder (). decode ( plaintext );
const metadata = JSON . parse ( new TextDecoder (). decode ( aad ));
return {
success: true ,
message ,
sender: metadata . sender ,
timestamp: metadata . timestamp
};
} catch ( error ) {
return {
success: false ,
error: 'Message authentication failed - possible tampering'
};
}
}
Common Mistakes
Not Handling Errors
// WRONG: Ignoring errors
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce );
// Could throw and crash
// RIGHT: Handle errors
try {
const decrypted = await AesGcm . decrypt ( ciphertext , key , nonce );
// Use decrypted data
} catch ( error ) {
console . error ( 'Decryption failed:' , error );
// Handle error appropriately
}
Using Wrong Parameters
// WRONG: Using different key
const key1 = await AesGcm . generateKey ( 256 );
const key2 = await AesGcm . generateKey ( 256 );
const ct = await AesGcm . encrypt ( pt , key1 , nonce );
await AesGcm . decrypt ( ct , key2 , nonce ); // Fails
// WRONG: Using wrong nonce
const nonce1 = AesGcm . generateNonce ();
const nonce2 = AesGcm . generateNonce ();
const ct = await AesGcm . encrypt ( pt , key , nonce1 );
await AesGcm . decrypt ( ct , key , nonce2 ); // Fails
// RIGHT: Use same key and nonce
const ct = await AesGcm . encrypt ( pt , key , nonce );
const decrypted = await AesGcm . decrypt ( ct , key , nonce ); // Success
Partial Decryption Assumptions
// WRONG: Assuming partial data on failure
try {
const decrypted = await AesGcm . decrypt ( tamperedCiphertext , key , nonce );
} catch ( error ) {
// 'decrypted' is undefined - no partial data available
// Cannot extract any information from failed decryption
}
References