Security Guides

How to store passwords safely: the right way to hash and salt

How to store user passwords: why plaintext, encryption and fast hashes fail, what a salt does, and how to choose and tune Argon2id, bcrypt or scrypt.

Published 2026-06-27 Updated 2026-10-23 8 min read
On this page

"How exactly should I store user passwords in the database?" — every builder hits this question once. The answer is clear. Here's the safe way to store them, in order, with no attack steps.

Why plaintext, encryption, and raw hashes all fail

Assume the database leaks someday. When it does, the damage differs wildly by storage method.

  • Plaintext: every password is exposed the instant it leaks. Worse, password reuse chains the breach into the user's other services. The worst case.
  • Encryption (reversible): the key turns it back — so if the key leaks alongside it, you're back to plaintext. A password never needs reading back, so being reversible buys you nothing.
  • Raw hash (MD5/SHA-256): a fast hash lets an attacker test guesses rapidly. Common passwords fall to rainbow tables and brute force.

Four stages to safe storage

It's quickest to understand as adding one fix at a time to a weak method.

1. plaintext: every password readable if it leaks
↓ make it one-way
2. raw hash: weak to rainbow tables
↓ add a per-user salt
3. salted hash: tables defeated, but brute force still fast
↓ make it deliberately slow
4. salted + slow hash (Argon2id / bcrypt): this is the answer
plaintext → raw hash → salted → slow hash. Only at the end is it actually 'safe storage.'

The key insight: a salt and a slow hash do different jobs. A salt defeats precomputation (rainbow tables) and bulk reuse cracking. A slow hash drops brute force to an impractical rate. You need both before it's safe (→ what hashing is).

In practice: what to do now

1

Use Argon2id (or bcrypt)

For new systems, make Argon2id the first choice — it can demand memory too, which resists GPU brute force. If you favor a well-tested implementation, bcrypt is plenty in practice. Use the standard library implementation as-is for either.

2

Let the library generate the salt

bcrypt/Argon2 generate a per-user salt and embed it inside the hash string. You don't keep a separate salt column. Hand-building MD5(salt + password) is the classic thing not to do.

3

Tune the cost for your environment

Set bcrypt's cost factor, or Argon2's memory/iterations/parallelism, as high as you can while a legitimate login still feels instant. As servers get faster, so do attackers — revisit and raise it yearly.

4

Verify with the standard compare function

At login, match the input against the stored hash with the library's verify function (most use a timing-safe constant-time compare). Don't write your own string equality.

5

Migrate weak hashes by re-hashing on login

If you already store MD5/SHA-256, the moment a user logs in successfully, re-hash with the new scheme and save it. For users who haven't logged in, you can strengthen them in the meantime by hashing the existing hash again with bcrypt (a layered hash).

Common mistakes vs the right implementation

Common mistake

  • Encrypt passwords for storage (key leaks → over)
  • Store MD5(password) or SHA-256(password) directly
  • Use a single shared salt for all users
  • Hand-roll hash(salt + password)

Right implementation

  • One-way hash with Argon2id / bcrypt
  • A per-user salt (the library adds it automatically)
  • Cost as high as feels OK, raised periodically
  • Use the standard hash/verify functions as-is

Hashing vs encryption vs encoding

These three get confused, but they differ completely in whether you can reverse them. Only hashing is acceptable for storing passwords.

HashingEncryptionEncoding (Base64 etc.)
Reversible?No (one-way)Yes, with the keyYes, by anyone
KeyNoneRequiredNone
Typical usePassword storage, tamper detectionKeeping data or traffic confidentialChanging data format
For password storageUse it (salt + slow hash)Don'tDon't (same as plaintext)

The minimum settings from the OWASP Password Storage Cheat Sheet. Treat these as a floor and raise them to suit your server.

AlgorithmMinimum settingWhen to use it
Argon2id19 MiB memory, 2 iterations, parallelism 1First choice for new systems
scryptN=2^17 (128 MiB), r=8, p=1When Argon2id isn't available
bcryptCost 10 or more. Input capped at 72 bytesExisting systems, proven track record
PBKDF2-HMAC-SHA256600,000 iterations or moreWhen FIPS 140 compliance is required

Also check your platform's built-ins. PHP's password_hash() uses bcrypt by default (PASSWORD_DEFAULT), and PHP 8.4 raised the default cost from 10 to 12. Django uses PBKDF2 by default; install django[argon2] and put Argon2 first in PASSWORD_HASHERS to switch. Django automatically re-hashes passwords stored with an older algorithm when the user logs in.

How to check your own service

You can usually tell the scheme from the start of the stored hash string. Look at a few rows in an environment you control, without copying production data elsewhere.

What the hash looks likeSchemeAction
$argon2id$v=19$m=…,t=…,p=…$Argon2idCheck m, t and p meet the OWASP minimum
$2y$12$… / $2b$10$…bcrypt (the number is the cost)Raise it if below 10
pbkdf2_sha256$1000000$…PBKDF2 (the number is the iteration count)Check it's at least 600,000
32 hex charactersMost likely MD5Migrate by re-hashing on login
40 / 64 hex charactersMost likely SHA-1 / SHA-256Same as above
The login string as-isPlaintextMigrate first. Assess the impact if it leaked

Also check the following:

  • Entered passwords are not recorded in app or web server logs, error reports, or analytics tools
  • If you use bcrypt, you've decided how to handle input longer than 72 bytes
  • Backups and database copies made for testing contain only the same kind of hashes

Real example: plaintext storage widened the damage

In the Takufile-bin incident (2019), unauthorized access to a server leaked about 4.8 million records, and the login passwords among them were stored in plaintext. Had they been stored with a salted slow hash, the risk of the leaked passwords being used directly on other services would have been far lower. Even if you can't prevent every intrusion, how you store passwords changes how much damage a leak does.

This site's view: for password storage, use the proven standard implementation

Password storage is not a place to be creative. Using the widely tested standard implementations of Argon2/bcrypt is the safest and the easiest to operate. Our stance: don't innovate here. Spend your effort instead on reducing dependence on passwords at all — thorough multi-factor authentication (MFA), and an eventual move to passkeys (passwordless). Even the strongest hash can't save a weak, reused password.

Sources

FAQ

QShould I just encrypt passwords for storage?
A

No — encryption is the wrong tool. Encryption is reversible with a key, so if the key leaks, every password is back to plaintext. A password never needs to be read back, even by you, so an irreversible 'hash' is the right answer. But a raw hash isn't enough; combine a per-user salt with a slow hash (bcrypt/Argon2).

Qbcrypt or Argon2 — which should I use?
A

For new development, Argon2 (Argon2id especially) is the first choice: it lets you tune both memory and compute, which resists GPU brute force well. If you value a long-established, well-tested implementation, bcrypt is still perfectly practical. The key point with either is to use the standard library implementation as-is and not roll your own.

QI already stored passwords with MD5/SHA-256. How do I migrate?
A

You can't bulk-convert back to plaintext (that's the point), so migrate on login. The moment a user logs in successfully, re-hash the correct password they just entered with the new scheme (e.g. Argon2id) and save it. For users who haven't logged in, you can strengthen them in the meantime by hashing the existing hash again with bcrypt (a layered hash).

QWhat's the difference between a salt and a pepper?
A

A salt is a random value that differs per user and is stored in the database alongside the hash. It doesn't need to be secret. A pepper is one secret value shared by all users, kept somewhere other than the database (a secrets manager or an HSM, for example). OWASP describes a pepper as extra defense in case only the database leaks. The salt is required; the pepper is an optional addition.

QWhat is bcrypt's 72-byte limit?
A

bcrypt only uses the first 72 bytes of its input; anything after byte 72 is ignored. Multi-byte characters (Japanese, emoji) take around 3–4 bytes each, so the limit arrives sooner than you'd think. For new systems, choose Argon2id, which has no such limit; if you use bcrypt, set a maximum input length (OWASP).

QDoes the salt need to be kept secret?
A

No. A salt's job is to make the same password hash differently for each user, which defeats precomputed tables and bulk cracking. It doesn't need to be secret, and bcrypt and Argon2 store it inside the hash string. NIST SP 800-63B-4 requires salts of at least 32 bits.