TL;DR
- Never store plaintext or fast hashes (MD5/SHA‑1/SHA‑256 alone). Use Argon2id (preferred) or bcrypt with a unique salt per user.
- Tune cost so verify takes ~50–200ms on your hardware. Re‑benchmark over time.
- Keep an optional pepper in KMS/secret manager, not in the DB or code.
- Migrate legacy hashes on login; rehash to stronger params when
needs_upgrade. - Add rate limiting, progressive delays, and encourage MFA—hashing isn’t your only control.
1) What to store (PHC string format)
Store the entire encoded hash (algorithm + params + salt + hash). Examples:
$argon2id$v=19$m=65536,t=3,p=1$BASE64_SALT$BASE64_HASH
$2b$12$Cj0qQJYbC7...$V4vSg7iJbT2mY...
Why: the verifier can read params and decide whether to rehash with newer settings.
2) Recommended algorithms & parameters
- Argon2id — memory‑hard (resists GPU/ASIC cracking). Start around:
memoryCost: 64–256 MiB,timeCost: 2–4,parallelism: 1–4.
- bcrypt — fine if widely supported in your stack. Set cost (log rounds) so verify ≈ 100ms. Avoid cost < 10 today.
- PBKDF2 — acceptable in regulated stacks; use high iterations (e.g., 310k+) and consider migration.
Pick one algorithm per system; expose a feature flag or config to tweak parameters after benchmarking.
3) Code snippets you can paste
Node (argon2)
import argon2 from "argon2";
// Fetch from KMS/secret manager in prod
const PEPPER = process.env.PEPPER || "";
export async function hashPassword(pw: string): Promise<string> {
return argon2.hash(pw + PEPPER, {
type: argon2.argon2id,
memoryCost: 1 << 16, // 64 MiB
timeCost: 3,
parallelism: 1,
});
}
export async function verifyPassword(pw: string, encoded: string): Promise<boolean> {
return argon2.verify(encoded, pw + PEPPER);
}
// Example "needs upgrade" check — rehash when params too weak
export async function needsUpgrade(encoded: string): Promise<boolean> {
// argon2 doesn't expose built-in needsRehash; parse or store policy version
const desired = { memoryCost: 1 << 16, timeCost: 3, parallelism: 1, type: argon2.argon2id };
// Simple approach: try verify, and rehash when policy version column mismatches
return false; // track via your own policy/version field
}
Python (argon2‑cffi)
from argon2 import PasswordHasher
import os
PEPPER = os.environ.get("PEPPER", "")
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
def hash_password(pw: str) -> str:
return ph.hash(pw + PEPPER)
def verify_password(pw: str, encoded: str) -> bool:
try:
return ph.verify(encoded, pw + PEPPER)
except Exception:
return False
def needs_upgrade(encoded: str) -> bool:
try:
return ph.check_needs_rehash(encoded)
except Exception:
return True
Go (argon2id via x/crypto + scrypt-compatible encoder)
package auth
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/argon2"
"os"
)
var pepper = os.Getenv("PEPPER")
func HashPassword(pw string) (string, error) {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil { return "", err }
// params
m := uint32(64 * 1024) // 64 MiB
t := uint32(3)
p := uint8(1)
hash := argon2.IDKey([]byte(pw+pepper), salt, t, m, p, 32)
// PHC-ish encoding (simplified)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
m, t, p, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash)), nil
}
4) Peppering (optional, but helpful)
- Pepper is a site‑wide secret concatenated with the password before hashing.
- Store it in KMS/secret manager (or HSM), not the DB or repo. Rotate with care—support multiple active peppers for a window so old hashes can still verify.
Verify with multiple peppers (sketch)
const peppers = [PEPPER_CURRENT, PEPPER_OLD].filter(Boolean);
const ok = await Promise.any(peppers.map(p => argon2.verify(encoded, pw + p))).catch(() => false);
5) Migrating legacy hashes (no forced resets)
- Detect algorithm/cost by prefix/column.
- On login, verify with the old algorithm. If valid, rehash with Argon2id/better params and replace the stored hash.
- Mark a
password_hash_versioncolumn so you can find rows still on legacy settings.
Node (bcrypt → argon2id) sketch
import bcrypt from "bcrypt";
import argon2 from "argon2";
async function verifyAndMigrate(pw: string, user) {
const hash = user.password_hash;
const isBcrypt = hash.startsWith("$2a$") || hash.startsWith("$2b$");
let ok = false;
if (isBcrypt) ok = await bcrypt.compare(pw, hash);
else ok = await argon2.verify(hash, pw + PEPPER);
if (!ok) return false;
if (isBcrypt || await needsUpgrade(hash)) {
const newHash = await argon2.hash(pw + PEPPER, { type: argon2.argon2id, memoryCost: 1<<16, timeCost: 3, parallelism: 1 });
await db.users.update(user.id, { password_hash: newHash, password_hash_version: "argon2id_64MB_t3_p1" });
}
return true;
}
6) Operational hygiene (beyond hashing)
- Rate-limit login attempts per IP/user and add progressive delays.
- Detect credential stuffing: many users from one IP / many IPs for one user; throttle & require CAPTCHA/MFA.
- Don’t log raw passwords or full tokens. Redact secrets; use structured logs.
- Password resets: single‑use signed tokens, short TTL (e.g., 15–30 min), invalidate on use. Never email passwords.
- Encourage MFA (TOTP/WebAuthn) and device/session management.
7) Pitfalls & fast fixes
| Pitfall | Why it’s risky | Fix | |---|---|---| | Plaintext, MD5, SHA‑1/256 alone | Fast to crack offline | Use Argon2id or bcrypt | | Shared/global salt | Rainbow‑table friendly | Unique per‑user salt | | Pepper in source code | Repo leak = compromise | Keep pepper in KMS/HSM | | Under‑tuned cost | Cheap brute forcing | Benchmark to ~100ms verify | | Forcing mass resets | UX damage | On‑login migration | | Logging passwords/tokens | Secret leakage | Redact and lock down logs |
Quick checklist
- [ ] Use Argon2id (or bcrypt) with per‑user salt.
- [ ] Tune for ~100ms verify; re‑benchmark annually.
- [ ] Optional pepper stored in KMS; support rotation.
- [ ] Implement on‑login rehash upgrade path.
- [ ] Add rate limiting and promote MFA.
- [ ] Never log secrets; use structured logs and alerts.
One‑minute adoption plan
- Wrap hashing behind an interface; default to Argon2id with benchmarked params.
- Add password_hash_version and needsUpgrade checks.
- Wire on‑login migration from legacy hashes.
- Store pepper in KMS and enable rate limiting + progressive delays.
- Add MFA and a secure reset flow; monitor login anomaly metrics.