Why “Hash the Password” Isn’t Enough
(5 minutes) | How salts, key stretching, and memory-hard algorithms stop brute-force attacks.
Get our 142-page System Design Handbook for FREE on newsletter signup:
Why Your AI Agent Has Amnesia
Presented by Oracle
Many AI agents struggle with memory. They lose context, forget prior steps, and produce inconsistent outputs across sessions. This guide explores why it happens and how to fix it using agent memory patterns. A great read!
How Databases Keep Passwords Safe
“Hash it and you’re done” isn’t a strategy. It’s how breaches turn into headlines.
Every system is one misconfiguration, forgotten backup, or zero-day exploit away from a database leak.
When that happens, the only thing standing between you and a wave of account takeovers is how those passwords were stored.
Password security isn’t about keeping data secret; it’s about making stolen data useless.
That’s why modern storage relies on one-way hashing, unique salts, and slow, memory-hard algorithms that make every brute-force attempt painfully expensive.
Real security doesn’t assume safety, it assumes compromise and plans for it.
How secure storage works
When a database breach happens, it’s already too late to protect the data itself.
What still matters is how much that data can be used against you.
If your system stores plaintext passwords, attackers can log in immediately. Even “hashed” passwords aren’t safe if you used fast, general-purpose algorithms like MD5 or SHA-1.
GPUs can brute-force at billions of guesses per second.
To prevent that, password storage must slow attackers down.
Modern password storage relies on layered defenses. Each step (hashing, salting, stretching, and sometimes peppering) closes a different gap that attackers exploit.
Hashing → Converts a password into a one-way digest that can’t be reversed. The server never stores the password itself, only its hash.
Use password-specific algorithms like bcrypt, scrypt, or Argon2id, not general-purpose hashes like SHA-256. Fast hashes are designed for data integrity, not for resisting brute force. A proper password KDF (Key Derivation Function) intentionally slows verification to make offline guessing expensive.
Salting → Adds a random, per-user value to each password before hashing.
Salts ensure that identical passwords never produce the same hash and stop rainbow table lookups. They don’t need to be secret, but they must be unique and unpredictable (16 bytes or more).
Key stretching → Increases the cost of each hash operation through extra iterations or memory hardness.
This throttles brute-force attacks: where a fast hash might take microseconds, a stretched one takes hundreds of milliseconds. On your servers, that’s manageable. For an attacker, multiplied by millions of guesses, it’s economically pointless.
Peppering (optional) → Adds a server-side secret, stored outside the database (like in an HSM or key vault).
Even if the database leaks, the attacker can’t verify guesses without the pepper. It strengthens security but complicates rotation; use it only if your infrastructure can protect and rotate secrets safely.
The goal isn’t to make passwords uncrackable, but to make cracking so slow and individualized that attackers move on long before they succeed.
How passwords are stored and verified
When a user creates an account or logs in later, the system never handles their password in plain text.
Both flows (signup and login) rely on the same hashing process but apply it at different points.
1. Signup → Creating and storing the hash
When a user signs up or resets their password, the system never stores the password itself. Instead, it transforms it into a secure representation:
Generate a salt → Create a random, unique salt for this user (usually 16 bytes or more).
Hash the password → Run the password and salt through a password hashing function such as Argon2id, bcrypt, or scrypt. The function also takes cost parameters (iterations, memory, or parallelism) that control how slow the process is.
Store the result → Save the resulting hash, along with the salt and parameters, in the database.
The original password is discarded immediately. Only the salted hash and configuration is stored.
2. Login → Verifying the password
When the user logs in later, the system must check the password without knowing the original one:
Retrieve stored values → Fetch the user’s stored hash, salt, and parameters.
Re-hash the input → Run the entered password through the same function using the retrieved salt and parameters.
Compare securely → Use a constant-time comparison to check whether the new hash matches the stored one. Unlike normal string checks that exit early on mismatches, constant-time comparisons take the same amount of time no matter how similar the inputs are. This prevents timing attacks, where an attacker measures tiny response-time differences to guess parts of the correct hash one byte at a time.
Rehash if needed → If parameters are outdated (for example, fewer iterations, lower memory, or an outdated algorithm), re-hash and update the stored entry after a successful login.
Authorize access → If hashes match, issue a session token or JWT for the user.
At no point does the system store, transmit, or recover the plaintext password. The entire process proves identity by reproducing the same one-way result, not by revealing the secret itself.
What not to do
Most password breaches don’t happen because teams ignored security, they happen because they trusted weak defaults or outdated practices.
Here’s what to avoid if you want your password storage to hold up under real attacks.
Store plaintext passwords → The worst-case scenario. A single database dump means instant access to every account. There’s no recovery from this.
Use general-purpose hashes (MD5, SHA-1, SHA-256) → These functions are designed for speed, not security. They are trivial to brute-force at scale with modern GPUs and ASICs.
Skip or reuse salts → Without a unique, random salt per password, identical passwords produce identical hashes, enabling rainbow table attacks and cross-user correlation.
Set low cost factors → bcrypt cost=8 or PBKDF2 with a few thousand iterations might have been fine a decade ago. Today, it’s too cheap to crack. Always tune cost to current hardware performance.
Roll out your own algorithm → Security through creativity is not security. Established KDFs like Argon2id, bcrypt, or scrypt have been battle-tested and peer-reviewed; your custom hashing function has not.
Forget edge behaviors → Logging plaintext passwords during debugging or including them in error traces can silently undo all your protections.
Expose hints or partial passwords → Anything that helps an attacker narrow the search space reduces the work of cracking. Even “helpful” password hints expose predictable details.
Recap
Good password storage doesn’t depend on secrecy; it depends on math, cost, and discipline.
Hashing, salting, stretching, and (optionally) peppering turn every password into a unique, expensive problem for attackers to solve.
The goal isn’t to make breaches impossible, but to make them irrelevant by ensuring stolen data is useless.
Use proven KDFs like Argon2id or bcrypt, tune them regularly, and avoid shortcuts that trade speed for safety.
When done right, a database leak becomes an inconvenience; not a disaster.
👋 If you liked this post → Like + Restack + Share to help others learn system design.
Subscribe to get high-signal, clear, and visual system design breakdowns straight to your inbox:





