Symmetric encryption is the workhorse of modern cryptography, providing fast and efficient encryption for everything from securing database fields to encrypting entire filesystems. In this guide, we'll dive deep into symmetric encryption, explore popular algorithms, and learn best practices for implementation.
Understanding Symmetric Encryption
Symmetric encryption uses the same cryptographic key for both encryption and decryption. This shared key must be kept secret between the communicating parties, which is why it's sometimes called "secret key" or "private key" encryption.
How It Works
- 1Key Generation: A secret key is generated or derived
- 2Encryption: The plaintext is combined with the key using an encryption algorithm
- 3Decryption: The ciphertext is combined with the same key to recover the plaintext
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
// Shared secret (must be kept private!)
const key = randomBytes(32); // 256-bit key for AES-256
function encryptSymmetric(plaintext: string, key: Buffer): { iv: string; ciphertext: string } {
const iv = randomBytes(16); // Unique IV for each encryption
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
};
}
function decryptSymmetric(encrypted: { iv: string; ciphertext: string; authTag: string }, key: Buffer): string {
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Popular Symmetric Algorithms
AES (Advanced Encryption Standard)
AES is the gold standard for symmetric encryption. It's fast, secure, and widely supported.
Key Sizes:
- AES-128: 128-bit key (faster, slightly less secure)
- AES-192: 192-bit key (balance between speed and security)
- AES-256: 256-bit key (most secure, recommended for sensitive data)
Modes of Operation:
- GCM (Galois/Counter Mode): Recommended - provides authentication
- CBC (Cipher Block Chaining): Common but requires separate authentication
- CTR (Counter Mode): Useful for parallel encryption
- ECB (Electronic Codebook): ❌ Never use - insecure
import { createCipheriv, randomBytes } from 'crypto';
// AES-256-GCM (Recommended)
function encryptAES256GCM(plaintext: string, key: Buffer) {
const iv = randomBytes(12); // GCM recommends 12-byte IV
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
};
}ChaCha20
ChaCha20 is a modern stream cipher that's faster than AES in software-only implementations.
Advantages:
- Excellent performance on mobile and embedded devices
- Resistant to timing attacks
- No hardware acceleration needed
import { createCipheriv } from 'crypto';
function encryptChaCha20(plaintext: string, key: Buffer) {
const nonce = randomBytes(12); // ChaCha20 uses a nonce
const cipher = createCipheriv('chacha20-poly1305', key, nonce);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
nonce: nonce.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
};
}Encryption Modes Explained
ECB Mode (Electronic Codebook) - ❌ DON'T USE
ECB encrypts each block independently, which reveals patterns in the data.
// ❌ NEVER USE ECB - It's insecure!
const cipher = createCipheriv('aes-256-ecb', key, Buffer.alloc(0));CBC Mode (Cipher Block Chaining)
CBC is better than ECB but requires proper implementation and authentication.
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encryptCBC(plaintext: string, key: Buffer) {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
return { iv: iv.toString('hex'), ciphertext: encrypted };
}
function decryptCBC(encrypted: { iv: string; ciphertext: string }, key: Buffer) {
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}GCM Mode (Galois/Counter Mode) - ✅ RECOMMENDED
GCM provides both encryption and authentication, making it the preferred choice.
import { createCipheriv, createDecipheriv } from 'crypto';
function encryptGCM(plaintext: string, key: Buffer, associatedData?: string) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
// Add associated data (authenticated but not encrypted)
if (associatedData) {
cipher.setAAD(Buffer.from(associatedData));
}
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
};
}
function decryptGCM(encrypted: any, key: Buffer, associatedData?: string) {
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
if (associatedData) {
decipher.setAAD(Buffer.from(associatedData));
}
let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Key Management
Generating Strong Keys
import { randomBytes, scryptSync } from 'crypto';
// Method 1: Generate random key
const key = randomBytes(32); // 256-bit key
// Method 2: Derive key from password
function deriveKeyFromPassword(password: string, salt: Buffer): Buffer {
return scryptSync(password, salt, {
N: 16384, // CPU/memory cost parameter
r: 8, // Block size parameter
p: 1, // Parallelization parameter
maxmem: 32 * 1024 * 1024,
});
}
const password = 'strong-password-here';
const salt = randomBytes(16);
const derivedKey = deriveKeyFromPassword(password, salt);Storing Keys Securely
import { config } from 'dotenv';
// Load from environment variables
config();
const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
// For production, use a key management service
// AWS KMS, Azure Key Vault, or Google Cloud KMSPractical Implementation Patterns
Encrypting User Data
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
class UserDataEncryption {
private key: Buffer;
constructor(keyBase64: string) {
this.key = Buffer.from(keyBase64, 'base64');
}
encryptPersonalInfo(data: Record<string, string>): string {
const plaintext = JSON.stringify(data);
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', this.key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return JSON.stringify({
iv: iv.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
});
}
decryptPersonalInfo(encryptedData: string): Record<string, string> {
const { iv, ciphertext, authTag } = JSON.parse(encryptedData);
const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
}
// Usage
const encryption = new UserDataEncryption(process.env.ENCRYPTION_KEY!);
// Encrypt before storing
const encryptedSSN = encryption.encryptPersonalInfo({
ssn: '123-45-6789',
dob: '1990-01-01',
});
await database.saveUser({ ssn: encryptedSSN });
// Decrypt after retrieving
const user = await database.getUser(userId);
const decrypted = encryption.decryptPersonalInfo(user.ssn);Encrypting Files
import { createCipheriv, createDecipheriv, randomBytes, createReadStream, createWriteStream } from 'crypto';
import { promisify } from 'util';
import { pipeline } from 'stream';
const pipelineAsync = promisify(pipeline);
async function encryptFile(inputPath: string, outputPath: string, key: Buffer) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const input = createReadStream(inputPath);
const output = createWriteStream(outputPath);
// Write IV at the beginning
output.write(iv);
await pipelineAsync(input, cipher, output);
// Get auth tag and append to file
const authTag = cipher.getAuthTag();
const fd = await promisify(output.open.bind(output))(outputPath, 'a');
await promisify(output.write.bind(output))(fd, authTag);
await promisify(output.close.bind(output))(fd);
}
async function decryptFile(inputPath: string, outputPath: string, key: Buffer) {
const input = createReadStream(inputPath);
const output = createWriteStream(outputPath);
// Read IV from beginning
const iv = await readIV(inputPath);
const decipher = createDecipheriv('aes-256-gcm', key, iv);
// Read auth tag from end
const authTag = await readAuthTag(inputPath);
decipher.setAuthTag(authTag);
await pipelineAsync(input, decipher, output);
}Database Encryption Helper
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
class DatabaseEncryption {
private key: Buffer;
constructor(keyBase64: string) {
this.key = Buffer.from(keyBase64, 'base64');
}
encrypt(value: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', this.key, iv);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`;
}
decrypt(value: string): string {
const [ivHex, ciphertext, authTagHex] = value.split(':');
const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(ivHex, 'hex'));
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage in database queries
const encryption = new DatabaseEncryption(process.env.DB_KEY!);
// Encrypt before saving
const encryptedEmail = encryption.encrypt('user@example.com');
// Decrypt after retrieving
const email = encryption.decrypt(row.encrypted_email);Performance Optimization
Use Web Crypto API in Browser
// Browser-compatible encryption
async function encryptInBrowser(data: string, key: CryptoKey): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(data)
);
// Combine IV and ciphertext
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv);
combined.set(new Uint8Array(ciphertext), iv.length);
return btoa(String.fromCharCode(...combined));
}Parallel Encryption with Worker Threads
import { Worker } from 'worker_threads';
import { randomBytes } from 'crypto';
async function encryptParallel(data: string[], key: Buffer): Promise<string[]> {
const workers = data.map((item) => {
return new Promise<string>((resolve) => {
const worker = new Worker('./encrypt-worker.js', {
workerData: { data: item, key: key.toString('hex') },
});
worker.on('message', resolve);
});
});
return Promise.all(workers);
}Common Mistakes to Avoid
1. Reusing Initialization Vectors
// ❌ BAD: Reusing IV
const iv = Buffer.alloc(12, 0);
// ✅ GOOD: Generate new IV each time
const iv = randomBytes(12);2. Using Weak Keys
// ❌ BAD: Short or predictable key
const key = Buffer.from('password123');
// ✅ GOOD: Strong random key
const key = randomBytes(32);3. Forgetting Authentication
// ❌ BAD: Using CBC without HMAC
const cipher = createCipheriv('aes-256-cbc', key, iv);
// ✅ GOOD: Use authenticated encryption
const cipher = createCipheriv('aes-256-gcm', key, iv);4. Storing Keys with Data
Never store encryption keys alongside encrypted data.
// ❌ BAD
const encrypted = { key, data: encrypt(data, key) };
// ✅ GOOD: Store key separately
const encrypted = { data: encrypt(data, key) };
// Key stored in secure key management systemTesting Your Implementation
import { describe, it, expect } from 'vitest';
describe('Symmetric Encryption', () => {
it('should produce unique ciphertext for same plaintext', () => {
const key = randomBytes(32);
const plaintext = 'test data';
const encrypted1 = encryptGCM(plaintext, key);
const encrypted2 = encryptGCM(plaintext, key);
expect(encrypted1.ciphertext).not.toBe(encrypted2.ciphertext);
});
it('should fail with wrong auth tag', () => {
const key = randomBytes(32);
const plaintext = 'test data';
const encrypted = encryptGCM(plaintext, key);
encrypted.authTag = encrypted.authTag.replace('a', 'b');
expect(() => decryptGCM(encrypted, key)).toThrow();
});
});Conclusion
Symmetric encryption is fast, efficient, and essential for protecting data at rest and in transit. By following best practices—using authenticated encryption modes like AES-GCM, properly managing keys, and avoiding common pitfalls—you can build secure systems that protect sensitive data effectively.
Remember: symmetric encryption is just one piece of the security puzzle. Combine it with proper key management, access controls, and other security measures for comprehensive protection.