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

Take your password security further with NordPass, a premium password manager that generates, stores, and syncs your credentials with zero-knowledge encryption across all your devices.