Storing passwords safely: hashing, salting, bcrypt and Argon2

If your database leaks, how you stored passwords decides how much damage follows. Learn the safe way and the common mistakes.

A password turned into a salted hash and stored safely

Passwords are among the most sensitive data your application holds. Databases get leaked through bugs, backups, misconfiguration and insider mistakes. How you store passwords decides whether a leak is a disaster or a contained incident. This article is a defensive guide for developers.

Never store plain text, and never encrypt them

  • Plain text means anyone who reads the database sees every password.
  • Reversible encryption is also wrong for user passwords. If the key is stolen or the app can decrypt, an attacker can too. You never need to know a user's password, only to check whether what they typed matches.

The correct tool is a one-way password hash: a function that turns the password into a fixed value from which the password cannot be recovered. At login you hash the typed password and compare.

Why ordinary fast hashes are not enough

General hashes such as MD5, SHA-1 and plain SHA-256 are designed to be fast. That is a weakness for passwords, because an attacker with a stolen hash database can try billions of guesses per second on modern hardware, and people choose predictable passwords. Fast hashes also let attackers use precomputed tables (rainbow tables) and spot users who share a password.

Salting

A salt is a random value generated per password and stored alongside the hash. The hash is computed over the password plus the salt.

  • Two users with the same password get different hashes.
  • Precomputed tables become useless.
  • Attackers must attack each hash separately.

The salt is not secret. It just has to be unique and random. Modern password hashing libraries create and store the salt for you inside the output string.

Use a slow, purpose-built password hash

Password hashing algorithms are intentionally slow and memory-hungry, with adjustable cost, so guessing becomes expensive for the attacker while one login stays quick for you.

  • Argon2id: a modern, widely recommended choice, resistant to GPU attacks because it uses configurable memory. Winner of the Password Hashing Competition.
  • scrypt: also memory-hard, a solid option.
  • bcrypt: older but battle-tested and widely available. Work factor (cost) is adjustable. Note it has a 72-byte input limit in common implementations, so very long passwords are truncated.
  • PBKDF2: acceptable where standards or compliance require it, with a high iteration count. It is not memory-hard.

Choose the algorithm your platform supports well, and follow current guidance from a trusted source such as the OWASP Password Storage Cheat Sheet for parameters, since recommended settings change over time as hardware improves. Tune the cost so that hashing takes a noticeable but acceptable time on your servers (for example in the range of a fraction of a second per login), and test under load.

Example (Node.js style, concept only)

// using a maintained library that handles salts and parameters for you
const hash = await argon2.hash(password);          // store this whole string
const ok   = await argon2.verify(hash, attempt);   // true or false at login

Use well-maintained libraries. Do not invent your own scheme, and do not combine hashes in clever ways.

Pepper (optional extra layer)

A pepper is a secret value mixed in that is not stored in the database but in application configuration or a hardware security module. If only the database leaks, the pepper is still unknown. It adds protection but also complexity (rotation is hard), so treat it as an extra, not a replacement for a good password hash.

Upgrading hashes over time

Store the algorithm and parameters with each hash (most libraries do). When a user logs in successfully and their hash uses old settings, re-hash with the new settings and save it. That migrates users gradually without asking them to reset.

Password policy

  • Allow long passwords and passphrases, and allow all characters, including spaces and Unicode. Set a sensible maximum (for example 64 to 128 characters) to prevent denial-of-service from huge inputs.
  • Require a reasonable minimum length (current guidance emphasises length over odd complexity rules).
  • Check new passwords against lists of known breached or common passwords.
  • Do not force frequent periodic changes without a reason. Require a change when there is evidence of compromise.
  • Support password managers (allow paste, proper input types).
  • Offer multi-factor authentication (an authenticator app, security key or passkeys). It protects accounts even when a password is stolen.
  • Consider passkeys (WebAuthn) as a passwordless option.

Login and reset flows

  • Show generic error messages ("incorrect email or password") so attackers cannot learn which emails exist.
  • Apply rate limiting and lockout or delay on login attempts, and monitor for credential stuffing.
  • Password reset: send a single-use, random, short-lived token (store only its hash), over HTTPS, to the verified email. Invalidate it after use, and notify the user of the change. Never email the password itself, and never send existing passwords at all (you cannot, if you hash correctly).
  • Do not log passwords or put them in URLs, analytics or error reports.
  • Use HTTPS everywhere.

If you suspect a breach

  1. Contain and investigate the cause.
  2. Force password resets for affected accounts, and invalidate sessions and tokens.
  3. Notify users and follow the legal rules that apply to you. Tell users to change passwords wherever they reused them.
  4. Fix the root cause and review your hashing settings.

Common mistakes

  • Using MD5, SHA-1 or unsalted SHA-256 for passwords.
  • Reversible encryption, or storing passwords in a recoverable form.
  • A single global salt for all users.
  • A cost setting copied from an old tutorial.
  • Logging requests that contain passwords.
  • Differentiating "email not found" from "wrong password".
  • Building a custom hashing scheme.

Quick checklist

  • Argon2id (or scrypt/bcrypt) with a maintained library and current parameters.
  • Unique random salts, handled by the library.
  • Re-hash on login when parameters change.
  • Breached-password checks, long passwords allowed, MFA offered.
  • Safe reset tokens, generic errors, rate limiting.
  • No passwords in logs, URLs or backups in plain form.

Takeaway: never store or encrypt passwords reversibly. Use a slow, salted password hash such as Argon2id with a maintained library, add MFA and breach checks, and secure reset and login flows.


Web security series, Part 3 (accounts and defence): 1 of 5.

Keep reading

Also on Newssite