How Password Hashing Works: Salts, Peppers, bcrypt and Argon2
Why good sites never keep your password, and the slow math that protects it when databases leak

Every few weeks another company admits its user database was stolen. Whether that turns into thousands of hijacked accounts or a shrug depends almost entirely on one decision made years earlier: how the passwords were stored.
Good sites never keep your actual password. They keep a fingerprint of it, produced by a deliberately slow one-way function. This guide explains how password hashing works, why salts and work factors matter, how bcrypt, scrypt, PBKDF2 and Argon2 compare, and what settings current guidance recommends if you build or run a login system.
Hashing is not encryption
Encryption is two-way: anyone with the key can turn the ciphertext back into the original. Hashing is one-way: you can compute the hash from the password, but not the password from the hash.
That property is exactly what a login check needs. When you sign in, the server hashes what you typed and compares the result with the stored hash. If they match, you typed the right password, and the server never needed to know it. The OWASP Password Storage Cheat Sheet is blunt here: passwords should be hashed, and encryption is only justified in rare cases where the original must be recovered, such as feeding a legacy system that cannot use modern delegated sign-in.
If an attacker steals an encrypted password table along with the key (which often sits on the same server), every password is exposed at once. Stealing hashes gives them a much harder job.
Why a fast hash like SHA-256 is the wrong tool
General-purpose hash functions such as SHA-256 are designed to be fast, which is great for checking file downloads and terrible for passwords. Attackers who steal hashes do not reverse them; they guess. They hash billions of candidate passwords from leaked lists and dictionaries and look for matches. The faster each hash is, the more guesses per second a graphics card can try.
Password hashing functions flip the goal: they are intentionally expensive. A login that takes a fraction of a second is unnoticeable to one user, but it multiplies the cost of every guess for an attacker trying millions of them.
Salts: making every hash unique
If two users pick the same password and the site hashes it the same way, the stored hashes are identical. That leaks information and allows rainbow tables, huge precomputed lists of password-to-hash mappings.
A salt fixes this. It is a random value generated for each password and stored alongside the hash. Because the salt is mixed into the input, the same password produces a different hash for every user. OWASP lists three benefits:
- Precomputed rainbow tables become useless.
- Attackers must crack each hash individually instead of the whole table at once.
- Nobody can tell which users share a password by comparing hashes.
Salts are not secret. NIST's SP 800-63B (revision 3) required salts of at least 32 bits, and RFC 9106, the Argon2 standard, recommends 128 bits as suitable for all applications. In practice, modern libraries create and store the salt for you.
Peppers: an optional extra secret
A pepper is a secret shared across all passwords and kept outside the database, for example in a secrets vault or hardware security module. If only the database leaks, the attacker is missing a piece needed to test guesses. NIST describes the same idea as a secret salt value that must be stored separately from the hashes. OWASP treats peppers as defense in depth, not a replacement for a proper algorithm, and notes a catch: rotating a pepper forces users to reset their passwords.
The four algorithms worth knowing
Each of these is built to be slow and tunable. Their cost settings are called the work factor.
| Algorithm | What makes it costly | When to use it | OWASP starting point |
|---|---|---|---|
| Argon2id | CPU time and memory | First choice for new systems | 19 MiB memory, 2 iterations, parallelism 1 |
| scrypt | CPU time and memory | When Argon2id is unavailable | N=217, r=8, p=1 (about 128 MiB) |
| bcrypt | CPU time (cost factor) | Legacy systems | Cost of 10 or higher |
| PBKDF2 | Iteration count | FIPS-140 or NIST-mandated environments | 600,000 iterations with HMAC-SHA-256 |
Argon2id: the memory-hard default
Argon2, designed by Alex Biryukov, Daniel Dinu and Dmitry Khovratovich, won the Password Hashing Competition in 2015 and was standardized as RFC 9106 in 2021. Its key idea is memory hardness: each hash needs a large block of RAM, which is expensive to replicate across thousands of GPU cores or custom chips. It comes in three variants. Argon2d resists GPU cracking but is more exposed to side-channel attacks, Argon2i takes the opposite trade-off, and the hybrid Argon2id blends both, which is why both the RFC and OWASP point to it.
bcrypt: still fine, with one gotcha
bcrypt is a long-established workhorse and remains acceptable, but most implementations only read the first 72 bytes of input. OWASP advises enforcing a 72-byte maximum and warns that naively pre-hashing long passwords before bcrypt can introduce new weaknesses.
PBKDF2: the compliance choice
PBKDF2 simply repeats an HMAC many times. It is not memory-hard, so it needs very high iteration counts, but it is the option approved for FIPS-140 environments.
What it looks like in code
You should never write these algorithms yourself. Use a maintained library that handles salts, encoding and constant-time comparison. Here is a minimal Python example with the widely used argon2-cffi package:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
# At sign-up: store this single string in your users table
stored = ph.hash("correct horse battery staple")
# e.g. $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
# At login
try:
ph.verify(stored, typed_password)
if ph.check_needs_rehash(stored):
stored = ph.hash(typed_password) # upgrade to current settings
except VerifyMismatchError:
deny_login()
That self-describing format (known as the PHC string format) is what OWASP recommends: keep the algorithm and its parameters with every hash so you can raise the cost later.
Tuning and upgrading over time
Hardware keeps getting faster, so yesterday's work factor slowly becomes too cheap. A sensible routine:
- Benchmark on your own servers. OWASP's rule of thumb is that one hash should take less than a second; pick the strongest setting that keeps logins responsive.
- Watch for abuse. Very high costs can turn a flood of login attempts into a denial-of-service, so pair them with rate limiting.
- Rehash on login. When a user signs in successfully, you briefly have their plaintext password; if their hash uses old settings, replace it.
- Migrate legacy hashes. For old MD5 or SHA-1 tables, OWASP describes wrapping the existing hash inside the new algorithm, then replacing it with a direct hash at the next login.
Storage is only one layer. NIST also asks services to reject new passwords that appear in known breach lists or common-password dictionaries. And the strongest fix is to need fewer passwords at all: passkeys replace the shared secret with a key pair, leaving nothing on the server worth cracking.
Key takeaways
- Store password hashes, never plaintext or reversibly encrypted passwords.
- Use a deliberately slow, salted password hashing function, not a fast hash like SHA-256.
- Argon2id is the preferred choice for new systems; scrypt, bcrypt and PBKDF2 remain acceptable in specific situations.
- Store algorithm and cost settings with each hash and rehash on login as hardware improves.
- Pair good storage with HTTPS, breached-password checks, rate limiting and, where possible, passkeys.
Frequently asked questions
Can a website tell me my password if I forget it?
A well-built site cannot. It only holds a one-way hash, which is why it offers a reset link instead of emailing your old password. A site that can send back your original password is storing it unsafely.
Does a salt need to be kept secret?
No. Salts are stored right next to the hash. Their job is to make each hash unique, not to be hidden. A pepper is the optional secret value, and it is kept outside the database.
Is bcrypt still safe to use?
Yes, for existing systems, with a cost factor of at least 10 and attention to its 72-byte input limit. For new projects, OWASP recommends Argon2id first.
If hashes are stolen, can my password still be cracked?
Weak or common passwords can still be guessed, even with strong hashing, because attackers try likely passwords first. Slow, salted hashing makes each guess expensive, so long, unique passwords or passkeys remain the best protection.







Comments
No comments yet — be the first to share your thoughts.
Join the discussion