DEV Community

Cover image for How Does Password Hashing Actually Work? Why Isn't SHA-256 Enough?
Aditya Sharma
Aditya Sharma

Posted on

How Does Password Hashing Actually Work? Why Isn't SHA-256 Enough?

When you create an account on a website, the server obviously shouldn't store your password as plaintext. A database breach would immediately expose every user's credentials. So instead the server stores something derived from the password, and uses that for verification later.

But hashing is a one-way operation. There's no "decrypt the hash." So how does the server verify your password every time you log in?

The answer is that it doesn't decrypt anything. It hashes the password you just entered and compares the result to what's stored. If they match, the passwords match.

Registration:
Password → hash function → stored hash

Login:
Entered password → same hash function → computed hash
Computed hash == stored hash → authenticate
Enter fullscreen mode Exit fullscreen mode

The server never needs to see your original password again. It only needs to know whether your entered password produces the same hash under the same process.


Hashing Is Not Encryption

These get confused often enough to be worth clarifying.

Encryption is reversible. With the right key, you can recover the original plaintext. That's its purpose.

Plaintext → encrypt with key → ciphertext → decrypt with key → plaintext
Enter fullscreen mode Exit fullscreen mode

Hashing is one-way. A hash function takes input and produces a fixed-size digest. There's no key and no reverse operation.

Password → hash function → digest
Enter fullscreen mode Exit fullscreen mode

Storing an encrypted password is a different security posture from storing a hash. Encryption means the application holds a key that can recover plaintext. If that key is compromised, all passwords are compromised. Hashing avoids that: the original password is gone, and the server functions without it.


Why SHA-256 Seems Like the Right Tool

SHA-256 is a well-designed, widely trusted cryptographic hash function. It's deterministic, collision-resistant, and one-way. The temptation is to use it for password storage:

stored = sha256(password.encode()).hexdigest()
Enter fullscreen mode Exit fullscreen mode

This looks correct. But there's a property of SHA-256 that makes it wrong for this purpose.

It's fast.

SHA-256 is extremely fast to compute, allowing attackers to test enormous numbers of password guesses against stolen hashes. That speed is exactly what makes it useful for checksums, integrity verification, and digital signatures. For password storage, it's the problem.


The Real Threat: Offline Cracking

Consider what happens when a database containing password hashes is stolen. The attacker doesn't need to interact with the login page anymore. They can work offline, guessing passwords at whatever rate their hardware allows:

"123456"    → SHA-256 → compare
"password"  → SHA-256 → compare
"qwerty"    → SHA-256 → compare
...billions more...
Enter fullscreen mode Exit fullscreen mode

Application-level rate limiting, account lockouts, CAPTCHA: none of that applies to an attacker running offline against stolen hashes. The only thing protecting users is how expensive each guess is.

With SHA-256, each guess is extremely cheap, so attackers can test huge numbers of candidates against a stolen hash database. Common passwords fall quickly. Even moderately weak ones can be recovered.

A good password-hashing function should make each guess deliberately expensive.


Salts: Defeating Precomputation

Before getting to that, there's another problem worth understanding.

Without a salt, the same password always produces the same hash. Two users with the password hunter2 have identical stored hashes. An attacker who recognizes the hash immediately knows both passwords.

Worse, attackers can precompute. A rainbow table is a precomputed mapping from common passwords to their hashes. With SHA-256 and no salt, cracking a recognizable hash doesn't even require computation at query time.

A salt fixes this. Before hashing, the server generates a random value unique to that password:

Password + unique random salt → hash function → stored hash
Enter fullscreen mode Exit fullscreen mode

The salt isn't a secret. It's stored alongside the hash. Its purpose is to make each password instance unique, so that two identical passwords produce different stored hashes, and precomputed tables become useless because they didn't use this specific salt.

But a salt doesn't make a weak password strong. An attacker who has both the hash and the salt can still guess:

"123456" + known salt → hash → compare
"password" + known salt → hash → compare
Enter fullscreen mode Exit fullscreen mode

The salt defeats precomputation. It doesn't slow down the guessing itself. For that, you need a function that's expensive per guess.


Purpose-Built Password Hashing

bcrypt, scrypt, and Argon2 exist specifically because general-purpose cryptographic hashes are the wrong tool for password storage.

These algorithms are designed to be slow. Not broken-slow, but deliberately expensive in a configurable way. bcrypt exposes a configurable cost factor that increases the computational work required for each password hash. Increase the cost factor, increase the cost per hash.

The security goal is asymmetry:

Legitimate login:
1 password verified → 100ms → acceptable

Offline attacker:
1 billion guesses → 100ms each → impractical
Enter fullscreen mode Exit fullscreen mode

The work factor can be increased over time as hardware improves, keeping the cost curve roughly constant for attackers even as CPUs get faster.

scrypt and Argon2 add another dimension: memory. GPU-based cracking rigs are cheap and massively parallel, but they have limited memory per compute unit. A hash function that requires substantial memory per guess constrains how many guesses can run in parallel on GPU hardware.

Fast hash:
Guess → CPU → result

Memory-hard hash:
Guess → CPU + large memory allocation → result
Enter fullscreen mode Exit fullscreen mode

Argon2, particularly Argon2id, is widely recommended for password hashing. It exposes configurable time cost, memory cost, and parallelism. scrypt similarly supports memory-hard parameters. bcrypt is older and lacks memory hardness but remains widely deployed and is substantially better than SHA-256 for this purpose.


What Actually Gets Stored and How Verification Works

Modern password-hashing libraries produce a self-contained output that encodes the algorithm, parameters, salt, and hash together in a standardized format. You don't track these fields separately; the library handles it.

Registration:

Password
   ↓
Generate unique random salt
   ↓
Password hashing function (with work factor, memory parameters)
   ↓
Store encoded output: algorithm + params + salt + hash
Enter fullscreen mode Exit fullscreen mode

Login:

Entered password
   ↓
Read stored encoded output
   ↓
Extract salt and parameters
   ↓
Run same password-hashing function with same parameters
   ↓
Compare result with stored hash
   ↓
Match → authenticate
Enter fullscreen mode Exit fullscreen mode

The server doesn't recover the password. It derives the expected hash from the entered password and compares it with the stored result.

Use an established library. The algorithms are subtle and easy to misuse. bcrypt, argon2-cffi, and similar well-maintained libraries have been tested and audited. Custom implementations of these algorithms are unnecessary and risky.


What This Actually Protects Against

Password hashing doesn't make a stolen database harmless. An attacker with your hashes and salts can still guess. The goals are more specific:

  • precomputed attacks fail because salts are unique per password
  • common passwords are harder to crack at scale because each guess is expensive
  • strong passwords remain highly resistant because the search space is enormous even with expensive hardware
  • every account requires independent cracking because salts make identical passwords produce different hashes Weak passwords remain vulnerable. A password such as 123456 has a small enough search space that an attacker may still recover it despite the cost imposed by a strong password-hashing function.

The distinction that matters:

General-purpose hash (SHA-256):
→ fast by design
→ inappropriate for password storage

Password hash (bcrypt, scrypt, Argon2):
→ deliberately expensive
→ salted per password
→ designed to resist offline guessing
Enter fullscreen mode Exit fullscreen mode

Password security isn't about making a hash impossible to reverse. It's about making every guessed password expensive to verify. The server never needs to know your password. It only needs to know whether what you entered produces the same result under the same stored parameters.

That's the whole model.

Top comments (0)