What is password hashing?
A practical guide to password hashing: why storing plaintext passwords is dangerous, and how algorithms like bcrypt and Argon2 keep stored passwords secure even after a breach.

Password hashing is the process of running a user's password through a one-way mathematical function before storing it, so that the application never keeps a copy of the password itself. Instead of storing "correct-horse-battery-staple," the database stores the output of a hash function, a fixed-length string that looks nothing like the original and cannot be reversed back into it.
When a user logs in, the application doesn't retrieve their stored password and compare it. It runs the newly submitted password through the same hash function and checks whether the result matches what's on file. The password itself is never stored anywhere, and it never needs to be, because verification only requires comparing hashes, not recovering the original text.
This guide explains why storing passwords in plaintext or reversible encryption is a serious risk, how password hashing works, and why purpose-built algorithms like bcrypt and Argon2 exist instead of just using a generic hash function.
Why storing plaintext passwords is dangerous
If an application stores passwords as plaintext, anyone who gains read access to that database, an attacker who exploits a vulnerability, a malicious insider, a misconfigured backup, or a stolen laptop with a local copy, gets every user's password immediately and in full.
The damage compounds because of password reuse. Most people reuse passwords, or close variations of them, across many services. A plaintext breach at one company routinely turns into successful logins at banks, email providers, and workplace systems the attacker had no direct access to, through what's known as credential stuffing. Breach databases containing billions of plaintext or weakly protected credentials circulate widely, and automated tools test them against other sites within hours of a leak surfacing.
Plaintext storage isn't a hypothetical risk. It shows up regularly in breach disclosures, usually because a team treated password storage as a convenience problem rather than a security requirement, for example to make "forgot my password, just email it to me" flows easier to build.
Why encryption isn't the answer either
Encrypting passwords instead of hashing them looks like an improvement, since the stored value is no longer readable at a glance. But encryption is reversible by design: whoever holds the decryption key can recover every password in the database. That key has to live somewhere the application can reach it, which means an attacker who compromises the application layer, not just the database, can often decrypt everything.
The deeper issue is that encryption solves a problem password verification doesn't actually have. The application never needs to know what a user's original password was. It only needs to confirm that a submitted password matches the one that was set. That's a one-way comparison problem, not a two-way storage problem, which is exactly what hashing is designed for and encryption is not.
How password hashing works
A cryptographic hash function takes an input of any length and produces a fixed-size output, deterministically: the same input always produces the same output, but there's no practical way to work backward from the output to recover the input. That one-way property, formally called preimage resistance, is what makes hashing suitable for password storage.
A basic password hashing flow looks like this:
- At signup, the application generates a random value called a salt, combines it with the user's password, and runs the result through a hashing algorithm. The resulting hash and the salt are both stored.
- At login, the application takes the submitted password, combines it with the stored salt for that user, runs it through the same algorithm, and compares the result to the stored hash.
- A match confirms the password is correct without the application ever needing to store or recover the original value.
Why every password needs a unique salt
Without a salt, two users with the same password would produce identical hashes, and an attacker could precompute hashes for common passwords once and check them against every stolen hash in a database instantly, using what's known as a rainbow table. A unique, random salt per password defeats this. It doesn't need to be secret, only unique, and it's stored alongside the hash so the application can reproduce the same computation at login.
Some systems add an additional secret value called a pepper, kept outside the database entirely, for example in a secrets manager or hardware security module. Unlike a salt, a pepper is meant to stay secret, so that a stolen database alone isn't enough to attempt cracking, even with the salts in hand. It's an optional extra layer, not a replacement for proper salting.
Why general-purpose hash functions aren't enough
Functions like MD5, SHA-1, and SHA-256 are cryptographic hash functions, but they were built to verify data integrity and to be fast, which is exactly the wrong property for password storage. Modern GPUs and specialized hardware can compute billions of these hashes per second. A salt stops an attacker from cracking every user's password with one precomputed table, but it does nothing to slow down a brute-force or dictionary attack against a single stolen hash, since recomputing a fast hash billions of times over is still cheap.
What password storage actually needs is an algorithm that's deliberately slow, and ideally expensive in memory as well as time, so that testing large numbers of guesses becomes computationally costly even with specialized hardware. That's the specific problem bcrypt, scrypt, and Argon2 were built to solve.
Purpose-built password hashing algorithms
bcrypt
Introduced in 1999 and based on the Blowfish cipher, bcrypt was one of the first algorithms designed specifically for password storage. It includes a configurable work factor, sometimes called a cost parameter, that controls how many rounds of computation each hash requires. Because the cost scales exponentially, increasing it by one roughly doubles the work required per guess. bcrypt has a 72-byte limit on input length and truncates anything beyond it, which implementations need to account for. It remains widely supported across nearly every programming language and, at a sufficiently high cost factor, is still considered acceptable for production use.
scrypt
scrypt was designed to address a specific gap in algorithms like bcrypt: they're slow, but they don't require much memory, which means specialized parallel hardware such as GPUs and ASICs can still attack them cost-effectively at scale. scrypt is memory-hard by design, requiring a configurable, substantial amount of memory per hash computation, which makes large-scale parallel cracking significantly more expensive to build hardware for.
Argon2
Argon2 won the Password Hashing Competition in 2015 and was later standardized in RFC 9106. It's memory-hard like scrypt but adds independently tunable parameters for memory cost, time cost, and parallelism, giving implementers finer control over the tradeoff between security and server load. Its Argon2id variant, which combines resistance to GPU-based cracking with resistance to side-channel attacks, is the current OWASP recommendation for new applications. OWASP's published baseline is a minimum of 19 MiB of memory, an iteration count of 2, and a parallelism of 1, though production systems with more memory headroom commonly run substantially higher memory settings, since memory cost does more to blunt GPU and ASIC attacks than time cost alone.
PBKDF2
PBKDF2 is NIST-recommended and has FIPS-140 validated implementations available, which makes it the standard choice in government, healthcare, and financial environments where FIPS compliance is a hard requirement, since Argon2, scrypt, and bcrypt currently are not FIPS-approved. Unlike the other three, PBKDF2 is not memory-hard, so it relies entirely on iteration count for its cost, and that iteration count needs to be set very high, into the hundreds of thousands, and increased periodically as hardware gets faster.
What developers should do
Never store passwords in plaintext or reversible encryption
Password storage should always go through a purpose-built hashing algorithm. There's no legitimate reason for an application to be able to recover a user's original password, and any design that requires it, such as emailing forgotten passwords back to users, is a sign the storage approach needs to change.
Default to Argon2id for new applications
Argon2id is the current OWASP recommendation and the right default when starting a new project. bcrypt remains an acceptable choice, particularly for existing systems already built on it, since migrating hashing algorithms for an entire user base takes deliberate planning.
Use a unique, randomly generated salt for every password
This should happen automatically as part of whatever hashing library is in use. Never reuse a salt across users, and never rely on a fixed or predictable value in place of one.
Set cost parameters based on your own infrastructure
Benchmark the hashing algorithm on the actual hardware serving login requests, and tune the work factor or memory cost so that a single hash takes on the order of a few hundred milliseconds. Too low, and the algorithm offers little protection. Too high, and login requests start to strain server capacity or frustrate users.
Re-hash on login as recommendations change
Store the parameters used for each hash alongside it, and when a user logs in successfully, check those parameters against your current standard. If they're out of date, quietly re-hash the password with updated parameters before storing it again. This lets an entire user base migrate to stronger settings over time without a forced password reset.
Treat hashing as one layer, not the whole defense
Hashing protects stored credentials if a database is exposed. It doesn't stop an attacker from trying to guess passwords against your live login endpoint. Pair it with rate limiting, account lockout policies, and multi-factor authentication, so a strong hash isn't the only thing standing between an attacker and an account.
The bigger picture
Databases get breached. That's no longer a remarkable claim, it's closer to an operating assumption for any application that stores user data at scale. Password hashing doesn't prevent a breach from happening. What it determines is what an attacker can actually do with the data once they have it: whether stolen credentials are an immediate, usable list of working logins, or a set of hashes that would take meaningfully longer, and cost meaningfully more, to turn into anything useful.
Hardware keeps getting faster, which is exactly why the cost parameters recommended for bcrypt, scrypt, Argon2, and PBKDF2 keep climbing every few years. Choosing a modern, memory-hard algorithm like Argon2id, salting properly, and revisiting your cost parameters periodically isn't a one-time setup task. It's an ongoing part of keeping a breach from turning into a catastrophe.



.webp)

.webp)

.webp)




.webp)
.webp)
.webp)
.webp)
.webp)
.webp)

