The short answer
Quick answer: Never store passwords themselves. Store a hash: the output of a one-way function that cannot be reversed. Use an algorithm designed specifically for passwords, Argon2id, scrypt or bcrypt, which is deliberately slow and costly to compute, so that an attacker who steals the database can only test guesses slowly. Give every password its own random salt, so identical passwords produce different hashes and precomputed tables are useless. To check a login, hash the submitted password with the stored salt and compare. Use a well-tested library; do not build this yourself.
Assume the database will leak
Password storage is designed around one scenario: an attacker obtains a copy of your user table. It happens through SQL injection, a misconfigured backup, a stolen laptop or an insider. The question is what the attacker can do next.
The stakes go beyond your site. Many people reuse passwords, so a leaked password for a small forum can unlock the same person's email or bank account. Attackers automate this with credential stuffing: trying leaked email and password pairs against other services.
What not to do
Plain text
Storing passwords as written means a breach exposes every account instantly. It also means your own staff, and anyone with access to logs or backups, can read them.
Encryption
Encrypting passwords is better than plain text but still wrong. Encryption is reversible: whoever holds the key can decrypt everything, and the key usually lives on the same systems as the data. You never need to recover a user's password. You only need to check whether a submitted one matches.
A fast hash
A cryptographic hash function such as SHA-256 is one-way: you cannot run it backwards. Storing SHA-256(password) seems reasonable, and it has been the cause of many disastrous breaches.
The problem is that these functions are built to be fast. A modern graphics card can compute billions of them per second. An attacker does not reverse the hash; they guess. They hash every word in a dictionary, every leaked password from earlier breaches, and every common pattern, and compare the results with your table. Most human-chosen passwords fall quickly.
And without salts:
- Identical passwords have identical hashes, so cracking one cracks every account that shares it.
- Rainbow tables, huge precomputed lists of hashes for common passwords, turn cracking into a lookup.
Salts
A salt is a random value, generated separately for each password and stored alongside the hash. It is combined with the password before hashing.
| User | Password | Salt | Stored hash |
|---|---|---|---|
| alice | sunshine1 | 9f2a... | c41d... |
| bob | sunshine1 | 07be... | e88a... |
Same password, different results. That achieves two things:
- Precomputed tables are useless, because the attacker would need a separate table for every salt.
- The attacker must attack each account separately. Cracking one tells them nothing about the others.
A salt does not need to be secret. It must be unique and random, generated by a cryptographically secure random number generator. See Salt (cryptography).
A salt alone does not help against guessing a single user's weak password. For that, the hash must be slow.
Slow by design
Password hashing functions (also called key derivation functions) are engineered to be expensive, with an adjustable work factor.
If checking one password takes a tenth of a second, a real user logging in does not notice. An attacker who could have tried billions of guesses per second is now limited to a handful per second per processor. That turns an afternoon's cracking into centuries for any reasonably strong password.
| Algorithm | Key property | Notes |
|---|---|---|
| Argon2id | Memory-hard: needs a configurable amount of RAM per guess | Winner of the Password Hashing Competition; the first choice for new systems |
| scrypt | Memory-hard | A solid alternative |
| bcrypt | CPU-hard, with a cost factor | Long proven and widely available; processes only the first 72 bytes of input |
| PBKDF2 | Repeats a standard hash many times | Not memory-hard; used where formal compliance requires it |
Memory-hardness matters because attackers use graphics cards and custom chips that run thousands of computations in parallel. Requiring a chunk of memory for every guess makes that parallelism expensive. See Argon2.
The OWASP Password Storage Cheat Sheet lists current recommended algorithms and parameters. Consult it for the specific settings, since recommendations are raised as hardware improves.
What gets stored
Modern libraries produce a single self-describing string containing the algorithm, its parameters, the salt and the hash:
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
algorithm parameters salt hash
You store that one string per user. Because the parameters travel with the hash, you can raise the work factor later without breaking old accounts.
The login flow
Registering
- Receive the password over HTTPS.
- The library generates a random salt and computes the hash.
- Store the resulting string. Discard the password.
Logging in
- Look up the stored string for that user.
- Hash the submitted password with the same salt and parameters.
- Compare, using the library's verify function, which takes the same time whether or not the values match.
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash(password) # at registration
ph.verify(stored, submitted) # at login; raises an error if wrong
if ph.check_needs_rehash(stored): # parameters have been raised since
stored = ph.hash(submitted) # upgrade while we have the password
Upgrading over time. When you increase the work factor or change algorithm, rehash each user's password the next time they log in successfully, since that is the only moment you have it.
Peppers
A pepper is a secret value added to every password hash, stored outside the database, for example in a secrets manager or hardware security module. If only the database is stolen, the hashes cannot be attacked without it. It is an extra layer, not a replacement for salts and a slow hash.
Common mistakes
- Inventing a scheme.
SHA-256(SHA-256(password) + salt)is still fast. See why you should never roll your own crypto. - One salt for all users. That defeats the purpose.
- Logging passwords in request logs or error reports.
- Emailing passwords. A site that can send you your password is storing it badly.
- Unnecessary limits. Capping passwords at 16 characters or forbidding symbols suggests poor handling behind the scenes. Allow long passwords and passphrases.
- Forced periodic changes. Current guidance favours changing a password only when there is evidence of compromise.
- Leaking via error messages. "No such user" versus "wrong password" tells attackers which accounts exist.
Around the hash
Good storage is one layer. Also:
- Rate limit login attempts. See how rate limiters work.
- Check new passwords against lists of known breached passwords.
- Offer multi-factor authentication. See how two-factor authentication works.
- Support passkeys, which remove passwords entirely.
- Consider not storing passwords at all, by delegating sign-in to an identity provider. See how OAuth works.
Frequently asked questions
What is the difference between hashing and encryption?
Encryption is reversible with a key. Hashing is one-way: you cannot recover the input from the output. Passwords should be hashed.
What is a salt?
A random value, unique to each password, mixed in before hashing so that identical passwords give different hashes and precomputed tables do not work.
Is SHA-256 safe for passwords?
Not on its own. It is a sound hash function but far too fast, which lets attackers test billions of guesses per second.
Should I use bcrypt or Argon2?
Argon2id for new systems. bcrypt remains acceptable and is a reasonable choice where Argon2 is unavailable.
Conclusion
Store a salted hash made with a slow, purpose-built algorithm, using a trusted library, and nothing more. The goal is modest and realistic: when the database leaks, make each password guess so expensive that attackers give up on all but the weakest. Then add rate limiting and a second factor so that passwords are not your only defence.
Related articles
- How Two-Factor Authentication Codes Are Generated
- Why You Should Never Roll Your Own Crypto
- JWT vs Sessions: How Authentication Really Works
- How Public Key Cryptography Works
- How Ransomware Spreads and Encrypts Systems
