Cryptography / Authentication
Password Hashing & Key Derivation
Passwords are low-entropy user secrets highly vulnerable to offline dictionary and GPU brute-force attacks. Secure password storage requires specialized, computationally expensive Password-Based Key Derivation Functions (PBKDFs) that incorporate unique per-stored-hash salts and tunable cost parameters to raise the cost of offline cracking, per NIST SP 800-63B-4. The outcome still depends on the chosen parameters, the password's entropy, and the attacker's hardware, so no PBKDF makes an arbitrarily weak password uncrackable. Memory-hard functions such as Argon2id additionally impose memory cost for stronger GPU/ASIC resistance where the deployment can afford it, while accepted constructions also include time-cost-only functions such as PBKDF2 and CPU-bound ones such as bcrypt.
Why Plain Cryptographic Hashes Fail for Passwords
Fast general-purpose hash functions such as SHA-256 and MD5 can be evaluated massively in parallel, making weak passwords vulnerable to rapid offline guessing. Password-hashing functions deliberately impose computational cost, while memory-hard functions such as Argon2id additionally impose substantial memory requirements.
Modern Password Complexity & Policy Guidelines (NIST SP 800-63B)
Modern password security standards (formalized in NIST SP 800-63B) prioritize length and entropy over arbitrary complexity rules. Traditional policies forcing mixed casing, digits, and symbols are deprecated because they result in predictable user behavior (e.g., capitalizing the first letter and appending 1!) while making passwords harder to remember.
NIST SP 800-63B Core Authentication Requirements
- Length-Based Security:
- Enforce a minimum length of at least 15 characters for single-factor password authentication per NIST SP 800-63B-4 (an 8-character minimum is permitted only when the password is used as one factor alongside an independent authenticator in MFA; 16+ characters recommended as an organizational best practice for administrative or privileged accounts).
- Allow maximum lengths of at least 64 characters to support easy-to-remember, high-entropy passphrases (e.g.,
correct horse battery staple).
- Eliminate Arbitrary Complexity Rules:
- Deprecate rules requiring specific character mixes (uppercase, numbers, symbols) to improve user adoption and key diversity.
- Ban Periodic Expiry / Forced Rotation:
- Do not force users to change passwords periodically (e.g., every 90 days) unless a compromise is active. Forced rotation results in users selecting weaker, sequential passwords (e.g.,
Winter2025!toSpring2025!).
- Do not force users to change passwords periodically (e.g., every 90 days) unless a compromise is active. Forced rotation results in users selecting weaker, sequential passwords (e.g.,
- Enforce Breached Password Blacklisting:
- NIST SP 800-63B-4 requires checking new passwords against more than just breach lists — it also calls for rejecting commonly used, expected, and context-specific passwords (e.g., the service name, the username, or other account-specific dictionary words), not only credentials from known compromised databases (breach lists like HaveIBeenPwned).
- Deprecate Security Questions / Hints:
- Knowledge-based authentication (KBA) questions (e.g., “What was your first pet’s name?”) are forbidden because answers are easily researched using open-source intelligence (OSINT).
Password Entropy & Search-Space Estimator
Type any password or passphrase below to calculate its idealized character-set search-space upper bound (bits of security) and an estimated brute-force crack time. This math assumes uniform random guessing over the detected character set — real human-chosen passwords (including well-known example passphrases) have much lower effective entropy because dictionary and pattern-based attacks skip most of that idealized search space, so treat the numbers below as an optimistic upper bound, not a real-world guarantee.
Specialized Password Hashing Functions Matrix
| Algorithm | Memory Hardness | GPU / ASIC Resistance | Specification & Recommended Status |
|---|---|---|---|
| Argon2id (RFC 9106) | HIGH (Memory-Hard) | Generally considered strongest of this group when comparably tuned: memory-hardness raises the cost of GPU/ASIC parallelism more directly than CPU-only designs, though actual resistance depends on the chosen parameters and the attacker’s hardware budget, not the algorithm choice alone. | PRIMARY RECOMMENDATION: RFC 9106 / OWASP recommended first-choice algorithm for modern applications (OWASP Cheat Sheet). |
| bcrypt | None (CPU-Hard) | Weaker against custom ASICs than memory-hard designs: its fixed, small memory footprint (4 KiB) is cheap to replicate in dedicated hardware, though it still resists commodity GPU cracking better than an unsalted fast hash. | COMMONLY ACCEPTED LEGACY OPTION: Acceptable where already deployed or required for compatibility; watch out for 72-byte truncation limit. |
| PBKDF2-HMAC-SHA256 | None (CPU-Hard) | Weakest of this group against GPU/ASIC parallelism: a high iteration count (600,000+) raises the cost per guess, but with no memory requirement at all, that cost is easy to parallelize across many cheap cores. | FIPS COMPLIANCE OPTION: FIPS 140-3 itself does not mandate PBKDF2 — it certifies cryptographic modules, not a specific password-hashing choice. Use PBKDF2 (NIST SP 800-132) when your environment requires an algorithm implemented inside a FIPS 140-3 validated module, since Argon2id, bcrypt, and scrypt are not currently NIST-approved primitives eligible for that validation. |
| scrypt (RFC 7914) | MODERATE | An earlier memory-hard design, predating Argon2id’s hybrid approach: scrypt defines independent cost parameters (N for CPU/memory cost, r for block size, and p for parallelization), but its memory-access pattern is data-dependent, unlike Argon2id’s hybrid data-dependent/data-independent design. Argon2id was engineered and vetted through the Password Hashing Competition to balance GPU/ASIC resistance with side-channel resistance; this is why RFC 9106 and OWASP recommend it first, not because scrypt lacks tunable parameters. |
ACCEPTABLE FALLBACK: A reasonable choice when Argon2id isn’t available in your stack. |
Salting & Peppering Architecture
1. Unique Salt Per Stored Hash (Public Metadata)
A Salt is a CSPRNG-generated random sequence, generated uniquely per stored password hash — a fresh salt accompanies each newly generated hash, including a rehash after a password change, not just once per account for its lifetime — and stored alongside the hash digest in cleartext; 16 bytes (128 bits) is a common, sound choice, but the required length depends on the specific password-hashing construction in use (e.g., Argon2id’s recommended salt length differs from bcrypt’s fixed input) rather than being a universal fixed value. Salting enforces two critical controls:
- Defeats Precomputed Rainbow Tables: A rainbow table built before the salt is known can’t match any of these hashes — the attacker would need a separate precomputed table per salt value, which defeats the entire point of precomputing a table in advance.
- Prevents Duplicate Hash Discovery: Two users sharing the identical password
"Password123!"yield completely different hash digests.
2. Secret Pepper (KMS Custody)
A Pepper is a secret key stored outside the primary user database (e.g., inside an AWS KMS or HSM); 32 bytes (256 bits) is a common, sound choice, but the required length depends on the specific pepper mechanism (a raw HMAC key, an AES key wrapping a per-user value, etc.) rather than being a universal fixed value. The application combines the pepper with the salted password prior to hashing. If an adversary’s breach is scoped to a SQL database dump alone (e.g., via SQL injection) and doesn’t also expose the pepper, they cannot perform offline cracking without it — but that protection depends on the breach genuinely not reaching the pepper; an attacker with broader access (application-server compromise, KMS misconfiguration) that reaches both the database and the pepper isn’t stopped by this control.
| Security Control | Storage Location | Entropy Source | Primary Attack Mitigated |
|---|---|---|---|
| Pepper (Secret Key) | KMS / HSM / Secret Manager | 256-bit CSPRNG secret | Database exfiltration & offline GPU cracking. |
| Salt (Public Metadata) | Database table alongside hash | 128-bit CSPRNG per stored hash | Precomputed Rainbow Tables & cross-user hash matching. |
Argon2id Recommended Parameters (RFC 9106)
Specified in RFC 9106 and recommended by OWASP, Argon2id is a memory-hard password-hashing function designed to resist parallel cracking while balancing side-channel considerations. The current OWASP Password Storage Cheat Sheet lists five single-lane (p=1) configurations OWASP states provide an equal level of defense, trading memory for time cost rather than ranking strongest-to-weakest: m=47104 KiB (46 MiB), t=1; m=19456 KiB (19 MiB), t=2; m=12288 KiB (12 MiB), t=3; m=9216 KiB (9 MiB), t=4; and m=7168 KiB (7 MiB), t=5 — pick based on the memory your deployment can dedicate per concurrent hash operation, since under-provisioning memory on a busy auth endpoint can matter as much as the specific numbers chosen. RFC 9106 separately recommends m=65536 KiB (64 MiB), t=3, p=4 as its own memory-constrained profile (the tuner’s default below), which spreads the work across four parallel lanes rather than OWASP’s single lane:
Argon2id Cost Estimator & Parameter Tuner
Adjust memory, time, and thread parameters to generate and copy a Node.js Argon2 config that meets OWASP or RFC 9106 guidance — the right values depend on your deployment's available memory and request volume, not a single universally "optimal" setting.
The Bcrypt 72-Byte Truncation Limit Pitfall
The traditional (OpenBSD-style) bcrypt algorithm silently truncates input password strings at 72 bytes, ignoring any characters beyond that during authentication — but this is implementation-dependent, not a universal behavior of every bcrypt library. For example, current versions of the widely used pyca/bcrypt Python library raise a ValueError on a password longer than 72 bytes instead of truncating it. Verify your specific implementation's actual behavior rather than assuming silent truncation.
A tempting mitigation is to pre-hash long passwords with plain SHA-256(password) — producing a fixed 32-byte digest — before passing them to bcrypt. The OWASP Password Storage Cheat Sheet advises against this because of a technique known as password shucking — but the risk isn't that possessing the bcrypt(SHA-256(password)) database alone lets an attacker skip bcrypt's cost. Attacking that stored value directly still requires one full bcrypt computation per guess, exactly as intended, regardless of what the inner hash is. The actual danger is cross-referencing: because SHA-256(password) is fast, unkeyed, and produces the same output for the same password everywhere it's used, if that intermediate digest ever becomes independently known — leaked from a different breach that stored it raw, exposed by a debug log, a caching layer, or another service using the identical pre-hash-then-store pattern — an attacker can feed that leaked SHA-256 value directly into bcrypt and compare the result to your stored hash, with a single bcrypt operation. They do not need to first crack that digest back to the plaintext password to make the comparison — simply knowing the intermediate hash value is enough to confirm the match. That direct-comparison shortcut is what "shucks off" the bcrypt shell, and it works with or without the attacker ever recovering the literal password. Without that separately-leaked intermediate digest, unkeyed SHA-256 pre-hashing on its own doesn't hand an attacker a shortcut against the bcrypt-protected database — attacking the bcrypt hash directly still costs one full bcrypt computation per guess.
OWASP's recommended construction instead uses a keyed pre-hash: bcrypt(base64(HMAC-SHA-384(key=pepper, data=password)), salt, cost). Because HMAC-SHA-384 is keyed with a secret pepper (see "Salting & Peppering Architecture" above), an attacker without that pepper cannot reuse generic public SHA-2 cracking infrastructure against the pre-hash at all — they would first need the pepper itself, which is why the pepper should live in KMS/HSM custody separate from the password database. Base64-encoding the HMAC output (rather than feeding bcrypt the raw binary digest) also avoids embedded null bytes, which some bcrypt implementations treat as a C-style string terminator and truncate on.
Pepper-management implications: this construction only helps if the pepper stays secret and available. Plan for pepper rotation (version peppers so old hashes can still be verified during a rotation window, then rehash on next login), a pepper backup/recovery strategy (losing the pepper makes every stored hash unverifiable — unlike a compromised salt, which only affects the one stored hash it belongs to), and awareness that a single shared pepper is a single point of failure: its compromise affects the whole user base at once, which is why it belongs in a KMS/HSM rather than application config.
Password Lifecycle Beyond Initial Storage
Choosing a hashing algorithm is only the starting point — the hash’s parameters and the surrounding authentication flow need ongoing maintenance:
- Rehash-on-login: Because you don’t have the plaintext password to re-hash a user’s credential in bulk, cost-parameter upgrades (bumping Argon2id’s memory or time cost, or migrating between algorithms) roll out gradually: check the stored hash’s embedded parameters at every successful login, and if they’re below the current target, re-hash the just-verified plaintext with the new parameters and overwrite the stored value. Inactive accounts stay on old parameters until they next log in — an accepted trade-off, not an oversight.
- Legacy-hash migration: The same rehash-on-login mechanism handles migrating between algorithms entirely (e.g., an old system storing plain salted SHA-256, or MD5, moving to Argon2id): verify against the legacy scheme on login, and on success, immediately compute and store the new Argon2id hash of the same plaintext. Accounts that never log back in either get force-reset or stay flagged as needing migration — silently leaving them on a broken legacy scheme indefinitely is the failure mode to design against.
- Unicode handling: Password inputs should be normalized (typically Unicode NFC) before hashing, and the byte encoding must be both consistent and well-defined (UTF-8 is standard) — an application that hashes raw, unnormalized bytes risks legitimate users being locked out when the same password is typed on a different keyboard layout, IME, or OS that produces a different Unicode normalization form for visually identical text. This is a different notion of “length” from the general password-policy minimum/maximum discussed earlier on this page, which NIST SP 800-63B-4 requires counting in Unicode code points, not bytes. The byte-based caveat applies specifically to algorithm-level input ceilings, like bcrypt’s 72-byte limit (see above): multi-byte UTF-8 sequences can hit that truncation boundary well before a naive code-point count would suggest, but that’s an algorithm implementation detail, not a general password-policy length rule.
- Authentication DoS limits: Memory-hard functions are expensive by design — that’s the point against attackers, but it also means a login endpoint doing Argon2id verification on every request is a much easier target for a resource-exhaustion DoS than a stateless endpoint. Rate-limit authentication attempts per account and per source, and size Argon2id’s memory/time parameters with your expected peak concurrent-login load in mind, not just against offline-cracking resistance in isolation — the parameters that are “strong” against an offline attacker can still be tuned so a login storm doesn’t exhaust server memory.
- Privacy-preserving breached-password checks: NIST SP 800-63B-4’s breached-password blacklisting requirement (see above) shouldn’t be implemented by sending plaintext passwords to a third-party API. The Have I Been Pwned Pwned Passwords API supports a k-anonymity model: the client hashes the candidate password with SHA-1, sends only the first 5 hex characters of the digest, and receives back all breached-password suffixes sharing that prefix — the client checks the full digest locally, so the full password (or its full hash) never leaves the client/server boundary being checked.
Argon2id is the primary recommendation for password storage, memory-hard against GPU/ASIC cracking; a unique per-hash salt prevents rainbow tables, and an HSM/KMS-held pepper adds protection specifically when a breach is scoped to the database alone. Rehash-on-login is how cost parameters and legacy algorithms get upgraded gradually, since bulk re-hashing isn't possible without the plaintext.
Primary references
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications — IETF RFC 9106
- NIST SP 800-63B-4: Digital Identity Guidelines: Authentication and Lifecycle Management — NIST CSRC SP 800-63B-4
- OWASP Password Storage Cheat Sheet: Current Argon2id, bcrypt, and PBKDF2 parameter recommendations — OWASP Cheat Sheet Series