Skip to content
How-to · Cryptography

How to Store Passwords Securely

The way you store passwords decides how bad a database breach is. Done right, a stolen table of hashes is close to worthless to an attacker; done wrong, it is a ready-made credential dump. This guide covers the modern standard — a memory-hard hashing function, per-user salt, a secret pepper, carefully tuned work factors — and how to migrate off legacy hashes without forcing everyone to reset.

In plain words

Storing passwords securely means hashing them with a memory-hard function instead of ever keeping them readable, and PlayCISO has a free, browser-based Password Storage Lab to see it work. Use Argon2id (or bcrypt/scrypt), a unique random salt per user, a secret pepper kept outside the database, and work factors tuned to your hardware.

Never store plaintext or reversible encryption. When upgrading from weak hashes, wrap the old hash in a strong KDF immediately and re-hash on next login — no mass reset required.

Step by step

  1. 1

    Never store plaintext or reversible encryption

    A password store must be one-way: if someone copies the database, they must not be able to recover the passwords. That rules out plaintext and any reversible encryption, because the decryption key lives near the data and will be stolen with it. You only ever need to verify a password, not read it back, so hash it instead of encrypting it.

  2. 2

    Choose a memory-hard password hashing function

    Use a function designed for passwords: Argon2id is the current first choice (OWASP recommends it), with scrypt and bcrypt as acceptable alternatives. These are deliberately slow and, for Argon2id and scrypt, memory-hard, which makes GPU and ASIC cracking far more expensive. Never use fast general-purpose hashes like MD5, SHA-1 or a bare SHA-256 for passwords.

  3. 3

    Generate a unique random salt per user

    Create a cryptographically random salt (at least 16 bytes) for every password and store it alongside the hash. The salt stops precomputed rainbow tables and ensures two users with the same password get different hashes. Modern KDFs like Argon2id and bcrypt generate and embed the salt in their output string automatically, so you store a single self-describing value.

  4. 4

    Add a secret pepper held outside the database

    A pepper is a single secret value, the same for all users, kept out of the database — in a secrets manager, HSM, or app config, not in the table with the hashes. Apply it as an HMAC over the password before hashing, or as a key to the KDF where supported. If the database alone leaks, the pepper means the stolen hashes still cannot be cracked.

  5. 5

    Tune work factors to your hardware

    Set the cost parameters so a single hash takes a noticeable but acceptable time on your server — commonly around 0.25 to 1 second under load. For Argon2id tune memory, iterations and parallelism; for bcrypt raise the cost factor (around 10 to 12 is a common floor). Measure on real production-grade hardware and re-tune as hardware gets faster, keeping an eye on login throughput.

  6. 6

    Verify with constant-time comparison

    When checking a login, re-derive the hash from the submitted password using the stored salt and parameters, then compare using the KDF library verify function, which performs a constant-time comparison. Avoid plain string equality, which can leak timing information. Treat a successful verify as the only signal; never branch on partial matches.

  7. 7

    Migrate legacy hashes safely

    If you inherit weak hashes (MD5, SHA-1, unsalted SHA-256), do not wait for every user to log in. Wrap the old hash in the new KDF — for example Argon2id(old_hash) — so every stored value is immediately upgraded, then transparently re-hash from the real password on next successful login and drop the wrapper. This removes the weak primitive from your store without forcing a mass password reset.

Do it now — free, in your browser
Open the Password Storage Lab

Compare hashing schemes, see how salt and work factors change the output, and watch why plaintext and fast hashes fail — free, in your browser.

Related

Frequently asked questions

Why not just encrypt passwords instead of hashing them?

Encryption is reversible, which means the key needed to decrypt the passwords exists somewhere — usually on the same server as the data. An attacker who steals the database will typically steal the key too, and then every password is exposed. Hashing is one-way: you can verify a login without ever being able to reconstruct the password, so a database leak does not hand over the passwords.

Argon2id vs bcrypt vs scrypt — which should I use?

Argon2id is the current recommended default: it is memory-hard and resists both GPU and side-channel attacks. scrypt is also memory-hard and a solid choice. bcrypt is older and not memory-hard but is still acceptable and widely available, with the caveat that it silently truncates inputs beyond 72 bytes. If your platform ships a vetted Argon2id implementation, prefer it; otherwise use a well-maintained bcrypt or scrypt library.

What is the difference between a salt and a pepper?

A salt is unique per password and is stored with the hash in the database; its job is to defeat rainbow tables and ensure identical passwords hash differently. A pepper is a single secret shared across all users and kept outside the database, for example in a secrets manager. The salt protects against precomputation; the pepper protects the whole store if only the database leaks.

How do I pick work factors without slowing down logins too much?

Benchmark on hardware that matches production and aim for a per-hash time of roughly a quarter to a full second under expected concurrent load. Higher is more resistant to cracking but costs CPU and memory at login time, so balance against your throughput. Revisit the setting periodically because faster hardware lets attackers crack the same cost more cheaply over time.

Should I force all users to reset passwords when upgrading from a weak hash?

Usually no. You can wrap the existing weak hash inside a strong KDF immediately so the stored value is no longer crackable, then quietly re-hash from the real password the next time each user logs in. That upgrades the whole store at once without the support burden and churn of a mass reset. A forced reset is only warranted if you believe the weak hashes have already leaked.

Is a long SHA-256 with many rounds good enough?

Not really. SHA-256 is a fast general-purpose hash, and even iterated it is cheap to run on GPUs at massive scale. Password KDFs like Argon2id are deliberately memory-hard specifically to break that economics. Use a purpose-built password hashing function rather than trying to make a fast hash slow yourself.

🎉 New here? Use code WELCOME10 for 10% off any plan at checkout
How to Store Passwords Securely — PlayCISO · PlayCISO