100% Private

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 universe

Length 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 table

Credential 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.

FactorWeakStrong
Length8 characters or fewer16+ characters
Character poolOnly lowercase lettersMixed case, digits, symbols
PredictabilityDictionary words, names, datesTruly random characters
UniquenessSame password reusedDifferent password per site

Password Strength Examples

password123Dead on ArrivalIn every wordlist ever made
P@ssw0rd!Still TerribleMutation of "password" — in every cracker's ruleset
J0hn$m1th2024!WeakPersonal info + predictable substitutions
correct-horse-battery-stapleDecentLong passphrase, but famous — use your own random words
Kj#9xM$pL2@nQ5wRStrongRandomly 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)
EntropyAssessmentOffline crack time (10B/sec)
<40 bitsVery WeakSeconds to minutes
40–60 bitsWeakHours to months
60–80 bitsReasonableDecades
80–100 bitsStrongCenturies
100+ bitsVery StrongCosmological 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

ManagerModelCostNotes
BitwardenCloud, open sourceFree / $10/yrAudited, self-hostable
1PasswordCloud$36/yrPolished UX, family plans
KeePassXCLocal fileFreeNo cloud, you own the file
Apple PasswordsiCloudFreeSeamless on Apple ecosystem
Proton PassCloudFree / $48/yrPrivacy-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)
Phishing protection bonus: Password managers autofill based on exact domain match. If a phishing site at 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

AlgorithmRecommendationNotes
Argon2idBest choice for new codeMemory-hard, PHC winner
bcryptGood, widely supported70-year limit is rarely an issue
scryptGood alternativeBuilt into Node.js crypto
PBKDF2Acceptable with high iterationsNot memory-hard, prefer above
SHA-256/SHA-512Never for passwordsToo fast
MD5NeverBroken 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

  1. 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.
  2. 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.
  3. Push notifications: Duo, Microsoft Authenticator
    Convenient but vulnerable to MFA fatigue attacks — spamming approval requests until the user accidentally approves.
  4. 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:

  1. Your email (especially recovery email — controls everything else)
  2. Your password manager
  3. Banking and financial accounts
  4. Work accounts (SSO, GitHub, cloud providers)
  5. 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 password

Many 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 Password
Have I Been Pwned

Check if your email appears in known data breaches. Updated continuously as new breaches are discovered.

Check Breaches

Summary 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

Last updated: March 2026

The password generator on ToolsDock uses crypto.getRandomValues(). Nothing is sent to any server.

Privacy Notice: This site works entirely in your browser. We don't collect or store your data. Optional analytics help us improve the site. You can deny without affecting functionality.