Why "Password Complexity Rules" Are Wrong (NIST SP 800-63B)
For decades, the standard advice for passwords was: at least one uppercase, one lowercase, one digit, one special character; force a change every 90 days; do not allow reuse of the last 5. Most enterprise IT departments still mandate exactly this. NIST — the U.S. National Institute of Standards and Technology, the agency that defines federal cybersecurity guidance — published Special Publication 800-63B in 2017 with a major revision in 2024 that explicitly says: stop doing all of that.
The reasoning, after measuring real-world outcomes, is that complexity rules and forced rotation make passwords worse, not better. When users are forced to satisfy "must have uppercase + digit + special", the dominant outcome is a predictable transformation — Password1!, Password2!, Password3! — that human-derived guessing tools enumerate trivially. When forced to rotate every 90 days, users append a counter to whatever their previous password was. The "complexity" appears to satisfy a regex, but the entropy gain over the equivalent unconstrained password is essentially zero, while user friction skyrockets and the password gets reused or written on a sticky note.
NIST SP 800-63B, Section 5.1.1.2 (memorized secrets), now reads: "Verifiers SHOULD NOT impose other composition rules (e.g., requiring mixtures of different character types) for memorized secrets. Verifiers SHOULD NOT require memorized secrets to be changed arbitrarily (e.g., periodically)." Changes are required only on evidence of compromise. The minimum length is 8 (or 6 for randomly-generated), the maximum SHALL be at least 64, and ALL printable ASCII and Unicode characters MUST be permitted. The character class restriction is gone.
What replaces complexity rules is a denylist. NIST mandates that passwords be checked against a list of known-compromised values: passwords that have appeared in breach corpuses, dictionary words, repetitive sequences (aaaaaaaa, qwerty), and context-specific strings (the username, the site name). The free service Have I Been Pwned exposes an API that you can integrate using k-anonymity (send the first 5 characters of a SHA-1 hash, get back all hashes with that prefix, check locally) — millions of services use this in 2026.
The practical user advice that drops out of all this: long random passwords (16+ characters from a generator), one per site, stored in a password manager. Or memorable passphrases (4+ random English words from a list, ~50+ bits of entropy). Stop forcing rotation. Stop blocking emoji or Unicode. Stop forcing a special character. Block the obviously-bad and let users choose their own length and shape past that.
// HIBP k-anonymity check (no full hash leaves your server)
async function checkPwned(password) {
const hash = sha1Hex(password).toUpperCase();
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const res = await fetch('https://api.pwnedpasswords.com/range/' + prefix);
const text = await res.text();
for (const line of text.split('\n')) {
const [s, count] = line.trim().split(':');
if (s === suffix) return parseInt(count, 10); // # of breaches
}
return 0;
}
// 2026 sign-up flow per NIST SP 800-63B
async function validateSignup(password) {
if (password.length < 8) return 'too short';
if (password.length > 64) return 'too long';
if (/^(.)\1{4,}$/.test(password)) return 'too repetitive';
const breached = await checkPwned(password);
if (breached > 0) return `appeared in ${breached} breaches`;
return null; // OK — no character-class checks
}
Entropy Math — How Long Should a Password Be?
Password strength is quantified by Shannon entropy: H = log2(N^L), where N is the alphabet size and L is the length. The unit is bits, and each bit doubles the average number of guesses an attacker needs. A password drawn uniformly at random from a 26-letter alphabet at length 10 has 26^10 ≈ 1.4 × 10^14 possible values, or log2(1.4 × 10^14) ≈ 47 bits of entropy. The same length over a 95-character printable ASCII alphabet is 95^10 ≈ 6 × 10^19, or about 66 bits.
The key word is "uniformly at random." Human-chosen passwords have far less entropy than the alphabet suggests, because humans pick from a tiny fraction of the space. Studies of leaked password corpora (RockYou, LinkedIn, Adobe) consistently estimate human-chosen 8-character passwords at 18–22 bits of effective entropy — about 4 to 8 million guesses to crack at 50% probability, fully within reach of a single GPU in seconds. The same character count from a generator is 50+ bits, or about 10^15 guesses — measured in years on dedicated hardware.
Modern attacker capabilities, as a calibration: a single Nvidia RTX 4090 in 2026 computes about 200 billion bcrypt-cost-5 hashes per day. A small cluster of 100 GPUs reaches 20 trillion. Cloud-rented hashcat clusters can do orders of magnitude more for a few hours. The protective work factor of the password hashing algorithm matters enormously — at bcrypt cost 12 (currently OWASP-recommended), the same hardware does about 80 thousand hashes per day, so an attacker brute-forcing requires ~250 million times longer than at cost 5. This is why the choice of password hash is at least as important as the length of the password.
Operational guidance for 2026, given typical Argon2id work factors: 12 random characters from a 95-symbol alphabet is about 79 bits of entropy and effectively uncrackable for the next decade. 16 characters is ~105 bits and uncrackable for the foreseeable future even against state-actor budgets. For passphrases, 4 random words from a 7,776-word list (the EFF wordlist) is log2(7776^4) ≈ 51 bits — comparable to a 12-char password but vastly more memorable. 5 words is 65 bits, the practical sweet spot for human-memorable use.
A subtle trap: entropy assumes uniform random selection. If your generator uses Math.random(), the entropy claim is a lie — Math.random() in V8 is xorshift128+, which produces predictable output if the attacker captures a few outputs. Always use crypto.getRandomValues(new Uint32Array(...)) (browser) or crypto.randomBytes(...) (Node) for any password-class output. The difference is the difference between 79 bits of entropy and 0.
// Entropy table (assumes uniform random)
//
// alphabet | length | bits | crack time at 1e9 guesses/sec
// --------- | ------ | ----- | -----------------------------
// 26 | 8 | 37.6 | 2 minutes
// 62 | 8 | 47.6 | 1 day
// 95 | 8 | 52.5 | ~50 days
// 95 | 12 | 78.7 | ~10 million years
// 95 | 16 | 105.0 | ~10^15 years (heat death of sun)
//
// — and remember: real cracking goes through Argon2id which is
// ~10^6 to ~10^9 times slower than raw hashing.
// CORRECT: cryptographic random
function generate(length, alphabet) {
const out = new Array(length);
const r = new Uint32Array(length);
crypto.getRandomValues(r);
for (let i = 0; i < length; i++) out[i] = alphabet[r[i] % alphabet.length];
return out.join('');
}
// WRONG: Math.random — predictable, NOT for passwords
function bad(length) {
let s = '';
for (let i = 0; i < length; i++) s += String.fromCharCode(97 + Math.floor(Math.random() * 26));
return s;
}
// EFF passphrase
import wordlist from './eff_large_wordlist.json'; // 7776 words
function passphrase(n = 5) {
const r = new Uint32Array(n);
crypto.getRandomValues(r);
return [...r].map(x => wordlist[x % 7776]).join('-');
}
Passkeys, FIDO2, WebAuthn — The Post-Password World
Passwords have a structural problem that no amount of length or hashing can fix: they are bearer secrets. The user knows them, the server stores a hash of them, the network carries them. Anywhere that secret exists, it can be phished, breached, intercepted, or replayed. Passkeys are the industry's coordinated answer.
A passkey is a public-private key pair, generated by the user's device and stored in the device's secure enclave (Apple Secure Enclave, Android StrongBox, Windows Hello TPM, hardware FIDO2 keys like YubiKey). At sign-up, the device generates the key pair and sends only the public key to the server. At sign-in, the server sends a random challenge, the device signs the challenge with the private key, the server verifies with the public key. The private key never leaves the device. The challenge changes every login. There is no shared secret to phish.
The standards are: FIDO2 (the umbrella from the FIDO Alliance), WebAuthn (the W3C browser API, Recommendation status since 2019), and CTAP2 (the protocol between browser and authenticator hardware). Apple, Google, Microsoft, and Mozilla shipped synchronized passkey support in 2022–2023, with Apple iCloud Keychain and Google Password Manager replicating passkeys across a user's devices using end-to-end encryption. As of 2026, every major site (Google, Apple, Microsoft, GitHub, PayPal, Amazon, Shopify, etc.) supports passkey-only sign-in.
The phishing-resistance is structural. A WebAuthn signature includes the relying party identifier (the domain name), and the browser will refuse to use a passkey on the wrong origin. A user tricked into typing their Google credentials into google.com.attacker.example will not be able to use their Google passkey there — the browser checks the origin and the device refuses to sign. This is why FIDO is the single most effective anti-phishing technology shipped to consumers.
Implementation pattern (server side): on signup, accept a WebAuthn registration response, verify the attestation, store the public key + credential ID + counter for that user. On signin, generate a random challenge, send it to the client, receive back the signed assertion, verify the signature against the stored public key, check the counter is monotonically increasing (resists cloned authenticators), check the origin matches your domain. Libraries: @simplewebauthn/server (Node), webauthn-rs (Rust), java-webauthn-server (Java).
The transition pattern most apps follow in 2026: keep password sign-in for backwards compatibility, but on first sign-in offer to register a passkey, then prefer the passkey on subsequent logins. This is what GitHub does. Within 12–18 months of offering passkeys, leading sites report 60–80% of active users have at least one passkey enrolled, and password use declines accordingly. The end-state is "passkey by default, password as fallback for migration cases" and ultimately password-free authentication for new accounts.
A password generator produces a cryptographically random string using your chosen length and character classes — uppercase, lowercase, digits, and symbols. Random passwords are dramatically harder to guess than human-chosen ones, so generators paired with a password manager are the modern best practice. The tool runs entirely in your browser using the Web Crypto API.
How to use
Set the desired length — 16 characters or more is recommended.
Toggle the character classes you want included.
Optionally exclude lookalike characters (l/1/I, O/0) for safer manual transcription.
Click Generate to produce a new password.
Copy the result and store it in a password manager — never write it down or paste it into chat.
Common use cases
Creating a unique password for a new website or service.
Rotating service credentials after an incident or staff change.
Generating an API token or webhook secret stored in a vault.
Producing a temporary password for a shared device or one-off account.
Seeding a CI/CD pipeline secret that will be encrypted at rest.
Issuing a transient guest Wi-Fi password.
Frequently asked questions
Q. How long should a password be?
A. For most accounts, 16+ random characters is overkill in a good way. The bigger threat today is reuse and phishing, not raw length.
Q. Are these passwords cryptographically secure?
A. Yes. The generator uses crypto.getRandomValues from the Web Crypto API, which is designed to be cryptographically strong, not Math.random.
Q. Should I memorise my passwords?
A. Memorise one strong master password for your password manager. Let the manager generate and remember everything else.
Q. Why exclude similar characters?
A. Optional, but useful when humans must transcribe a password from a screen — l, 1, I, 0, O are easy to confuse. Skip the option for any password stored only in a manager.