iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Password Hashing (argon2/bcrypt)

Never store plaintext passwords; never hash them with a fast hash (SHA-256, MD5). Use a memory-hard, slow KDF: argon2id, scrypt, or bcrypt. Tune cost to ~250ms per hash on your hardware.

argon2id (Node, Python), upgrade path

EXAMPLE
// 1) Node — argon2 (npm i argon2)
import argon2 from 'argon2';

// Hash on signup
const hash = await argon2.hash(password, {
    type:        argon2.argon2id,
    memoryCost:  19 * 1024,   // 19 MB — OWASP 2024 minimum
    timeCost:    2,
    parallelism: 1,
});
await db.users.update({ id }, { password_hash: hash });

// Verify on login — constant-time comparison built in
const ok = await argon2.verify(stored.password_hash, attempt);

// Re-hash if params changed (e.g. you raised memoryCost)
if (ok && argon2.needsRehash(stored.password_hash, { memoryCost: 64 * 1024 })) {
    const fresh = await argon2.hash(attempt, { memoryCost: 64 * 1024 });
    await db.users.update({ id }, { password_hash: fresh });
}

// 2) Python — argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(memory_cost=19 * 1024, time_cost=2, parallelism=1)
hashed = ph.hash(password)
try:
    ph.verify(hashed, attempt)
    if ph.check_needs_rehash(hashed):
        new_hash = ph.hash(attempt)
except VerifyMismatchError:
    raise InvalidLogin()

// 3) Why NOT a plain hash
//   SHA-256(pw) is ~10ns. A laptop GPU brute-forces 10^10 candidates per second.
//   argon2id at memoryCost=19MB takes ~250ms. Same GPU: 4 attempts/sec.

// 4) Length requirements (NIST SP 800-63B)
//   Minimum 8 chars, allow up to 64+, allow all printable Unicode,
//   prohibit common passwords (top 100k), DO NOT require complex composition rules.

// 5) Check against the leaks corpus — HaveIBeenPwned range API
import crypto from 'crypto';
const sha1   = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const r = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const found = (await r.text()).split('\n').some(line => line.startsWith(suffix));
if (found) throw new Error('Password is in a known breach corpus');

// 6) Account-takeover defences AROUND the password
//   • Rate limit /login per IP + per account
//   • Lock account / require captcha after N failures
//   • Email on new device / IP / failed attempts
//   • MFA (TOTP, WebAuthn) for sensitive accounts
//   • Never log passwords (even hashed) in request logs

// 7) When migrating from an old hash
//   Verify with the old hash; if successful, re-hash with argon2id and update.
//   Run for 6-12 months; force a password reset for users who haven't logged in.

Why it matters

Argon2id with OWASP’s 2024 parameters is the right default. Pair it with a HaveIBeenPwned check at signup, rate-limited login endpoints, and optional MFA — that combination locks down 99% of account-takeover attempts.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Use a slow, memory-hard KDF, not a hash.
import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id });
await argon2.verify(hash, password);
// bcrypt cost 12+ is acceptable on legacy stacks.
Try it Yourself »

Exercise

Preferred password-hashing variant of Argon2.

argon2.hash(pw, { type: argon2. });

Test yourself

Q1. Passwords should be hashed with…
Q2. A common safe default today is…
Q3. Salt should be…

Discussion

Loading…