πŸ“… June 2026 ⏱ 12 min read 🏷️ password-security, entropy

Why Strong Passwords Are Your First Line of Defence

TL;DR: Password strength is not subjective. It's a direct function of entropy β€” measured in bits β€” determined by length and character set. A 16-character random password from a 94-character set delivers ~105 bits of entropy. Against a modern 8x RTX 5090 cracking rig running hashcat at 100 GH/s (SHA-1), that's computationally infeasible β€” longer than the age of the universe. But the same password stored with bcrypt cost=5 instead of cost=12 cuts the cracking resistance by a factor of 128.

Every authentication system β€” regardless of how sophisticated β€” starts with a password. Even in a passkey-first world, every system has a password recovery path. And every password recovery path is only as strong as the passwords users create.

This guide explains the mathematics of password strength from first principles, covers real-world cracking hardware benchmarks, and gives you Python code to measure and enforce password strength programmatically.

Entropy: The Mathematics of Password Strength

Entropy measures the uncertainty in a password β€” how many guesses an attacker needs, on average, to find it. The formula is straightforward:

H = L Γ— logβ‚‚(N)

Where:

Each bit of entropy doubles the number of possible passwords. A password with 40 bits of entropy has 2⁴⁰ possible combinations. At 80 bits, it's 2⁸⁰ β€” or roughly 1.2 Γ— 10²⁴ possibilities.

Character Set Sizes

Character Set N (pool size) Example
Digits only10123456
Lowercase26qwerty
Lowercase + digits36abc123
Mixed case52PassWord
Mixed case + digits62Pass123
All printable ASCII94P@ssw0rd!
Full Unicode (BMP)65,536πŸ”‘πŸ”’πŸ›‘οΈ

Real Entropy Values

Password Length Set Entropy Crack Time (SHA-1, 100 GH/s)
123456610~20 bits< 1 millisecond
password826~38 bits< 3 milliseconds
Pass123762~42 bits< 10 milliseconds
P@ssw0rd!994~59 bits~180 seconds
correct-horse-battery2026~94 bits~200,000 years
CSPRNG 16-char1694~105 bits~40 million years
CSPRNG 20-char2094~131 bits~170 billion years
Key insight: "P@ssw0rd!" at 9 characters has 59 bits of entropy β€” crackable in 3 minutes with a consumer GPU. The same character set at 16 characters jumps to 105 bits β€” utterly infeasible. Length dominates complexity.

Why Length Beats Complexity

There's a persistent myth that requiring mixed case, digits, and symbols is the most important password rule. The math says otherwise. Every additional character of length multiplies the search space by N (the pool size). Adding a character class adds at most ~5.5 bits per character position (94/26 β‰ˆ 3.6x, or logβ‚‚(3.6) β‰ˆ 1.85 extra bits per position).

Adding 1 character of length (94-char set): +6.55 bits
Adding symbols to lowercase-only: +1.85 bits per position

A 12-character lowercase password has 12 Γ— logβ‚‚(26) = 56 bits. A 10-character full-ASCII password has 10 Γ— logβ‚‚(94) = 65 bits. The 12-character lowercase password with fewer character classes is stronger because it's longer.

Real-World Cracking Hardware: 2026 Benchmarks

Password cracking hardware advances every generation. Here are conservative benchmarks for a single RTX 5090 GPU (released 2025) running hashcat:

Algorithm Hash Rate (single RTX 5090) 8-GPU Rig
MD5180 GH/s1.4 TH/s
SHA-1100 GH/s800 GH/s
SHA-25630 GH/s240 GH/s
NTLM200 GH/s1.6 TH/s
bcrypt (cost=5)80 KH/s640 KH/s
bcrypt (cost=10)2.5 KH/s20 KH/s
bcrypt (cost=12)640 H/s5.1 KH/s
argon2id (1,64,4)400 H/s3.2 KH/s
scrypt (N=16384, r=8, p=1)150 KH/s1.2 MH/s

Notice the difference between cost=5 and cost=12 for bcrypt: a factor of 128x. And notice how bcrypt cost=12 at 5.1 KH/s on an 8-GPU rig means cracking a 105-bit (16-char CSPRNG) password takes longer than the current age of the universe.

The winning combination: CSPRNG-generated passwords β‰₯ 16 characters stored with bcrypt cost=12 (or argon2id). Each layer independently makes the other more effective β€” strong passwords resist online guessing; strong hashing resists offline cracking if the database is breached.

Programmatic Password Strength Evaluation

Here's a Python implementation that calculates entropy and provides a strength rating:

#!/usr/bin/env python3
"""Password strength evaluator β€” calculates entropy bits and cracking costs."""

import math
import hashlib
import struct
from typing import Tuple, Dict

CHARSETS = {
    "lower": set("abcdefghijklmnopqrstuvwxyz"),
    "upper": set("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
    "digit": set("0123456789"),
    "symbol": set("!@#$%^&*()-_=+[]{}|;':\",./<>?`~"),
}

def calculate_entropy(password: str) -> float:
    """Calculate password entropy in bits."""
    # Determine effective character pool size
    pool_size = 0
    for name, charset in CHARSETS.items():
        if any(c in charset for c in password):
            pool_size += len(charset)
    
    # Detect if password has characters outside known sets
    known_chars = set().union(*CHARSETS.values())
    unknown = set(password) - known_chars
    if unknown:
        pool_size += 94  # Assume full ASCII printable for unknown chars
    
    # Guard against zero pool
    if pool_size == 0:
        pool_size = 94
    
    return len(password) * math.log2(pool_size)


def crack_time_estimate(entropy: float, hash_rate: float = 100e9) -> Dict:
    """Estimate time to crack at given hash rate (default: SHA-1 on RTX 5090)."""
    combinations = 2 ** entropy
    seconds = combinations / (2 * hash_rate)  # Average case = half the search space
    
    intervals = [
        ("nanoseconds", 1e-9),
        ("microseconds", 1e-6),
        ("milliseconds", 1e-3),
        ("seconds", 1),
        ("minutes", 60),
        ("hours", 3600),
        ("days", 86400),
        ("years", 31536000),
    ]
    
    for name, unit in intervals:
        if seconds < unit * 1000:
            value = seconds / unit
            return {
                "value": round(value, 1),
                "unit": name,
                "seconds": seconds,
            }
    
    return {"value": round(seconds / 31536000, 1), "unit": "years", "seconds": seconds}


def strength_rating(entropy: float) -> Tuple[str, str]:
    """Return strength rating and color code for a given entropy value."""
    if entropy < 30:
        return ("Very Weak", "#ef4444")
    elif entropy < 50:
        return ("Weak", "#f97316")
    elif entropy < 70:
        return ("Moderate", "#eab308")
    elif entropy < 90:
        return ("Strong", "#22c55e")
    else:
        return ("Very Strong", "#22d3ee")


def evaluate_password(password: str) -> Dict:
    """Full password evaluation."""
    entropy = calculate_entropy(password)
    rating, color = strength_rating(entropy)
    cracking = crack_time_estimate(entropy)
    
    return {
        "password_length": len(password),
        "entropy_bits": round(entropy, 1),
        "strength_rating": rating,
        "crack_time": f"{cracking['value']} {cracking['unit']}",
        "recommendation": (
            "Length β‰₯ 16 characters from full ASCII set, "
            "generated using crypto.getRandomValues()."
        ) if entropy < 80 else "Strong",
    }

# Example usage
if __name__ == "__main__":
    result = evaluate_password("5rX!kP9mQ@2nB&vW")
    print(f"Entropy: {result['entropy_bits']} bits")
    print(f"Rating: {result['strength_rating']}")
    print(f"Crack time (SHA-1, single GPU): {result['crack_time']}")

CSPRNG vs PRNG: Why the Source Matters

Entropy calculations assume uniform random selection from the character set. This is only true if you use a cryptographically secure pseudorandom number generator (CSPRNG). Common non-CSPRNG sources:

Source Type Security Recoverable?
JavaScript Math.random() Xorshift128+ ❌ Not secure Yes β€” 624 consecutive outputs recover the full state
Python random Mersenne Twister (MT19937) ❌ Not secure Yes β€” 624 outputs recover 19,937-bit state
Java java.util.Random LCG (48-bit) ❌ Not secure Yes β€” 2 outputs recover state
PHP rand() LCG (various) ❌ Not secure Yes
C rand() LCG ❌ Not secure Yes
JavaScript crypto.getRandomValues() OS CSPRNG (NIST SP 800-90A) βœ… Secure No β€” true entropy sources
Python secrets OS CSPRNG βœ… Secure No
/dev/urandom OS CSPRNG (Linux) βœ… Secure No

The Mersenne Twister (used by Python's random module and many other languages) has a 19,937-bit internal state. An attacker who observes 624 consecutive outputs can reconstruct the full state and predict ALL future outputs β€” including any passwords generated with that RNG instance. This is why all password generation must use CSPRNG sources.

Recommendation: Use our CSPRNG Password Tool for all production password generation. It uses crypto.getRandomValues() backed by the operating system's CSPRNG, consistent with NIST SP 800-90A. Every password is generated client-side β€” never transmitted over the network.

Practical Guidelines for Developers

  1. Enforce minimum entropy, not arbitrary rules. Instead of "must include uppercase, lowercase, digit, symbol, and be 8+ characters," enforce a minimum of 60 bits. This naturally produces stronger passwords without frustrating constraints.
  2. Generate passwords for users where possible. Pre-generated CSPRNG passwords at 20+ characters are always stronger than anything a human invents. Provide this option at signup and password-reset flows.
  3. Check against known breach corpora. Implement HIBP's k-Anonymity API at registration. A 40-bit-entropy password that's been exposed in a breach is effectively 0 bits of security.
  4. Use bcrypt cost β‰₯ 12 or argon2id. The additional authentication latency (250-500ms) is unnoticeable to users but multiplies an attacker's cost by orders of magnitude.
  5. Never store passwords without memory-hard hashing. If your database is compromised, hashing is your last line of defence. Make it count.

Conclusion

Password strength isn't mysterious β€” it's measurable. Entropy in bits, cracking time at known hash rates, and the quality of the random number source all yield objective numbers. A password with 105 bits of entropy from a CSPRNG, stored with bcrypt cost 12, is computationally immune to any offline attack foreseeable in the next decade.

Length is the single most important factor. Every additional character adds ~6.55 bits of entropy with a full ASCII set. A 20-character CSPRNG password delivers ~131 bits β€” beyond any feasible cracking budget for the foreseeable future.

Start with a strong, CSPRNG-generated password. Store it with memory-hard hashing. Check it against known breaches. And enable MFA as the safety net. That's your first line of defence β€” measurable, verifiable, and effective.