
Hash Function: You can't un-grind the beef
The only way to store passwords safely. One-way Encryption and Avalanche Effect.

The only way to store passwords safely. One-way Encryption and Avalanche Effect.
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?

As a junior developer, I was going through legacy code and my jaw dropped.
SELECT * FROM users WHERE password = 'mypassword123';
Me: "Shouldn't we NOT store passwords in plain text?"
Senior: "That's why we use hash functions."
Me: "What's a hash function?"
Senior: "Once you grind beef, you can't turn it back into a cow, right?"
"????"I had no clue what he meant. Beef? Cow? What does that have to do with security? The senior just smiled and said "You'll understand eventually."
The following week, our database got hacked.
What the hacker stole:
CEO: "How did this happen..."
Senior: "We didn't use hash functions. All passwords were exposed in plain text."
The worst part? Users reused those passwords on other sites. Banks, email, shopping sites... one breach compromised everything.
The legal team held emergency meetings. We faced massive GDPR fines. The CTO resigned.
That day, I started studying security. Actually, I had no choice but to study it.
When I started learning about security, I hit these walls:
Most importantly: "Why make it impossible to decrypt?" If you encrypt passwords for storage, can't you just decrypt them when needed?
I asked the senior: "Why not just store the decryption key safely?"
Senior: "Who manages that key? What if that key gets stolen?"
Me: "Then... we encrypt the key with another key..."
Senior: "Then who protects that key? It's infinite regression. That's why we make it irreversible in the first place."
That hit me like a truck.The senior actually brought beef and a blender to the office.
"I'm grinding this beef in a blender.
Ground beef ← Beef (grinding)
Now, can you turn this ground beef back into a living cow? Mathematically impossible.
Hash function = Blender Input (beef) → Output (ground beef) Recovering input from output = impossible"
Watching the ground beef, I finally understood.
"Oh, information gets destroyed!"It's not that decryption is impossible — the original information is destroyed from the start. When you grind beef, you lose all structural information: which part it was from, what shape it had. All that remains is "ground beef."
That's when I wrote in my notes: "Ground beef can't become a cow again." That was it. The essence of hashing is information destruction.
const crypto = require('crypto');
const hash1 = crypto.createHash('sha256').update('Hello').digest('hex');
const hash2 = crypto.createHash('sha256').update('This is a very long string with lots of text 123456789').digest('hex');
console.log(hash1.length); // 64
console.log(hash2.length); // 64
// Always the same length!
This fascinated me. Whether the input is 5 characters or 10,000 characters, the output is always 64 characters (for SHA-256).
Properties:
Initially, I only knew #1. I learned later that #3 and #4 are actually the most important.
-- users table
| id | email | password |
|----|-------------------|---------------|
| 1 | user@example.com | mypassword123 |
| 2 | admin@example.com | admin1234 |
Problems:
This was exactly our setup. Even database backups weren't encrypted, so stealing a backup file meant game over.
const bcrypt = require('bcrypt');
// During registration
async function registerUser(email, password) {
// 1. Convert password to hash
const saltRounds = 10; // 2^10 = 1024 iterations
const hashedPassword = await bcrypt.hash(password, saltRounds);
// 2. Store only the hash in DB
await db.query(
'INSERT INTO users (email, password_hash) VALUES ($1, $2)',
[email, hashedPassword]
);
console.log('Original password:', password);
console.log('Stored hash:', hashedPassword);
// $2b$10$N9qo8uLvGBiOGNWJF/T2FeHB7KzY5ZJZ7Nw8fO.5BkJN...
}
// During login
async function loginUser(email, inputPassword) {
// 1. Fetch hash from DB
const user = await db.query(
'SELECT password_hash FROM users WHERE email = $1',
[email]
);
// 2. Convert input password to hash and compare
const isMatch = await bcrypt.compare(inputPassword, user.password_hash);
if (isMatch) {
console.log('Login successful!');
return true;
} else {
console.log('Wrong password!');
return false;
}
}
-- users table (with hashing)
| id | email | password_hash |
|----|-------------------|----------------------------------------------|
| 1 | user@example.com | $2b$10$N9qo8uLvGBiOGNWJF/T2FeHB7KzY... |
| 2 | admin@example.com | $2b$10$KpOzL9mN3qR7sT8uV9wX0eYzA... |
Even if hackers steal the DB:
$2b$10$N9qo8uL...mypassword123)After switching to this approach, I could breathe again. Even if the DB gets breached, passwords remain safe.
const crypto = require('crypto');
const hash1 = crypto.createHash('sha256').update('Hello').digest('hex');
const hash2 = crypto.createHash('sha256').update('Hello.').digest('hex');
const hash3 = crypto.createHash('sha256').update('hello').digest('hex'); // only case changed
console.log('Hello:');
console.log(hash1);
// 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
console.log('\nHello.:');
console.log(hash2);
// f96b697d7cb7938d525a2f31aaf161d0478869e5c5bb94f4b3c2d3f3f3e2d2e1
console.log('\nhello:');
console.log(hash3);
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
// Completely different!
Adding just a . changes the entire hash. This is the avalanche effect.
When I first saw this, I thought "why does this matter?" But later, when checking file integrity, I understood.
Real-world applications:
I use this when uploading files to AWS S3. Compare hashes before and after upload to verify no corruption during transfer.
When storing user profile images in my service, I generated filenames like this:
// Bad example (code I actually used)
function generateImageFilename(userId, timestamp) {
// Hash userId and use it as filename
const hash = crypto.createHash('md5')
.update(userId.toString())
.digest('hex')
.substring(0, 8); // only use first 8 characters
return `profile_${hash}.jpg`;
}
// Result: profile_5d41402a.jpg
Looked clean and simple. I thought 8 characters would be enough.
When we reached about 50,000 users, weird bug reports started coming in:
"My profile picture changed to someone else's picture."
I didn't believe it at first. Can't be possible. But digging through the logs...
User A (ID: 12345) → profile_a3d5c8e1.jpg
User B (ID: 67890) → profile_a3d5c8e1.jpg
// Collision!
Hash collision happened.
Using only the first 8 characters of MD5 hash, there are only 16^8 = 4,294,967,296 (about 4.3 billion) possible combinations. According to the Birthday Paradox, with just 50,000 users, collision probability increases dramatically.
// Improved code
const { v4: uuidv4 } = require('uuid');
function generateImageFilename(userId) {
// UUID v4: completely random, extremely low collision probability
const uuid = uuidv4();
return `profile_${userId}_${uuid}.jpg`;
}
// Result: profile_12345_f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg
After this, collision problems completely disappeared. That's when I understood: "Hash collision isn't theory — it's reality."
This happened on a project I worked on.
import hashlib
def hash_password(password):
# Hash password with MD5
return hashlib.md5(password.encode()).hexdigest()
# Registration
password = 'password123'
hashed = hash_password(password)
print(hashed)
# 482c811da5d5b4bc6d497ffa98491e38
# Store in DB
db.execute(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
('user@example.com', hashed)
)
Team lead at the time: "MD5 is a hash function. It's irreversible, so it's safe."
I believed that too. Big mistake.
1. Stole hash from DB: 482c811da5d5b4bc6d497ffa98491e38
2. Google search: "482c811da5d5b4bc6d497ffa98491e38 md5"
3. Result: password123 (cracked in 0.3 seconds!)
Why?
import bcrypt
def hash_password(password):
# Hash password with Bcrypt
salt = bcrypt.gensalt(rounds=10) # 2^10 = 1024 iterations
return bcrypt.hashpw(password.encode(), salt)
# Registration
password = 'password123'
hashed = hash_password(password)
print(hashed)
# b'$2b$10$N9qo8uLvGBiOGNWJF/T2FeHB7KzY5ZJZ7Nw8fO.5BkJN...'
# Login verification
def check_password(input_password, stored_hash):
return bcrypt.checkpw(input_password.encode(), stored_hash)
# Test
result = check_password('password123', hashed)
print(result) # True
Improvements:
After switching to Bcrypt, average login time increased by 100ms. Users don't notice, but hackers can only try 10 attempts per second instead of 10 billion. That's the critical difference.
// Hashing without salt
function hashPasswordNoSalt(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
// Registration
const user1 = hashPasswordNoSalt('password123'); // a1b2c3d4...
const user2 = hashPasswordNoSalt('password123'); // a1b2c3d4...
const user3 = hashPasswordNoSalt('password123'); // a1b2c3d4...
// Same password = same hash!
Problems:
In our DB dump, hash a1b2c3d4... appeared for 3,000 users. They all used "password123".
const bcrypt = require('bcrypt');
async function hashPasswordWithSalt(password) {
const saltRounds = 10;
return await bcrypt.hash(password, saltRounds);
}
// Registration
const user1Hash = await hashPasswordWithSalt('password123');
const user2Hash = await hashPasswordWithSalt('password123');
const user3Hash = await hashPasswordWithSalt('password123');
console.log(user1Hash);
// $2b$10$a$f7b2N9qo8uLvGBiOGNWJF...
console.log(user2Hash);
// $2b$10$x3k9m1LvP2qR5sT8uV9wX...
console.log(user3Hash);
// $2b$10$c7d5e2MnQ4rS6tU7vW8xY...
// All different!
How Salt Works:
Salt is a random string appended to the password.
User 1: hash("password123" + "a$f7b2") → $2b$10$a$f7b2...
User 2: hash("password123" + "x3k9m1") → $2b$10$x3k9m1...
User 3: hash("password123" + "c7d5e2") → $2b$10$c7d5e2...
Same password, different salt → completely different hash.
How Bcrypt Stores Salt:
$2b$10$N9qo8uLvGBiOGNWJF/T2FeHB7KzY5ZJZ7Nw8fO.5BkJN...
[algo][cost][ salt ][ actual hash ]
Salt is stored inside the hash! No need to manage it separately. During verification, Bcrypt automatically extracts and compares it.
| Algorithm | Output Length | Speed | Security | Use Case | Recommended |
|---|---|---|---|---|---|
| MD5 | 128-bit | Very fast (billions/sec) | ❌ Broken | File checksum (non-security) | Never |
| SHA-1 | 160-bit | Fast (billions/sec) | ❌ Broken | Git commits (non-security) | No |
| SHA-256 | 256-bit | Fast (millions/sec) | ✅ Safe | File checksum, blockchain | Yes for files |
| Bcrypt | 448-bit | Slow (10-100/sec) | ✅ Safe | Password storage | ✅ Passwords |
| Argon2 | Variable | Slow (tunable) | ✅ Latest & strongest | Password storage | ✅ Passwords (best) |
Speed matters for files, security matters for passwords. Got it.
Here's a script I actually use when deploying to servers.
# Calculate SHA-256 hash of file
$ shasum -a 256 app.js
d3b07384d113edec49eaa6238ad5ff00 app.js
# Save hash to file
$ shasum -a 256 app.js > app.js.sha256
$ cat app.js.sha256
d3b07384d113edec49eaa6238ad5ff00 app.js
# Upload file to server
$ scp app.js user@server:/var/www/
$ scp app.js.sha256 user@server:/var/www/
# Verify on server
$ ssh user@server
$ cd /var/www
$ shasum -a 256 -c app.js.sha256
app.js: OK
# Same hash → no corruption during transfer!
const crypto = require('crypto');
const fs = require('fs');
function calculateFileHash(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
async function verifyFileIntegrity(filePath, expectedHash) {
const actualHash = await calculateFileHash(filePath);
if (actualHash === expectedHash) {
console.log('✅ File integrity verified');
return true;
} else {
console.log('❌ File corrupted!');
console.log('Expected hash:', expectedHash);
console.log('Actual hash:', actualHash);
return false;
}
}
// Usage example
(async () => {
// Save hash before deployment
const originalHash = await calculateFileHash('./dist/app.js');
console.log('Pre-deployment hash:', originalHash);
// Verify after deployment
await verifyFileIntegrity('./dist/app.js', originalHash);
})();
Adding this to my deployment pipeline helped catch file corruption or tampering immediately.
Hashing is also used in data structures. Hash tables magically find data in O(1) time.
class HashTable {
constructor(size = 53) {
this.keyMap = new Array(size);
}
// Hash function (simple version)
_hash(key) {
let total = 0;
const WEIRD_PRIME = 31; // using prime reduces collisions
for (let i = 0; i < Math.min(key.length, 100); i++) {
const char = key[i];
const value = char.charCodeAt(0) - 96;
total = (total * WEIRD_PRIME + value) % this.keyMap.length;
}
return total;
}
// Store value
set(key, value) {
const index = this._hash(key);
// Handle collisions with Separate Chaining
if (!this.keyMap[index]) {
this.keyMap[index] = [];
}
// Update if key already exists
for (let i = 0; i < this.keyMap[index].length; i++) {
if (this.keyMap[index][i][0] === key) {
this.keyMap[index][i][1] = value;
return;
}
}
// Add if new
this.keyMap[index].push([key, value]);
}
// Retrieve value
get(key) {
const index = this._hash(key);
const bucket = this.keyMap[index];
if (bucket) {
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
return bucket[i][1];
}
}
}
return undefined;
}
// Get all keys
keys() {
const keysArr = [];
for (let i = 0; i < this.keyMap.length; i++) {
if (this.keyMap[i]) {
for (let j = 0; j < this.keyMap[i].length; j++) {
keysArr.push(this.keyMap[i][j][0]);
}
}
}
return keysArr;
}
}
// Usage example
const ht = new HashTable();
ht.set('hello', 'world');
ht.set('goodbye', 'moon');
ht.set('pink', '#ff69b4');
ht.set('cyan', '#00ffff');
console.log(ht.get('hello')); // 'world'
console.log(ht.get('pink')); // '#ff69b4'
console.log(ht.keys());
// ['hello', 'goodbye', 'pink', 'cyan']
Collision handling methods:
My code uses Separate Chaining because it's simple and easy to understand.
Problem I faced when scaling cache servers from 1 to 3.
// Bad example
function getServerIndex(key, serverCount) {
const hash = simpleHash(key);
return hash % serverCount;
}
// 3 servers
getServerIndex('user123', 3); // server 1
getServerIndex('user456', 3); // server 2
getServerIndex('user789', 3); // server 0
Looked fine. But when I added 1 more server (3 → 4)...
// 4 servers
getServerIndex('user123', 4); // server 3 (changed!)
getServerIndex('user456', 4); // server 0 (changed!)
getServerIndex('user789', 4); // server 1 (changed!)
All key locations changed! Cache hit rate dropped to 0%, DB got hammered. Service went down.
class ConsistentHash {
constructor(replicas = 150) {
this.replicas = replicas; // virtual node count
this.ring = new Map(); // hash ring
this.sortedKeys = [];
this.nodes = [];
}
// Add server
addNode(node) {
this.nodes.push(node);
// Create multiple virtual nodes
for (let i = 0; i < this.replicas; i++) {
const hash = this._hash(`${node}:${i}`);
this.ring.set(hash, node);
this.sortedKeys.push(hash);
}
this.sortedKeys.sort((a, b) => a - b);
}
// Remove server
removeNode(node) {
this.nodes = this.nodes.filter(n => n !== node);
for (let i = 0; i < this.replicas; i++) {
const hash = this._hash(`${node}:${i}`);
this.ring.delete(hash);
this.sortedKeys = this.sortedKeys.filter(k => k !== hash);
}
}
// Find which server stores the key
getNode(key) {
if (this.ring.size === 0) return null;
const hash = this._hash(key);
// Find nearest server clockwise
for (let i = 0; i < this.sortedKeys.length; i++) {
if (hash <= this.sortedKeys[i]) {
return this.ring.get(this.sortedKeys[i]);
}
}
// If at end of ring, wrap to beginning
return this.ring.get(this.sortedKeys[0]);
}
_hash(key) {
// Simple hash function (in production use CRC32, etc.)
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = ((hash << 5) - hash) + key.charCodeAt(i);
hash = hash & hash; // convert to 32-bit integer
}
return Math.abs(hash);
}
}
// Usage example
const ch = new ConsistentHash();
// Add 3 servers
ch.addNode('server1');
ch.addNode('server2');
ch.addNode('server3');
console.log('user123 → ', ch.getNode('user123')); // server2
console.log('user456 → ', ch.getNode('user456')); // server1
console.log('user789 → ', ch.getNode('user789')); // server3
// Add 1 more server
ch.addNode('server4');
console.log('user123 → ', ch.getNode('user123')); // server2 (unchanged!)
console.log('user456 → ', ch.getNode('user456')); // server1 (unchanged!)
console.log('user789 → ', ch.getNode('user789')); // server4 (some change)
With Consistent Hashing, when adding/removing servers, only 1/N of total data needs remapping. The rest stays put.
AWS ElastiCache, Redis Cluster, Cassandra — they all use this. Mind blown.
Pre-computed hash-password pairs:
// rainbowtable.txt (hundreds of GB)
482c811da5d5b4bc6d497ffa98491e38 → password123
5f4dcc3b5aa765d61d8327deb882cf99 → password
e10adc3949ba59abbe56e057f20f883e → 123456
...
Hackers just look up the hash in this table. Lookup takes less than a second.
Search "MD5 decrypt" online and you'll find tons of free services.
// Bcrypt automatically generates salt and includes it in hash
const bcrypt = require('bcrypt');
async function testRainbowTableDefense() {
const password = 'password123'; // common password
// Same password generates different hash each time
const hash1 = await bcrypt.hash(password, 10);
const hash2 = await bcrypt.hash(password, 10);
const hash3 = await bcrypt.hash(password, 10);
console.log(hash1);
// $2b$10$aF7b2N9qo8uLvGBiOGNWJF/T2FeHB7KzY5ZJZ7Nw8fO.5BkJN...
console.log(hash2);
// $2b$10$xK9m1LvP2qR5sT8uV9wX0eYzAbCdEfGhIjKlMnOpQrStUvWxYz...
console.log(hash3);
// $2b$10$c7D5e2MnQ4rS6tU7vW8xYzAbCdEfGhIjKlMnOpQrStUvWxYz...
// All different! Rainbow Table useless
}
Why it's safe:
Now I get it. Salt isn't just "salt" — it's the core defense mechanism that neutralizes Rainbow Tables.
When migrating the old company's system from MD5 to Bcrypt.
// Existing DB
| id | email | password_hash (MD5) |
|----|-------|---------------------|
| 1 | user@example.com | 5f4dcc3b5aa... |
Can't extract original password from MD5 hash, so can't re-hash with Bcrypt.
async function loginAndMigrate(email, password) {
const user = await db.query(
'SELECT id, password_hash, hash_type FROM users WHERE email = $1',
[email]
);
// 1. Verify with existing hash method
let isValid = false;
if (user.hash_type === 'md5') {
// MD5 verification
const md5Hash = crypto.createHash('md5').update(password).digest('hex');
isValid = (md5Hash === user.password_hash);
if (isValid) {
// 2. If login successful, re-hash with Bcrypt!
const bcryptHash = await bcrypt.hash(password, 10);
await db.query(
'UPDATE users SET password_hash = $1, hash_type = $2 WHERE id = $3',
[bcryptHash, 'bcrypt', user.id]
);
console.log(`User ${user.id} migrated to Bcrypt`);
}
} else if (user.hash_type === 'bcrypt') {
// Bcrypt verification
isValid = await bcrypt.compare(password, user.password_hash);
}
return isValid;
}
How it works:
Six months later, 95% of active users had migrated to Bcrypt. The remaining 5% were dormant accounts that would auto-migrate on next login.
The insight: Hash is one-way, but during login we have access to the original password, so that's when we migrate.
| Aspect | Hash | Encryption |
|---|---|---|
| Direction | One-way (irreversible) | Two-way (decryptable) |
| Key | Not needed | Needed (symmetric/asymmetric) |
| Purpose | Integrity, password storage | Data hiding |
| Examples | SHA-256, Bcrypt | AES, RSA |
| Use Cases | File checksum, passwords, blockchain | Message encryption, HTTPS |
| Speed | Fast (SHA) or slow (Bcrypt) | Relatively fast |
| Collision | Possible (low probability) | Not applicable |
Initially I thought hash was just "simple encryption." Completely different concepts.
Encryption: Secret letter. Can be opened with key.
Hash: Grinding meat. Can't be reversed.
Initially I wondered "why make passwords undecryptable?"
Now I understand:
Decryptable = Hacker can decrypt tooSo they made it irreversible. Information gets destroyed, making recovery impossible.
Lessons learned from my projects:
This one sentence captures the essence of hash functions. The moment when the senior ground beef in a blender — I remember it vividly.
Five years since the DB hack. Now I'm a senior developer, teaching security to juniors. Every time, I use the blender analogy.
Junior: "What's a hash function?"
Me: "Once you grind beef, you can't turn it back into a cow, right?"
Junior: "????"
Seeing that expression, I see myself from five years ago. This is how knowledge gets passed down, I realize.