Password Security Best Practices
How passwords actually get cracked, what genuinely makes them strong, and what you should do about it — whether you're a developer storing credentials or just trying to keep your accounts safe.
How Passwords Actually Get Cracked
Before fixing the problem, it's worth understanding the threat. Most people imagine password cracking as some hacker hunched over a terminal trying combinations. Reality is more mundane and more effective.
Brute Force
The blunt approach: try every possible combination. An 8-character lowercase password has 268 ≈ 200 billion combinations. On a modern GPU rig running hashcat, you can test billions of hashes per second. That 8-character password falls in minutes. Throw in uppercase and numbers, it might take days. Still not enough.
# Approximate crack times at 10 billion hashes/sec (MD5 on a GPU farm)
8 chars, lowercase only: < 1 minute
8 chars, mixed case + digits: ~1 hour
8 chars, full charset (94): ~1 day
12 chars, full charset: ~centuries
16 chars, full charset: heat death of the universeLength is your strongest defense against brute force. Every character you add multiplies the search space exponentially.
Dictionary Attacks
Nobody starts with brute force. Attackers use wordlists — huge files containing millions of common passwords, words from every language, and known leaked passwords. The rockyou.txt list, leaked in 2009, has 14 million passwords. Modern lists have billions of entries with mutations baked in: password, Password, p@ssw0rd, P@$$w0rd, password1!.
If your password looks like a real word with substitutions, it's probably in the list. Crackers run these mutations automatically — testing 3 for e, @ for a, ! as a suffix, capitalized first letter, year appended. What you think is clever is table stakes for any cracker.
Rainbow Tables
Before salting became universal, rainbow tables were the attack of choice. They're precomputed lookup tables mapping hash values back to their original inputs. If a site stored MD5("password123") = 482c811da5d5b4bc6d497ffa98491e38, an attacker just looks up that hash value in the table and instantly gets the plaintext.
The defense is salting: before hashing, append a random string unique to each user. Now even if two users have the same password, their hashes differ. Rainbow tables become useless because precomputing for every possible salt is infeasible.
# Without salt — rainbow table attack works
hash("password123") → 482c811d... → lookup in table → "password123"
# With salt — each user gets a unique random value
salt = "x7Km9pQ2"
hash("password123" + salt) → 9f4c2a1b... → not in any tableCredential Stuffing
The most common attack today isn't technical at all. Attackers buy or download lists of leaked username/password pairs from breached sites, then try those exact credentials on other services. If you use the same password on Gmail and your bank, and your email is in the LinkedIn breach, your bank account is at risk. This is why password reuse is a bigger problem than weak passwords.
What Actually Makes a Password Strong
Two things matter above everything else: length and uniqueness. Complexity (symbols, mixed case) helps, but it's a multiplier on length, not a substitute for it.
| Factor | Weak | Strong |
|---|---|---|
| Length | 8 characters or fewer | 16+ characters |
| Character pool | Only lowercase letters | Mixed case, digits, symbols |
| Predictability | Dictionary words, names, dates | Truly random characters |
| Uniqueness | Same password reused | Different password per site |
Password Strength Examples
password123 | Dead on Arrival | In every wordlist ever made |
P@ssw0rd! | Still Terrible | Mutation of "password" — in every cracker's ruleset |
J0hn$m1th2024! | Weak | Personal info + predictable substitutions |
correct-horse-battery-staple | Decent | Long passphrase, but famous — use your own random words |
Kj#9xM$pL2@nQ5wR | Strong | Randomly generated — requires a password manager |
Generate properly random passwords with our Password Generator. It uses crypto.getRandomValues() — the browser's cryptographically secure RNG — and nothing leaves your device.
Entropy: The Math Behind It
Entropy quantifies how unpredictable a password is, measured in bits. Each bit doubles the search space. 40 bits means 240 ≈ 1 trillion possible values. 80 bits means 280 ≈ 1.2 quintillion.
Entropy = length × log₂(character_pool_size)
Character pool sizes:
Lowercase only (a-z): 26 chars → 4.7 bits/char
Lower + upper: 52 chars → 5.7 bits/char
Lower + upper + digits: 62 chars → 5.95 bits/char
Full printable ASCII (94 chars): 94 chars → 6.55 bits/char
Examples:
8 chars, full ASCII: 8 × 6.55 = ~52 bits (crackable)
12 chars, full ASCII: 12 × 6.55 = ~79 bits (safe today)
16 chars, full ASCII: 16 × 6.55 = ~105 bits (very safe)
20 chars, full ASCII: 20 × 6.55 = ~131 bits (overkill, but fine)| Entropy | Assessment | Offline crack time (10B/sec) |
|---|---|---|
| <40 bits | Very Weak | Seconds to minutes |
| 40–60 bits | Weak | Hours to months |
| 60–80 bits | Reasonable | Decades |
| 80–100 bits | Strong | Centuries |
| 100+ bits | Very Strong | Cosmological timeframes |
Note: These assume offline cracking against a fast, unsalted hash. Well-implemented bcrypt/Argon2 reduces effective speed to thousands or fewer guesses per second even offline, which changes the calculus significantly.
Password Managers
The single most impactful thing a non-developer can do for their security is start using a password manager. Not because of any one feature — because it enables the one thing that actually matters: unique, random, 20+ character passwords on every single site.
Without a manager, you're choosing between remembering a handful of passwords (and reusing them) or writing them down. With a manager, you remember one strong master password and let software handle the rest.
How They Work
Your password vault is encrypted locally with a key derived from your master password using a slow KDF (key derivation function) like PBKDF2, bcrypt, or Argon2. The encrypted blob is synced to their servers — but they never see your master password, and the blob is useless without it. Even if they're breached, your passwords are safe as long as your master password is strong.
Options Worth Considering
| Manager | Model | Cost | Notes |
|---|---|---|---|
| Bitwarden | Cloud, open source | Free / $10/yr | Audited, self-hostable |
| 1Password | Cloud | $36/yr | Polished UX, family plans |
| KeePassXC | Local file | Free | No cloud, you own the file |
| Apple Passwords | iCloud | Free | Seamless on Apple ecosystem |
| Proton Pass | Cloud | Free / $48/yr | Privacy-focused |
Master Password Rules
This is the one password worth serious thought:
- 20+ characters minimum
- A passphrase you can actually remember — five random words work well
- Never reused anywhere else, ever
- Written down on paper and stored somewhere physically secure (not in a note on your phone)
paypa1.com tricks you, your manager won't autofill your PayPal credentials — it only fills on paypal.com. This is a real, tangible security benefit beyond just storing passwords.For Developers: Storing Passwords Correctly
If you're building anything with user accounts, you need to know this. Plain SHA-256 or MD5 are not acceptable for password storage. These functions are designed to be fast — and fast is the enemy of password security. A modern GPU can compute billions of SHA-256 hashes per second.
bcrypt
bcrypt has been the standard for decades for good reason: it's deliberately slow, includes built-in salting, and has a cost factor you can tune as hardware gets faster.
// Node.js with bcrypt
const bcrypt = require('bcrypt');
// Hash a password (cost factor 12 = ~300ms on typical server)
async function hashPassword(password) {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
// Verify at login
async function verifyPassword(password, storedHash) {
return bcrypt.compare(password, storedHash);
}
// Usage
const hash = await hashPassword('user_password');
// "$2b$12$KIjJ5R3Kcz8M..." — includes salt and cost factor
const valid = await verifyPassword('user_password', hash);
// true# Python with bcrypt
import bcrypt
def hash_password(password: str) -> bytes:
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt)
def verify_password(password: str, hashed: bytes) -> bool:
return bcrypt.checkpw(password.encode(), hashed)Argon2
Argon2 won the Password Hashing Competition in 2015 and is the current best practice. It's memory-hard, meaning it can't be efficiently parallelized on GPUs the way bcrypt can. Use argon2-id (the hybrid variant) for new systems.
// Node.js with argon2
const argon2 = require('argon2');
async function hashPassword(password) {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3, // iterations
parallelism: 4,
});
}
async function verifyPassword(password, hash) {
return argon2.verify(hash, password);
}scrypt
scrypt is another memory-hard alternative, available in Node's built-in crypto module — no external dependencies.
const crypto = require('crypto');
const { promisify } = require('util');
const scrypt = promisify(crypto.scrypt);
async function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex');
const derived = await scrypt(password, salt, 64);
return `${salt}:${derived.toString('hex')}`;
}
async function verifyPassword(password, stored) {
const [salt, hash] = stored.split(':');
const derived = await scrypt(password, salt, 64);
return crypto.timingSafeEqual(
Buffer.from(hash, 'hex'),
derived
);
}Which to Use
| Algorithm | Recommendation | Notes |
|---|---|---|
| Argon2id | Best choice for new code | Memory-hard, PHC winner |
| bcrypt | Good, widely supported | 70-year limit is rarely an issue |
| scrypt | Good alternative | Built into Node.js crypto |
| PBKDF2 | Acceptable with high iterations | Not memory-hard, prefer above |
| SHA-256/SHA-512 | Never for passwords | Too fast |
| MD5 | Never | Broken and too fast |
Set your bcrypt cost factor so hashing takes 200–500ms on your server hardware. When servers get faster, bump the cost factor up and re-hash on next login.
Two-Factor Authentication
A strong password gets you most of the way there. 2FA closes the remaining gap. Even if your password leaks in a breach, an attacker still can't get in without your second factor.
Methods, Best to Worst
- Hardware security keys (FIDO2/WebAuthn): YubiKey, Google Titan, Passkeys on device
Phishing-proof. The key cryptographically verifies the domain, so fake sites get nothing. Best for high-value accounts. - Authenticator apps (TOTP): Authy, Google Authenticator, 1Password built-in
Time-based 6-digit codes. Secure if you protect the seed backup. Phishable in theory (attacker relays your code in real time) but requires active interception. - Push notifications: Duo, Microsoft Authenticator
Convenient but vulnerable to MFA fatigue attacks — spamming approval requests until the user accidentally approves. - SMS codes:
Better than nothing, but SIM swap attacks are real and not rare. If you have a choice, use an app instead.
Priority Order for Enabling 2FA
Can't do everything at once? Start here:
- Your email (especially recovery email — controls everything else)
- Your password manager
- Banking and financial accounts
- Work accounts (SSO, GitHub, cloud providers)
- Social media
Breach Checking
Billions of credentials have been leaked in data breaches over the past two decades. Your email address and possibly your passwords are sitting in databases being traded on forums right now.
Have I Been Pwned (HIBP) by Troy Hunt is the standard resource. It indexes breached databases and lets you check whether your email appears in any of them. The Pwned Passwords API lets you check whether a specific password has ever appeared in a breach — it does this cleverly using k-anonymity so your actual password never leaves your machine.
# k-anonymity API: only sends first 5 chars of SHA-1 hash
# Your actual password never sent to HIBP servers
import hashlib, requests
def check_password_breach(password):
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
hashes = response.text.splitlines()
for line in hashes:
h, count = line.split(':')
if h == suffix:
return int(count) # Times seen in breaches
return 0 # Not found
count = check_password_breach("password123")
# Returns 9,659,354 — don't use this passwordMany password managers (Bitwarden, 1Password) have HIBP integration built in and will alert you when saved credentials appear in new breaches.
Passkeys: The Future Without Passwords
Passkeys are the endgame for authentication. They replace passwords entirely with public-key cryptography, backed by your device's biometric or PIN unlock.
Here's how they work: when you register, your device generates a keypair. The private key never leaves your device (stored in secure hardware like Apple's Secure Enclave). The site gets the public key. When you log in, the site sends a challenge, your device signs it with the private key (after you verify with Face ID / fingerprint / PIN), and that's it — no password transmitted, nothing to phish, nothing to breach.
Why Passkeys Are Better
- Phishing-proof: The private key only signs for the exact registered domain
- No password to breach: Nothing to steal from the server side
- Credential stuffing impossible: Nothing to stuff
- No password to forget: Just use your biometric
Current State
As of early 2026, passkeys are supported by Apple, Google, Microsoft, and most major browsers. Adoption is growing quickly — Google, GitHub, 1Password, PayPal, Amazon, and hundreds of other services support them. The FIDO2/WebAuthn spec is the underlying standard.
// WebAuthn registration (server-side simplified)
const publicKeyOptions = {
challenge: crypto.getRandomValues(new Uint8Array(32)),
rp: { name: "Your App", id: "yourapp.com" },
user: {
id: Uint8Array.from(userId, c => c.charCodeAt(0)),
name: userEmail,
displayName: userName,
},
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
},
timeout: 60000,
};
const credential = await navigator.credentials.create({
publicKey: publicKeyOptions,
});If you're building new authentication systems today, implement passkeys. Keep passwords as a fallback for devices that don't support them yet, but design around passkeys as the primary path.
Tools
Password Generator
Cryptographically random passwords with configurable length, character sets, and entropy display. Everything runs in your browser.
Generate PasswordHave I Been Pwned
Check if your email appears in known data breaches. Updated continuously as new breaches are discovered.
Check BreachesSummary Checklist
Do
- Use 16+ character randomly generated passwords
- Use a unique password for every account
- Use a reputable password manager
- Enable 2FA — hardware key first, then TOTP app
- Use Argon2id or bcrypt for password storage (developers)
- Set up HIBP monitoring for your email
- Enable passkeys where supported
Don't
- Reuse passwords across sites
- Use personal information in passwords
- Rely on character substitutions (@ for a, etc.)
- Change passwords on a fixed schedule (change on breach)
- Store passwords in plain text or SHA-256
- Skip 2FA because it's inconvenient