
Symmetric Encryption: One Key to Rule Them All
Fast and efficient. But how do you share the key safely? The classic Key Distribution Problem.

Fast and efficient. But how do you share the key safely? The classic Key Distribution Problem.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

In the early days of my startup, I needed to encrypt customer database backups for the first time. "Encryption? Easy. Just use a library, right?" That's what I thought. But once I started writing code, I fell into a pit of choices.
// Staring at Node.js crypto docs...
crypto.createCipheriv('aes-128-cbc', key, iv);
crypto.createCipheriv('aes-256-gcm', key, iv);
crypto.createCipheriv('chacha20-poly1305', key, iv);
AES-128? AES-256? CBC? GCM? ChaCha20? What's all this? I got that bigger numbers meant more security, but why so many options? The bigger problem was: "How do I safely deliver this encryption key to the server?"
That night, diving into encryption materials, I realized something. Symmetric encryption looks simple on the surface, but to use it properly in production, you need to understand the entire ecosystem: block cipher modes, key derivation, key exchange, everything.
At first, I thought simply. "Encryption = locking with a password." But working with it revealed two different worlds.
Local file encryption (BitLocker, FileVault) is set-it-and-forget-it. Enter password once, entire drive gets encrypted. Blazingly fast. Even hundreds of gigabytes get read and written in real-time.
HTTPS communication is way more complex. How does a browser and server, meeting for the first time, safely share an encryption key? When eavesdroppers can see every packet?
Pondering this difference, it hit me. Local encryption is fine because I personally enter the key. But network encryption's core problem is "how to safely exchange the key."
The irony of symmetric encryption lies here. The encryption itself is incredibly fast and strong, but key distribution is brutally hard.
Symmetric encryption is genuinely intuitive.
One secret key encrypts, the same key decrypts. Done.
Think of your front door lock. Same key locks and unlocks the door. That's why it's called "symmetric." Mathematically:
Encrypt: Ciphertext = Encrypt(Plaintext, Key)
Decrypt: Plaintext = Decrypt(Ciphertext, Key)
This simplicity enables tremendous speed. While asymmetric encryption (like RSA) uses complex math operations like prime factorization, symmetric encryption just repeats bit substitution and permutation. For CPUs, it's like driving on a straight highway.
The actual speed difference is massive. Encrypting 1MB with RSA-2048 takes seconds. Same data with AES-256 takes milliseconds. Hundreds to thousands of times faster.
That's why production systems always use symmetric encryption for large data. Hard drives, databases, video streaming, all symmetric. Even HTTPS uses symmetric (AES) for actual data transfer. Asymmetric keys are only used for initial "key exchange."
AES (Advanced Encryption Standard) became the official standard adopted by NIST (National Institute of Standards and Technology) in 2001. The Rijndael algorithm created by two Belgian cryptographers (Joan Daemen, Vincent Rijmen) was selected.
AES uses a fixed block size of 128 bits. You can choose from three key sizes:
Larger numbers mean more security but slightly slower. In practice, AES-128 is considered plenty secure. No existing computer can brute-force 2^128 possibilities. Would take longer than the age of the universe.
AES-256 is used by governments and military for Top Secret documents. For regular services, AES-128 is sufficient.
Before AES, DES (Data Encryption Standard) was the standard. Adopted in 1977, used for over 20 years. But DES had a fatal weakness.
Key length only 56 bits. 2^56 = about 72 trillion. Sounds big, but in 1998, the EFF (Electronic Frontier Foundation) built dedicated hardware "Deep Crack" that cracked DES keys in 56 hours. In 1999, the distributed.net project cracked it in 22 hours.
Today, with GPU clusters, you can crack DES in hours. So DES was officially deprecated in 2005.
To extend DES's life, 3DES (Triple DES) emerged. Running DES three times consecutively (Encrypt-Decrypt-Encrypt). Key length increased to 168 bits, making it secure, but it got slower. Eventually AES replaced it.
Lesson: In encryption algorithms, key length is everything. No matter how complex the formula, short keys get brute-forced.
AES is a block cipher. It chops data into 128-bit blocks and encrypts them. Like laying bricks one by one.
Stream ciphers encrypt data continuously byte-by-byte (or bit-by-bit). Like flowing water. A prime example is ChaCha20.
Block Cipher (AES):
[128-bit block1] -> encrypt -> [ciphertext1]
[128-bit block2] -> encrypt -> [ciphertext2]
...
Stream Cipher (ChaCha20):
plaintext stream XOR key stream = ciphertext stream
Block ciphers need padding when encrypting data smaller than block size. Stream ciphers don't need padding, so data length stays unchanged.
ChaCha20 is faster than AES on mobile. AES needs CPU hardware acceleration (AES-NI instruction set) to be fast, but older ARM processors lack this. ChaCha20's software implementation is fast. That's why Google uses ChaCha20-Poly1305 in Android and Chrome.
When using block ciphers, if identical plaintext blocks always produce identical ciphertext blocks, patterns leak. This is ECB mode's fatal flaw.
ECB (Electronic Codebook) mode encrypts each block independently.
block1 -> AES -> cipherblock1
block2 -> AES -> cipherblock2
Problem: repeating plaintext blocks create repeating cipher blocks. Famous example is the "ECB penguin." Encrypting a penguin image with ECB shows the penguin outline. Pattern survives.
CBC (Cipher Block Chaining) mode mixes the previous block's ciphertext into the next block's encryption.
block1 XOR IV -> AES -> cipherblock1
block2 XOR cipherblock1 -> AES -> cipherblock2
IV (Initialization Vector) is a random value. Same plaintext with different IVs produces completely different ciphertexts. CBC was the standard for years, but vulnerabilities like Padding Oracle Attacks were discovered.
GCM (Galois/Counter Mode) is the modern standard. It encrypts and simultaneously generates an authentication tag to guarantee data integrity.
plaintext -> AES-CTR -> ciphertext
ciphertext -> GHASH -> auth tag
If someone tampers with the ciphertext, the auth tag won't match and decryption fails. TLS 1.3 uses AES-GCM by default. Encryption + integrity verification happens simultaneously, super efficient.
Symmetric encryption's biggest problem: how to safely share the key.
For Alice and Bob to communicate:
Transmitting the key plainly means Eve gets it too. Eve with the key can decrypt all ciphertexts. Game over.
This is the Key Distribution Problem. During the Cold War, diplomats physically carried encryption keys in briefcases. Physical delivery is safe. But in the internet era, impossible.
In 1976, Whitfield Diffie and Martin Hellman had an amazing idea. A way to create a secret key over a public channel.
Imagine this metaphor. Alice and Bob each have paint. In front of eavesdroppers:
The eavesdropper only saw yellow, orange, green. Can't make brown. Because mixing paint is easy but separating it back is hard.
Real Diffie-Hellman uses the Discrete Logarithm Problem.
1. Public values: prime p, generator g
2. Alice secret key: a, public key: A = g^a mod p
3. Bob secret key: b, public key: B = g^b mod p
4. Shared secret: s = B^a mod p = A^b mod p = g^(ab) mod p
Eavesdroppers know g, A, B but can't compute a or b. The discrete logarithm problem is mathematically hard (for large primes).
Thanks to Diffie-Hellman, Alice and Bob can create a shared secret without an encrypted channel. Use this shared secret as the AES key.
In production, we combine asymmetric (public key) and symmetric encryption.
Why? Asymmetric is slow, symmetric has key exchange difficulty. Combine the strengths of both.
HTTPS/TLS uses exactly this approach.TLS handshake:
1. Server sends public key certificate
2. Client generates random symmetric key
3. Encrypts symmetric key with server's public key, sends it
4. Server decrypts with private key, gets symmetric key
5. Now both have the same symmetric key
6. All subsequent communication encrypted with AES
Or create symmetric key with Diffie-Hellman. TLS 1.3 uses ECDHE (Elliptic Curve Diffie-Hellman Ephemeral). Creates new keys every time for Forward Secrecy.
Hybrid Encryption = Security (asymmetric) + Speed (symmetric)const crypto = require('crypto');
// AES-256-GCM encryption function
function encrypt(plaintext, password) {
// Derive key from password (PBKDF2)
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Generate IV (Initialization Vector)
const iv = crypto.randomBytes(12); // GCM recommends 12-byte IV
// Encrypt
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get auth tag (GCM integrity guarantee)
const authTag = cipher.getAuthTag();
// Must store salt, iv, authTag, encrypted together for decryption
return {
salt: salt.toString('hex'),
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
encrypted: encrypted
};
}
// AES-256-GCM decryption function
function decrypt(encryptedData, password) {
// Restore saved values
const salt = Buffer.from(encryptedData.salt, 'hex');
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
// Derive key same way (same salt produces same key)
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Decrypt
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Usage example
const secret = "Customer credit card: 1234-5678-9012-3456";
const password = "super-secret-password-2024";
const encrypted = encrypt(secret, password);
console.log("Encrypted result:", encrypted);
// {
// salt: '3a7f2b...',
// iv: '9c4e1a...',
// authTag: '8b2d3f...',
// encrypted: 'a3f8c2...'
// }
const decrypted = decrypt(encrypted, password);
console.log("Decrypted result:", decrypted);
// "Customer credit card: 1234-5678-9012-3456"
// Try with wrong password?
try {
decrypt(encrypted, "wrong-password");
} catch (err) {
console.log("Decryption failed:", err.message);
// "Unsupported state or unable to authenticate data"
}
Critical points:
Never use passwords directly as keys. Passwords are usually short and patterned. Must use Key Derivation Functions (KDF).
PBKDF2 (Password-Based Key Derivation Function 2) converts passwords into safe encryption keys. Running 100000 iterations makes brute-force harder.
Salt is a random value. Same password with different salts produces different keys. Prevents Rainbow Table attacks.
IV (Initialization Vector) is also random. Encrypting same plaintext multiple times produces different ciphertexts each time.
GCM mode generates authTag. If authTag doesn't match during decryption, it fails. Someone tampered with the ciphertext or wrong key.
const fs = require('fs');
const crypto = require('crypto');
// Encrypt large file with streams
function encryptFile(inputPath, outputPath, password) {
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const iv = crypto.randomBytes(16);
// Store salt and iv at beginning of file
const outputStream = fs.createWriteStream(outputPath);
outputStream.write(salt);
outputStream.write(iv);
// Stream encryption (CBC mode, typically used for files)
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const inputStream = fs.createReadStream(inputPath);
inputStream.pipe(cipher).pipe(outputStream);
outputStream.on('finish', () => {
console.log(`File encrypted: ${outputPath}`);
});
}
// Decrypt file
function decryptFile(inputPath, outputPath, password) {
const inputStream = fs.createReadStream(inputPath);
// First read salt and iv (16 bytes each)
let salt, iv;
inputStream.once('readable', () => {
salt = inputStream.read(16);
iv = inputStream.read(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
const outputStream = fs.createWriteStream(outputPath);
inputStream.pipe(decipher).pipe(outputStream);
outputStream.on('finish', () => {
console.log(`File decrypted: ${outputPath}`);
});
});
}
// Can encrypt even 10GB movie files without running out of memory
encryptFile('movie.mp4', 'movie.mp4.enc', 'my-password');
decryptFile('movie.mp4.enc', 'movie_decrypted.mp4', 'my-password');
Stream encryption advantages:
.env files when committing to Git (tools like git-crypt)Using passwords directly as keys is dangerous. Common passwords like "password123" are vulnerable to Dictionary Attacks.
PBKDF2 is old but safe. Downside: weak against GPU attacks. Only uses hash functions (SHA-256), so GPUs can parallelize.
Argon2 won the 2015 Password Hashing Competition, the latest algorithm. Uses lots of memory. Attacking with GPUs or ASICs requires massive memory costs, making brute-force inefficient.
// Using Argon2 (bcrypt, scrypt similar)
const argon2 = require('argon2');
async function deriveKey(password) {
// Argon2id (Argon2i + Argon2d hybrid, recommended)
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // Use 64MB memory
timeCost: 3, // 3 iterations
parallelism: 4 // 4 threads
});
// hash is string including salt
return hash;
}
async function verifyPassword(password, hash) {
return await argon2.verify(hash, password);
}
// Actual usage
const userPassword = "user-secure-password-2024";
const storedHash = await deriveKey(userPassword);
console.log(storedHash);
// $argon2id$v=19$m=65536,t=3,p=4$...
const isValid = await verifyPassword(userPassword, storedHash);
console.log(isValid); // true
Recommendations:
User enters password → PBKDF2 derives key → Decrypts master key → AES encrypts entire disk
2. HTTPS/TLSClient Hello → Server Hello + Certificate
→ Key Exchange (ECDHE)
→ Generate shared secret
→ Encrypt data with AES-128-GCM
Browser and server create new session keys every time. Even if one session key leaks, past/future sessions stay safe (Forward Secrecy).
3. Database Column Encryption-- PostgreSQL with pgcrypto extension
INSERT INTO users (name, ssn_encrypted)
VALUES ('Alice', pgp_sym_encrypt('123-45-6789', 'encryption-key'));
SELECT name, pgp_sym_decrypt(ssn_encrypted, 'encryption-key')
FROM users;
Sensitive personal data (SSN, credit cards) gets column-level encryption.
4. JWT EncryptionJWT usually only has signatures. Content is Base64, anyone can read it. For truly sensitive info, use JWE (JSON Web Encryption).
// JWE: RSA encrypts AES key, AES encrypts payload
const jose = require('node-jose');
const keystore = jose.JWK.createKeyStore();
const key = await keystore.generate('RSA', 2048);
const payload = JSON.stringify({ userId: 123, role: 'admin' });
const encrypted = await jose.JWE.createEncrypt({ format: 'compact' }, key)
.update(payload)
.final();
console.log(encrypted); // eyJhbGciOiJSU0EtT0FFUC0yNTYi...
When I first learned encryption, I thought "use AES-256, should be safe." But hitting production showed me choosing the encryption algorithm is only 10% of the puzzle.
The real challenges:
Symmetric encryption is fast and powerful, but key management is everything. Buying the strongest lock means nothing if you leave the key under the doormat.
Lessons learned in production:
In my startup's early days, encrypting database backups taught me all this. At first I thought just running openssl enc -aes-256-cbc would do. But considering key management, script automation, disaster recovery scenarios, I had to understand the entire encryption ecosystem.
Encryption isn't a silver bullet. Transport encryption (TLS), storage encryption (AES), key management (KMS), access control (IAM), audit logs all need to interlock for real security.
But it starts with properly understanding symmetric encryption. Why AES is fast, why block modes matter, why key distribution is hard. Once you get that, your view of HTTPS handshakes transforms. "Oh, there they create shared secret with ECDHE, then switch to AES-GCM." Puzzle pieces click together.
Moving forward, I'll study asymmetric keys (RSA, ECC), hashing (SHA-256), digital signatures (ECDSA) to complete this puzzle. But now I know I can build production-ready encryption systems with just symmetric encryption. That alone is huge progress.