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 measures the uncertainty in a password, how many guesses an attacker needs, on average, to find it. The formula is straightforward:
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 | N (pool size) | Example |
|---|---|---|
| Digits only | 10 | 123456 |
| Lowercase | 26 | qwerty |
| Lowercase + digits | 36 | abc123 |
| Mixed case | 52 | PassWord |
| Mixed case + digits | 62 | Pass123 |
| All printable ASCII | 94 | P@ssw0rd! |
| Full Unicode (BMP) | 65,536 | 🔑🔒🛡️ |
| Password | Length | Set | Entropy | Crack Time (SHA-1, 100 GH/s) |
|---|---|---|---|---|
| 123456 | 6 | 10 | ~20 bits | < 1 millisecond |
| password | 8 | 26 | ~38 bits | < 3 milliseconds |
| Pass123 | 7 | 62 | ~42 bits | < 10 milliseconds |
| P@ssw0rd! | 9 | 94 | ~59 bits | ~180 seconds |
| correct-horse-battery | 20 | 26 | ~94 bits | ~200,000 years |
| CSPRNG 16-char | 16 | 94 | ~105 bits | ~40 million years |
| CSPRNG 20-char | 20 | 94 | ~131 bits | ~170 billion years |
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.
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 |
|---|---|---|
| MD5 | 180 GH/s | 1.4 TH/s |
| SHA-1 | 100 GH/s | 800 GH/s |
| SHA-256 | 30 GH/s | 240 GH/s |
| NTLM | 200 GH/s | 1.6 TH/s |
| bcrypt (cost=5) | 80 KH/s | 640 KH/s |
| bcrypt (cost=10) | 2.5 KH/s | 20 KH/s |
| bcrypt (cost=12) | 640 H/s | 5.1 KH/s |
| argon2id (1,64,4) | 400 H/s | 3.2 KH/s |
| scrypt (N=16384, r=8, p=1) | 150 KH/s | 1.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.
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']}")
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.
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.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.