Security

How Strong Is My Password? Entropy, Crack Times, and What Meters Miss

2026-05-31 · 9 min read · Generate a strong password free →

You type a password into a registration form and a coloured strength bar appears — green, amber, or red. But what does that bar actually measure? Most password strength meters are broken in ways that matter: they reward patterns that look strong but are cracked in seconds, and they penalise patterns that are genuinely resistant to attack. This guide explains what password strength means mathematically, how crack time is calculated, why common meters fail, and exactly how to generate and measure passwords that hold up against real attacks.

What Password Strength Actually Measures: Entropy

Password strength is fundamentally about entropy — a mathematical measure of unpredictability, expressed in bits. A password's entropy determines how many guesses an attacker would need on average to crack it through brute force. Every additional bit of entropy doubles the required guesses. This exponential relationship explains why increasing password length by a few characters has a dramatically larger effect than adding a special character to a short password.

The entropy formula: entropy = log₂(character_set_size) × length. Consider the numbers: a 12-character password using all printable ASCII characters — 94 options — yields log₂(94) × 12 ≈ 78 bits of entropy. A 20-character password from the same character set yields ≈ 131 bits. At 131 bits, even a hypothetical computer capable of testing one trillion passwords per second would require more time than the current age of the universe to exhaust all possibilities. At 78 bits, the same computer would take about 12 million years — still effectively impossible by brute force alone, but attack strategies are more sophisticated than pure brute force.

The critical nuance: this formula assumes the password is generated with genuine randomness. It breaks down completely for dictionary words, predictable patterns, or any password a human consciously constructed. A password a human chooses is not random — it is constrained by what that human finds memorable — which means its effective entropy is far lower than the formula suggests.

Why Most Password Strength Meters Give Wrong Answers

The standard strength meter algorithm awards points for: having uppercase letters, having lowercase letters, having digits, having symbols, and being longer than some minimum. This produces results that are wrong in both directions. P@ssw0rd1! scores "Strong" or even "Very Strong" on most popular meters. It satisfies every complexity rule. It also appears verbatim in every major breach database ever leaked publicly, and is cracked in under a second by any modern password cracking tool. Meanwhile, correct-horse-battery-staple — made famous by an XKCD comic — scores "Weak" or "Moderate" on many meters despite having meaningfully high entropy from its length.

The reason complexity rules fail: modern password cracking tools do not attempt every possible character combination. They use wordlists of millions of known passwords sourced from data breaches, combined with rule-based transformations. These rules systematically apply substitutions — a to @, o to 0, s to $ — and appending patterns — add ! at the end, add 123, capitalise the first letter. A password like P@ssw0rd1! is caught by exactly these rules and is cracked in milliseconds despite satisfying all complexity requirements. The appearance of randomness is not the same as actual randomness.

The only widely used meter that measures actual crack resistance is zxcvbn, the open-source library developed by Dropbox. It pattern-matches inputs against real breach databases, detects dictionary words and common substitution patterns, and calculates how many guesses a determined attacker would need — accounting for rule-based attacks, not just brute force. This is the only kind of measurement that predicts real-world security.

Crack Time by Entropy Level

The table below shows estimated crack times at 10 billion guesses per second — a realistic offline attack rate on a dedicated cracking rig using a fast hash like MD5. For passwords hashed with bcrypt or Argon2 (as they should be), the cracking rate drops to thousands or millions of guesses per second, adding years to each row.

Entropy (bits)Crack time at 10B/secRatingTypical example
< 28Instant — under a secondTerriblepassword, 1234, qwerty, iloveyou
28 – 35Minutes to hoursWeakmonkey2026, summer1, hello!
36 – 59Days to decadesModerateRandom 8-char, P@ssw0rd-variants
60 – 95Centuries to millenniaStrong14+ char random, 4-word passphrase
96 – 127Millions of yearsVery strong18+ char random, 5-word passphrase
> 128Practically impossibleMaximum20+ char random, 6-word passphrase

Calculating Entropy — Python Implementation

import math

def password_entropy(password: str) -> float:
    """Calculate theoretical entropy in bits based on character set size.
    NOTE: Accurate only for randomly generated passwords, not dictionary words."""
    charset_size = 0
    if any(c.islower() for c in password): charset_size += 26   # a-z
    if any(c.isupper() for c in password): charset_size += 26   # A-Z
    if any(c.isdigit() for c in password): charset_size += 10   # 0-9
    if any(not c.isalnum() for c in password): charset_size += 32  # symbols
    if charset_size == 0: return 0
    return round(math.log2(charset_size) * len(password), 1)

def crack_time_estimate(entropy: float) -> str:
    guesses_per_sec = 10_000_000_000  # 10 billion — fast offline attack
    total_guesses = 2 ** entropy
    seconds = total_guesses / (2 * guesses_per_sec)  # average is half worst-case
    if seconds < 1: return "Instant"
    if seconds < 60: return f"{int(seconds)} seconds"
    if seconds < 3600: return f"{int(seconds/60)} minutes"
    if seconds < 86400: return f"{int(seconds/3600)} hours"
    if seconds < 31536000: return f"{int(seconds/86400)} days"
    if seconds < 3.15e10: return f"{int(seconds/31536000)} years"
    return "Centuries or longer"

def strength_label(entropy: float) -> str:
    if entropy < 28: return "Terrible"
    if entropy < 36: return "Weak"
    if entropy < 60: return "Moderate"
    if entropy < 96: return "Strong"
    if entropy < 128: return "Very Strong"
    return "Maximum"

# Demo
passwords = [
    "password",            # 38 bits by formula — but in every dictionary
    "P@ssw0rd1!",          # 55 bits by formula — but known leet-speak pattern
    "kX9#mL2@vQ4$",        # 85 bits — genuinely random 13 chars
    "correct-horse-battery-staple",  # 77 bits — 4-word passphrase
    "7hK!mP3xNq8@wR2vLz",  # 124 bits — 18-char random
]

for pwd in passwords:
    e = password_entropy(pwd)
    print(f"{pwd!r[:30]}")
    print(f"  Entropy: {e} bits | Strength: {strength_label(e)} | Crack time: {crack_time_estimate(e)}")
    print()

Important limitation: The entropy formula assumes genuine random character selection. Dictionary words and leet-speak substitutions score much higher than their actual resistance to attack. P@ssw0rd1! scores 55 bits by formula but is cracked instantly because it matches known attack patterns. Use zxcvbn for real measurement, not this formula alone.

The Two Strategies for Genuinely Strong Passwords

Strategy 1: Cryptographically random strings for a password manager

For every password stored in a manager — which should include all passwords except your master password — a randomly generated 16 to 20 character string using letters, digits, and symbols is the gold standard. The essential property is randomness, not length alone or character variety alone. A 20-character string where every character is chosen uniformly at random from a 94-character alphabet has 131 bits of entropy and no pattern that rule-based cracking tools can exploit.

ToolPry's Password Generator uses crypto.getRandomValues() — the browser's cryptographically secure random number generator, the same API browsers use for TLS key generation — to ensure genuine randomness. The password is generated entirely in your browser and never transmitted anywhere. You can verify this by checking the page source or using the browser's network tab to confirm no requests are made when clicking Generate.

Strategy 2: Diceware passphrases for passwords you must memorise

For passwords you need to type from memory — your password manager master password, your laptop login, your email account — Diceware passphrases combine memorability with genuine security. A Diceware passphrase randomly selects words from a numbered wordlist using physical dice, or a cryptographically secure random number generator. Each word from a 7,776-word list adds exactly log₂(7,776) ≈ 12.9 bits of entropy.

Four randomly selected words: ≈ 52 bits. Five words: ≈ 64 bits. Six words: ≈ 77 bits. Six words is comparable to a 13-character fully random password in entropy, but dramatically easier to memorise and type. The security comes entirely from random selection — if you choose words based on what feels memorable or meaningful to you, you are not using Diceware and the entropy is much lower.

import secrets

# Load the real EFF wordlist for production use
# https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt
# The list has 7,776 words (6^5 — one per five dice rolls)

# Simplified example with a small sample list
WORDS = [
    "apple", "bridge", "canopy", "dancer", "eagle", "fable", "garlic", "harbor",
    "island", "jungle", "kitten", "lemon", "mango", "novel", "ocean", "pepper",
    "quartz", "river", "sunset", "tiger", "umbra", "valley", "walnut", "xenon",
    "yellow", "zipper"
]

def generate_passphrase(word_count: int = 6, separator: str = "-") -> str:
    """Generate a random passphrase. Uses secrets module for cryptographic randomness."""
    words = [secrets.choice(WORDS) for _ in range(word_count)]
    return separator.join(words)

for length in [4, 5, 6]:
    phrase = generate_passphrase(length)
    entropy = length * math.log2(7776)  # using full wordlist size
    print(f"{length} words: {phrase}")
    print(f"  Entropy: {entropy:.1f} bits ({strength_label(entropy)})")
    print()

Checking If Your Password Has Been Breached

Format and length tell you how hard a password is to crack by brute force. They tell you nothing about whether the password already appears in breach databases that attackers use as their first line of attack. HaveIBeenPwned (HIBP) maintains a database of over 900 million breached passwords collected from publicly released data breach dumps. Their k-anonymity API lets you check any password against this database without ever transmitting the password itself.

import hashlib
import requests

def count_breach_appearances(password: str) -> int:
    """
    Returns how many times this password appears in known breach databases.
    Returns 0 if not found.
    Uses k-anonymity: only the first 5 chars of the SHA-1 hash are sent.
    The actual password is never transmitted.
    """
    sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
    prefix = sha1_hash[:5]
    suffix = sha1_hash[5:]

    response = requests.get(
        f'https://api.pwnedpasswords.com/range/{prefix}',
        headers={'Add-Padding': 'true'},  # prevents traffic analysis
        timeout=10
    )
    response.raise_for_status()

    for line in response.text.splitlines():
        hash_suffix, count = line.split(':')
        if hash_suffix == suffix:
            return int(count)
    return 0

# Check a password
password = "hunter2"
count = count_breach_appearances(password)
if count > 0:
    print(f'EXPOSED: This password appears in {count:,} known breaches.')
    print('Change it immediately on every site where you use it.')
else:
    print('Not found in HIBP breach database.')
    print('(This does not guarantee it is safe — only that it is not in this list.)')

Building a Real-Time Strength Indicator With zxcvbn

zxcvbn is the state-of-the-art JavaScript library for password strength estimation. Unlike formula-based meters, it evaluates the actual cracking resistance of a password by checking it against known patterns, breach wordlists, keyboard patterns, and dictionary attacks. Its score is a single integer from 0 (terrible) to 4 (very strong), along with estimated crack time and specific improvement suggestions.

// npm install zxcvbn
// CDN: https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.4.2/zxcvbn.js
import zxcvbn from 'zxcvbn';

const SCORE_CONFIG = [
  { label: 'Terrible',   colour: '#ef4444', width: '10%'  },
  { label: 'Weak',       colour: '#f97316', width: '25%'  },
  { label: 'Fair',       colour: '#eab308', width: '50%'  },
  { label: 'Good',       colour: '#22c55e', width: '75%'  },
  { label: 'Very Strong',colour: '#22d3ee', width: '100%' },
];

function evaluatePassword(password) {
  if (!password) return null;
  const result = zxcvbn(password);
  const config = SCORE_CONFIG[result.score];

  return {
    score: result.score,
    label: config.label,
    colour: config.colour,
    barWidth: config.width,
    crackTime: result.crack_times_display.offline_fast_hashing_1e10_per_second,
    suggestions: result.feedback.suggestions,
    warning: result.feedback.warning,
  };
}

// Example usage in a React component
function PasswordField() {
  const [password, setPassword] = React.useState('');
  const strength = password ? evaluatePassword(password) : null;

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Enter password"
      />
      {strength && (
        <div>
          <div style={{
            height: 4, background: '#1a1a1e', borderRadius: 2, marginTop: 8
          }}>
            <div style={{
              width: strength.barWidth,
              height: '100%',
              background: strength.colour,
              borderRadius: 2,
              transition: 'all 0.3s ease',
            }} />
          </div>
          <p style={{ fontSize: 13, color: strength.colour, marginTop: 4 }}>
            {strength.label} — crack time: {strength.crackTime}
          </p>
          {strength.warning && (
            <p style={{ fontSize: 12, color: '#f87171' }}>{strength.warning}</p>
          )}
          {strength.suggestions.map((s, i) => (
            <p key={i} style={{ fontSize: 12, color: '#a1a1aa' }}>• {s}</p>
          ))}
        </div>
      )}
    </div>
  );
}

Password Strength Requirements for Developers: What to Enforce

When building a registration form, the requirements you impose determine the password quality your users create. The most effective approach, supported by current NIST SP 800-63B guidance (updated 2024), is to enforce minimum length and breach checking, and nothing else. Do not require uppercase, lowercase, digits, and symbols — these rules produce P@ssw0rd1! patterns. Do not require periodic rotation — it produces Password1, Password2, Password3 sequences. Instead, enforce a minimum of 12 characters, check the password against the HIBP breach database, optionally use zxcvbn and reject anything scoring 0 or 1, and display real-time feedback to guide users toward better choices without blocking them unnecessarily.

// Server-side password validation following NIST SP 800-63B guidance
const axios = require('axios');
const crypto = require('crypto');

async function validatePassword(password) {
  const errors = [];

  // Rule 1: Minimum length (NIST recommends at least 8, we enforce 12)
  if (password.length < 12) {
    errors.push('Password must be at least 12 characters');
  }

  // Rule 2: Maximum length (prevent DoS via hashing extremely long inputs)
  if (password.length > 128) {
    errors.push('Password must be 128 characters or fewer');
  }

  // Rule 3: Check against breach database
  if (password.length >= 12) {
    const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
    const prefix = sha1.slice(0, 5);
    const suffix = sha1.slice(5);
    try {
      const resp = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`);
      const found = resp.data.split('\r\n').some(line => line.startsWith(suffix));
      if (found) {
        errors.push('This password has appeared in known data breaches. Please choose a different password.');
      }
    } catch (e) {
      // If HIBP is unreachable, do not block registration — just skip this check
      console.warn('HIBP check failed:', e.message);
    }
  }

  return { valid: errors.length === 0, errors };
}

Common Myths About Password Strength

Several beliefs about password strength are widespread but incorrect. Understanding them helps you make better decisions about both personal passwords and the policies you impose on users.

Myth: Special characters make a password much stronger. A special character from a set of 32 symbols adds log₂(32) = 5 bits of entropy to the character pool — equivalent to adding one more letter. Adding two random characters adds more entropy than changing a letter to a symbol. Special characters help slightly, but they are not a substitute for length.

Myth: A random-looking password with mixed case and symbols is hard to crack. Only if it is genuinely random. A human-chosen password that looks random — like substituting letters with similar-looking symbols — follows patterns that cracking tools exploit specifically. The appearance of randomness and actual randomness are not the same thing. Use a password generator with a verified cryptographically random source, not your own judgment about what looks random.

Myth: Longer passwords are always better, regardless of randomness. A 20-character password made entirely of dictionary words chosen for memorability has far lower effective entropy than a 16-character random string, because humans choose words non-randomly. Length helps, but randomness is the prerequisite. A long password constructed from related words is vulnerable to wordlist attacks even if its character count is high.

Myth: Password managers are risky because one breach exposes everything. The risk of a password manager breach is far smaller than the risk of password reuse across multiple sites. When a site is breached and passwords are exposed, attackers immediately try the same credentials on major email providers, banks, and social media — a practice called credential stuffing. A password manager with a strong master password and one unique generated password per site means a breach of any one site exposes nothing about any other. The asymmetric risk strongly favours using a manager.

Frequently Asked Questions

What minimum password length should I require for my application?

NIST SP 800-63B (2024 revision) recommends a minimum of 8 characters, but practical guidance for new applications is 12 characters as the minimum. The key reasoning: at 12 characters, even a password using only lowercase letters has 56 bits of entropy — into the Strong range. At 8 characters, a lowercase-only password has 37 bits — just barely into the Moderate range and potentially crackable given enough time and computational resources. For high-security contexts like financial or healthcare applications, consider requiring 16 characters minimum.

Should I use a passphrase or a random character string?

For passwords stored in a manager, use a random character string of 16 to 20 characters. For passwords you must memorise and type regularly, use a 5 to 6 word Diceware passphrase. At equivalent entropy, both are equally resistant to brute force. The difference is entirely usability: a 6-word passphrase like ocean-tiger-bridge-fancy-queen-apple is far easier to type accurately from memory than a 13-character random string. Choose based on whether you will store it or memorise it.

How often should passwords be changed?

Only when there is specific reason to believe they have been compromised — a breach notification from a service, suspicious account activity, or confirmed credential exposure. NIST explicitly deprecated mandatory periodic rotation in 2017 and reinforced this in its 2024 update, having found that forced rotation causes users to make small predictable changes that provide no real security benefit. A strong unique password that never changes is more secure than a weaker password changed every 90 days.