Client Area

Secure Password Hashing: bcrypt, argon2, and What Never to Use

ByDomain India Security Team
10 min read22 Apr 20264 views

In this article

  • 1The principle
  • 2The three acceptable algorithms in 2026
  • 3argon2id — the current recommendation
  • 4scrypt
  • 5bcrypt

Secure Password Hashing: bcrypt, argon2, and What Never to Use

Every major password leak in the news — and every minor one you don't hear about — is a story about bad hashing. The defence is cheap and has been well-understood for over a decade. This article covers what to use (argon2id, bcrypt), what to never use (MD5, SHA-256), how to tune the parameters, and how to rotate old hashes safely after an upgrade.

The principle

Never store a user's password. Store a slow, one-way derivation of it. On login, hash the submitted password the same way and compare. If your database leaks, the attacker has hashes — and a slow hash makes brute-forcing each one expensive.

Two qualities make a hash suitable for passwords:

  1. Slowness. Designed to take 100–500 ms per computation on modern hardware. Makes brute-force slow.
  2. Memory-hardness. Requires significant RAM per computation. Defeats GPU / ASIC acceleration.

Generic cryptographic hashes (MD5, SHA-256, SHA-3) are optimised for speed — billions of operations per second on GPU. That's the opposite of what passwords need.

The three acceptable algorithms in 2026

argon2id — the current recommendation

Winner of the Password Hashing Competition (PHC) in 2015. Recommended by OWASP since 2019. Memory-hard, tunable, modern.

  • argon2d — maximally GPU-resistant, but vulnerable to side-channel attacks on shared systems
  • argon2i — side-channel-resistant, slower than d
  • argon2id — hybrid; best of both. Use this.

Parameters: time cost, memory cost, parallelism. Default values in PHP 7.3+ are reasonable starting points.

scrypt

Older than argon2, still fine. Memory-hard. Used by Bitcoin. Slightly less flexible than argon2 but battle-tested.

bcrypt

From 1999, predates the memory-hard concept. Based on Blowfish cipher. Still acceptable if you use a sufficient cost parameter (12+).

Trade-off: not memory-hard — GPU clusters can brute-force bcrypt faster than argon2. On the other hand, bcrypt is everywhere — every language has a mature library, and migrating is rarely worth it unless you're already on something weaker.

What NOT to use

  • MD5 — broken. 10 billion hashes / second on modern GPU.
  • SHA-1 — also broken.
  • SHA-256 / SHA-512 — not broken, but far too fast for passwords.
  • PBKDF2 — still accepted by NIST; acceptable but less memory-hard than argon2. If you must (compliance reason), use 600,000+ iterations of PBKDF2-SHA-256.
  • Your own custom hash. Never. The only worse option than a bad choice is rolling your own.

PHP implementation

PHP has password_hash and password_verify built in. Use them — they're maintained by the core team, correct by default, and automatically handle migration.

Hash a new password

php
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_ARGON2ID);
// Store $hash in your DB

PASSWORD_ARGON2ID is available from PHP 7.3+. For earlier PHP (do upgrade!) use PASSWORD_BCRYPT.

The resulting hash string looks like:

$argon2id$v=19$m=65536,t=4,p=1$cHJtSGVhZA$hash-bytes-in-base64

It's self-contained — algorithm, parameters, salt, and the hash itself are all embedded. No need to store salt separately.

Verify a password on login

php
$submittedPassword = $_POST['password'];
$storedHash        = $user->password_hash;   // from DB

if (password_verify($submittedPassword, $storedHash)) {
    // Password is correct — log the user in
} else {
    // Wrong password
}

password_verify is timing-safe by design — no side-channel leaks about how close the submitted password was.

Rehash on login (parameter upgrade path)

Over time, you'll want to increase the cost parameters (hardware gets faster). Or migrate from bcrypt to argon2id. PHP has built-in support:

php
if (password_verify($submittedPassword, $storedHash)) {
    // Password is correct — check if hash needs upgrade
    if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($submittedPassword, PASSWORD_ARGON2ID);
        $user->updatePasswordHash($newHash);
    }
    loginUser($user);
}

This migrates each user to the new algorithm / cost on their next successful login — no forced password reset. After a few months, 95%+ of active users are on the new hash.

Node.js implementation

argon2 (preferred)

bash
npm install argon2
javascript
import argon2 from 'argon2';

// Hash a new password
const hash = await argon2.hash(password);

// Verify on login
if (await argon2.verify(hash, submittedPassword)) {
  // Password correct
}

// Check if rehash needed (e.g., after bumping parameters)
if (argon2.needsRehash(hash, { type: argon2.argon2id, memoryCost: 2 ** 17 })) {
  const newHash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 2 ** 17 });
  await user.update({ passwordHash: newHash });
}

bcryptjs / bcrypt (legacy or small dependencies)

bash
npm install bcrypt          # native binding, fastest
# or
npm install bcryptjs        # pure JS, no native dependencies
javascript
import bcrypt from 'bcrypt';

const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);

if (await bcrypt.compare(submittedPassword, hash)) {
  // Correct
}

12 is the current sensible minimum. 14 is better if your hardware can afford the 300–500 ms per hash.

Parameter tuning — the speed-security knob

The core trade-off: slower hash = stronger defence, but slower logins.

Rule of thumb: target 250–500 ms per verification on your production hardware. Measure it:

php
$start = microtime(true);
password_hash('test', PASSWORD_ARGON2ID);
echo (microtime(true) - $start) * 1000 . ' ms';

If you see < 100 ms, increase the cost. If > 1 second, decrease — users will notice login lag.

argon2 parameters

php
$options = [
    'memory_cost' => 65536,      // 64 MB (default: 65536)
    'time_cost'   => 4,           // iterations (default: 4)
    'threads'     => 1,           // parallelism (default: 1)
];
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);

OWASP current recommendations (2026):

  • memory_cost — at least 19 MiB (19456)
  • time_cost — at least 2
  • threads — 1 (one per verification — multiple threads means multiple cores busy)

bcrypt cost

php
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

Each increment doubles the work. 10 = too fast (2016 baseline). 12 = current acceptable minimum. 14 = better for high-security. 16 = overkill for most apps.

Pepper — an optional extra layer

A "pepper" is a secret added to the password before hashing, stored separately from the database (in an env var or a secret store — see Environment Variables & Secrets Management).

php
$pepper = $_ENV['PASSWORD_PEPPER'];   // long random string, NOT in DB
$hash = password_hash($submittedPassword . $pepper, PASSWORD_ARGON2ID);

If the DB leaks without the pepper also leaking, an attacker can't brute-force hashes — they don't know the pepper value. Requires compromising two separate systems.

Trade-off: pepper rotation is hard. If you change the pepper, every existing hash becomes invalid. Most apps skip pepper and rely on hash strength + defence-in-depth.

Password reset flows

Forgot-password is where password handling breaks down most often. Correct pattern:

  1. User requests reset by email
  2. Generate a cryptographically random token (32 bytes, hex-encoded)
  3. Store the HASH of the token in DB (same principle as passwords — don't store the plaintext)
  4. Email the plaintext token to the user as part of a URL
  5. Token expires in 15–60 minutes
  6. On click, hash the URL parameter, compare to stored hash, if match → allow new-password form
  7. Mark token used after one successful reset — single-use

Crucially:

  • Token is single-use
  • Token expires quickly (minutes, not days)
  • Don't reveal whether the email exists — respond the same for existing and non-existing users (prevents enumeration)
  • Rate-limit reset requests per IP and per email

What to do after a breach

If your password database is leaked:

  1. Invalidate all sessions immediately. Rotate your JWT signing key / session secret. Every user is logged out.
  2. Force password reset on next login for all users. Do not wait for them to notice.
  3. Email every user explaining what happened, what to do, and what you're doing. Required by DPDP Act in India, GDPR in EU, similar laws elsewhere.
  4. Rotate the pepper if you use one.
  5. Audit the breach vector and fix it (was it SQL injection? A backup exposed on an S3 bucket? Phishing of an admin account?).
  6. File required breach notifications — Data Protection Officer in India, ICO in UK, etc.
  7. Monitor for credential stuffing — attackers will try the leaked credentials on other sites; watch for unusual login patterns.

Common pitfalls

  1. Using MD5 or SHA-256. Still seen in legacy code. Migrate on next login with password_needs_rehash.
  2. Salt in the password. Before modern hashing, people salted manually. password_hash / argon2.hash do this automatically and correctly.
  3. Comparing hashes with `==`. Use password_verify / argon2.verify / crypto.timingSafeEqual. The built-ins are timing-safe.
  4. Storing hashes in logs. Passwords can accidentally land in logs via stack traces or debug dumps. Scrub them.
  5. Low cost parameter. 8 rounds of bcrypt is brute-forceable in hours on GPU. Minimum 12.
  6. Unlimited login attempts. Rate-limit by IP + account. Lock accounts after N failures.
  7. Password reset token in URL query string. Tokens in URLs leak via Referer headers and get logged. Prefer POST form submission of the token + a separate URL.
  8. Logging plaintext passwords during debug. Never. Even briefly. Especially in production.

Regulatory considerations (India-specific)

The Digital Personal Data Protection Act (DPDP) 2023, now enforced, requires reasonable security safeguards for personal data. While it doesn't prescribe specific algorithms, using MD5 or SHA-256 for passwords — well below modern baseline — would likely fail the "reasonable safeguards" test if a breach happens.

Industry-specific regulations (RBI guidelines for banks, SEBI for capital markets, MeitY for healthcare data) often have stricter password / authentication requirements. Check your sector's rules if you're in a regulated industry.

Frequently asked questions

Should I hash passwords client-side too?

Generally no. Client-side hashing only helps if an attacker is intercepting traffic — but HTTPS already solves that. Adding client-side hashing creates complexity with no real benefit. Use HTTPS, hash server-side.

How long should a minimum password be?

OWASP 2026: 8 characters minimum, 12+ recommended. Length beats complexity — a 16-character all-lowercase password is stronger than an 8-character one with all-character-class requirements. NIST now discourages forced "1 uppercase, 1 number, 1 special char" rules.

Should I require password expiration (every 90 days)?

Modern NIST guidance says no — forced rotation leads users to pick weaker passwords (they just add 123). Only force rotation if there's specific evidence of compromise.

What about passkeys / WebAuthn?

The future. Passkeys eliminate passwords entirely with device-bound cryptographic keys. Where feasible (your user base is on modern browsers + devices), offer passkeys as an option alongside passwords. Over time, passwords can be optional.

Is argon2id supported on all hosting plans?

PHP 7.3+ supports argon2id. Our cPanel and DirectAdmin plans all run PHP 7.4 or higher (default is 8.2+ on new accounts). For older PHP, bcrypt is the fallback.

How much slower is argon2 than bcrypt?

Similar verification time at comparable parameter strength. argon2's advantage is memory-hardness — same time, much more expensive for attackers using GPU.

Should I use 2FA in addition to a strong password hash?

Yes, absolutely, especially for administrative accounts. Password hashing protects against offline brute-force after a DB leak; 2FA protects against online attacks (credential stuffing, phishing). Use both.


Need help auditing your password-hashing code? [email protected]. Our senior support team can review authentication flow as a standard support request.

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket