Safeguard
Security

Storing Passwords Securely: What Actually Works

Storing a password means storing a slow, salted hash of it, never the password itself. Here is the modern approach that survives a database breach.

Aisha Rahman
Security Analyst
6 min read

Storing a password correctly means never storing the password at all — you store a slow, salted, one-way hash that lets you verify a login without ever holding the secret. Get this one design decision right and a stolen database is an inconvenience; get it wrong and a single breach hands attackers every account you have. This is one of the oldest problems in application security and, judging by the breach headlines, one of the most persistently mishandled.

The goal when storing password data is not to keep the value retrievable. It is the opposite. You want a system where even you, with full database access, cannot recover what a user typed. If you can decrypt or look up the original password, so can an attacker who gets the same access.

The three things you must never do

Before the right approach, the wrong ones, because they still ship constantly:

Plaintext. Storing the raw password is indefensible. There is no scenario in modern development where this is acceptable, and any framework that appears to require it is being misused.

Reversible encryption. Encrypting passwords feels safer than plaintext but is barely an improvement. The key has to live somewhere the application can reach, which means an attacker who compromises the app compromises every password. Encryption is for data you need to read back. Passwords are not that data.

Fast hashes like MD5 or SHA-256. Hashing is the right idea, but general-purpose hashes are built to be fast, and speed is the enemy here. A modern GPU can compute billions of SHA-256 hashes per second, so a leaked table of unsalted SHA-256 password hashes falls to brute force and rainbow tables quickly.

What to actually use

The correct tool is a password hashing function designed to be deliberately slow and memory-hard. As of today the mainstream choices are, in rough order of preference:

  • Argon2id — the winner of the Password Hashing Competition and the current default recommendation from OWASP. It resists both GPU and specialized-hardware attacks by consuming memory as well as CPU time.
  • scrypt — also memory-hard, a solid choice where Argon2 is unavailable.
  • bcrypt — older but still respectable, with a tunable work factor. Note its 72-byte input limit.
  • PBKDF2 — acceptable when a FIPS-validated algorithm is a hard requirement, though it is not memory-hard and needs a high iteration count.

Here is what correct usage looks like in Node.js with Argon2:

const argon2 = require("argon2");

// On signup
const hash = await argon2.hash(plaintextPassword, {
  type: argon2.argon2id,
  memoryCost: 19456, // 19 MiB
  timeCost: 2,
  parallelism: 1,
});
// Store `hash` in the database.

// On login
const valid = await argon2.verify(storedHash, submittedPassword);

Notice there is no separate salt column. Modern libraries generate a random salt per password and embed it, along with the algorithm parameters, inside the output string. That is by design.

Salt, and why per-user is non-negotiable

A salt is random data added to each password before hashing so that two users with the same password get different hashes. Without salting, identical passwords produce identical hashes, which lets an attacker crack many accounts at once and use precomputed rainbow tables.

The rule is one unique, random salt per password, generated with a cryptographically secure random source. You do not need to invent this yourself — Argon2, bcrypt, and scrypt all handle salt generation and storage internally. If you find yourself managing a salt column by hand, you are probably using the library wrong.

Tuning the cost, and keeping it tuned

The slowness is a parameter you choose. Tune the work factor so that a single hash takes a noticeable fraction of a second on your production hardware — enough to make mass cracking expensive, not so much that logins lag. For Argon2 that means adjusting memory and time cost; for bcrypt it is the rounds value.

Because hardware gets faster, today's comfortable cost is tomorrow's weak setting. Revisit these parameters periodically. When you raise them, upgrade existing hashes opportunistically: on each successful login, check whether the stored hash used old parameters and, if so, re-hash the freshly verified password with the new settings.

Defense in depth around the hash

Strong hashing is the core, but it is not the whole story:

  • A pepper — a secret value added to every password before hashing and stored outside the database (in a secrets manager or HSM) — means a database-only breach still lacks a piece needed to crack hashes.
  • Rate limiting and lockouts slow online guessing against your live login endpoint, which no hash can stop on its own.
  • Multi-factor authentication ensures a cracked password alone is not enough to take over an account.

It is also worth remembering that your own code is only part of the surface. Authentication and crypto libraries are dependencies, and dependencies carry their own vulnerabilities — a point our SCA product overview covers in more depth. A perfect hashing implementation sitting on top of a vulnerable framework is still exposed.

FAQ

Should I hash or encrypt passwords?

Hash them. Encryption is reversible, which means the key can be stolen and every password recovered. A one-way password hash cannot be reversed even by you, which is exactly the property you want when storing password data.

Is bcrypt still safe to use?

Yes. bcrypt remains a respectable choice with an adjustable work factor. Argon2id is the current top recommendation because it is memory-hard, but bcrypt is far better than any fast hash. Just be aware of its 72-byte input limit.

Do I need to store a separate salt?

You need per-user salting, but you rarely manage it by hand. Argon2, bcrypt, and scrypt generate a random salt for each password and embed it in the output string, so a single stored value contains the salt, parameters, and hash.

How slow should password hashing be?

Tune it so one hash takes a fraction of a second on your production hardware — commonly in the range of 100 to 500 milliseconds. That makes bulk offline cracking expensive while keeping interactive logins responsive. Re-evaluate as hardware improves.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.