Type Your Real Password Into a Random Website. What Could Go Wrong?
Go search "password strength checker" right now. Look at what you're being asked to do: paste your actual password — the one you maybe reuse, the one guarding your email recovery — into a text field on a website you've never heard of, with no visible indication of whether that string is being logged, sent to an analytics endpoint, or quietly saved in a form-tracking tool bolted onto the page.
Most of these tools could be doing the math client-side. Almost none of them tell you that they are. And a disturbing number of them are running third-party scripts that make client-side-only a hard thing to actually verify from the outside.
That's an insane amount of trust to extend to a page that also happens to have a sticky ad banner and a cookie consent modal stacked three deep.
So I built a Password Strength Calculator that does exactly one thing: computes real Shannon entropy, keyspace size, and brute-force crack-time estimates, entirely in your browser, with the network tab open the whole time to prove it. No password ever leaves the input field. Here's the actual math and the TypeScript behind it.
The Math: Password Strength Isn't a Vibe, It's Information Theory
Most consumer password meters are just regex rules dressed up as a progress bar — "has a number: +10%, has a symbol: +10%." That's not measurement, it's guesswork. The actual metric that matters is entropy, borrowed directly from Claude Shannon's information theory.
1. The Core Formula
E = L × log2(R)
Where:
- E — entropy, measured in bits
- L — password length (character count)
- R — size of the character pool actually used (keyspace) Every additional bit doubles the number of guesses an attacker needs. This is why entropy, not "does it have a special character," is the real signal.
2. Detecting the Character Pool
The pool size R isn't fixed — it depends on which character classes actually appear in the password:
| Class | Pool Size |
|---|---|
| Lowercase letters | 26 |
| Uppercase letters | 26 |
| Digits | 10 |
| Symbols | 33 |
Use only lowercase letters and R = 26. Mix in digits and symbols and R climbs to 95. This is the step naive "strength meters" skip — they check for the presence of a symbol without recalculating the actual keyspace it unlocks.
3. Worked Example
Take the password k9#M2$vL — 8 characters, using all four classes:
R = 26 + 26 + 10 + 33 = 95
E = 8 × log2(95) = 8 × 6.5698 ≈ 52.56 bits
Total brute-force search space: 95^8 ≈ 6.63 × 10^15 combinations.
4. Crack Time Estimation
Entropy alone is abstract. What actually lands with people is: how long would this take to break? Convert total combinations into wall-clock time by dividing by an attacker's guess rate:
secondsToCrack = totalCombinations / guessesPerSecond
Guess rate varies wildly by attack surface, so the calculator models a spread rather than a single number:
| Attack Scenario | Guess Rate |
|---|---|
| Online, rate-limited login | ~100 / sec |
| Offline, single GPU | ~10 billion / sec |
| Offline, multi-GPU cracking rig | ~100 billion / sec |
This is also the detail that makes the crack-time number honest: an 8-character password that survives "years" against a throttled login form can fall in seconds against an offline GPU rig if the hash ever leaks. Showing both numbers, not just the flattering one, is the whole point.
5. Why Length Beats Complexity
This is the counterintuitive part worth internalizing: length grows the search space exponentially, complexity only grows it linearly with pool size.
Tr0ub&9! → 8 chars, R=95 → E ≈ 52.6 bits
correct-horse-battery → 21 chars, R≈7776-word dictionary → E ≈ 51+ bits (per-word entropy stacked)
A 4-word passphrase from a 7,776-word list produces comparable or greater entropy than a dense 8-character symbol soup — and it's dramatically easier for a human to actually remember and type correctly, which matters because the least secure password is the one someone writes on a sticky note out of frustration.
The TypeScript Core: Entropy Logic, Fully Isolated
Same principle as always: the math has zero business knowing what a <button> is. Pure function in, typed result out — testable without mounting a single component.
// lib/calculatePasswordStrength.ts
export interface CrackTimeEstimate {
scenario: string;
guessesPerSecond: number;
secondsToCrack: number;
humanReadable: string;
}
export interface PasswordStrengthResult {
length: number;
poolSize: number;
entropyBits: number;
totalCombinations: number;
crackTimes: CrackTimeEstimate[];
strengthLabel: "Very Weak" | "Weak" | "Moderate" | "Strong" | "Very Strong";
}
const POOL_LOWERCASE = 26;
const POOL_UPPERCASE = 26;
const POOL_DIGITS = 10;
const POOL_SYMBOLS = 33;
const ATTACK_SCENARIOS: { scenario: string; guessesPerSecond: number }[] = [
{ scenario: "Online, rate-limited login", guessesPerSecond: 100 },
{ scenario: "Offline, single GPU", guessesPerSecond: 1e10 },
{ scenario: "Offline, multi-GPU rig", guessesPerSecond: 1e11 },
];
function detectPoolSize(password: string): number {
let pool = 0;
if (/[a-z]/.test(password)) pool += POOL_LOWERCASE;
if (/[A-Z]/.test(password)) pool += POOL_UPPERCASE;
if (/[0-9]/.test(password)) pool += POOL_DIGITS;
if (/[^a-zA-Z0-9]/.test(password)) pool += POOL_SYMBOLS;
return pool;
}
function formatDuration(seconds: number): string {
const units: [string, number][] = [
["centuries", 60 * 60 * 24 * 365 * 100],
["years", 60 * 60 * 24 * 365],
["days", 60 * 60 * 24],
["hours", 60 * 60],
["minutes", 60],
["seconds", 1],
];
for (const [label, unitSeconds] of units) {
const value = seconds / (unitSeconds as number);
if (value >= 1) {
return `${value < 1000 ? value.toFixed(1) : value.toExponential(2)} ${label}`;
}
}
return "instantly";
}
function labelStrength(entropyBits: number): PasswordStrengthResult["strengthLabel"] {
if (entropyBits < 28) return "Very Weak";
if (entropyBits < 40) return "Weak";
if (entropyBits < 60) return "Moderate";
if (entropyBits < 80) return "Strong";
return "Very Strong";
}
export function calculatePasswordStrength(password: string): PasswordStrengthResult {
const length = password.length;
const poolSize = detectPoolSize(password);
if (length === 0 || poolSize === 0) {
return {
length: 0,
poolSize: 0,
entropyBits: 0,
totalCombinations: 0,
crackTimes: [],
strengthLabel: "Very Weak",
};
}
const entropyBits = length * Math.log2(poolSize);
const totalCombinations = Math.pow(poolSize, length);
const crackTimes: CrackTimeEstimate[] = ATTACK_SCENARIOS.map(
({ scenario, guessesPerSecond }) => {
const secondsToCrack = totalCombinations / guessesPerSecond;
return {
scenario,
guessesPerSecond,
secondsToCrack,
humanReadable: formatDuration(secondsToCrack),
};
}
);
return {
length,
poolSize,
entropyBits: Number(entropyBits.toFixed(2)),
totalCombinations,
crackTimes,
strengthLabel: labelStrength(entropyBits),
};
}
Notes on the design:
-
detectPoolSizeuses regex presence checks, not counts. One uppercase letter contributes exactly the same 26 to the pool as ten uppercase letters would — pool size is about which character classes are in play, not how often they repeat. Conflating the two is a common bug in home-grown entropy calculators. -
formatDurationwalks largest-unit-first so a multi-century crack time doesn't render as an ugly six-digit "hours" figure — legibility here is the entire point of showing crack time at all. -
Empty/zero-pool input returns a valid zeroed result instead of
NaNor throwing. A password field starts empty on every page load; the calculation function needs to handle that as a first-class case, not an edge case.
The Client Component: Live Entropy, Zero Network Calls
// components/CalculatorForm.tsx
"use client";
import { useMemo, useState } from "react";
import { calculatePasswordStrength } from "@/lib/calculatePasswordStrength";
const STRENGTH_COLOR: Record<string, string> = {
"Very Weak": "bg-red-500/10 text-red-400",
Weak: "bg-orange-500/10 text-orange-400",
Moderate: "bg-yellow-500/10 text-yellow-400",
Strong: "bg-emerald-500/10 text-emerald-400",
"Very Strong": "bg-emerald-500/20 text-emerald-300",
};
export default function CalculatorForm() {
const [password, setPassword] = useState("");
const [visible, setVisible] = useState(false);
// Pure, synchronous, client-only — this value never touches the network.
const result = useMemo(() => calculatePasswordStrength(password), [password]);
return (
<div className="space-y-5">
<div className="relative">
<input
type={visible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Type a password to test"
autoComplete="off"
spellCheck={false}
className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 pr-16 font-mono text-zinc-100 outline-none focus:border-indigo-500"
/>
<button
type="button"
onClick={() => setVisible((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-semibold text-zinc-400 hover:text-zinc-200"
>
{visible ? "Hide" : "Show"}
</button>
</div>
{password.length > 0 && (
<div className="space-y-4 rounded-xl border border-zinc-800 bg-zinc-900/60 p-4">
<div
className={`inline-block rounded-full px-3 py-1 text-xs font-semibold ${STRENGTH_COLOR[result.strengthLabel]}`}
>
{result.strengthLabel} — {result.entropyBits} bits of entropy
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<Stat label="Length" value={result.length} />
<Stat label="Pool Size" value={result.poolSize} />
</div>
<div className="space-y-2 pt-2">
<p className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
Estimated Crack Time
</p>
{result.crackTimes.map((ct) => (
<div
key={ct.scenario}
className="flex items-center justify-between text-sm"
>
<span className="text-zinc-400">{ct.scenario}</span>
<span className="font-mono text-zinc-100">
{ct.humanReadable}
</span>
</div>
))}
</div>
</div>
)}
<p className="text-xs text-zinc-500">
This calculation runs entirely in your browser. Nothing you type here
is transmitted, logged, or stored.
</p>
</div>
);
}
function Stat({ label, value }: { label: string; value: string | number }) {
return (
<div className="rounded-lg bg-zinc-900 px-3 py-2">
<p className="text-xs text-zinc-500">{label}</p>
<p className="font-mono text-zinc-100">{value}</p>
</div>
);
}
A couple of things worth calling out beyond the obvious useMemo recalculation pattern:
-
autoComplete="off"andspellCheck={false}on the input. A password strength tester is the one input on the internet where you specifically don't want the browser trying to autofill, save, or spellcheck-highlight what's typed — all three create side channels you didn't ask for. - The trust statement lives directly under the input, not in a footer disclaimer. If the entire value proposition is "this doesn't leave your browser," that claim needs to be visible at the moment of use, not buried three scrolls down.
-
No
onSubmit, no fetch, no server action. There's no form to submit — the absence of a submit handler is the security model. If you go looking for a network call in this component, you won't find one, because there isn't one.
Why "Nothing Leaves the Browser" Is Also a Performance Win
Treating this as a security feature undersells it — cutting the network entirely also happens to be a straightforward performance win:
| Metric | Typical "Send to Server" Checker | HypeCalc (Client-Only) |
|---|---|---|
| Network round-trip per keystroke | 50–300ms+ | 0ms — never leaves the browser |
| Server dependency | Requires backend uptime | None — pure client function |
| Data exposure surface | Password transits network + server logs | Zero transmission, ever |
| Recalculation latency | Debounced, network-bound | Sub-millisecond, synchronous |
| Third-party script interference | Common (analytics, ad tags) | None on the calculation path |
There's a nice bit of alignment here: the architecture that's best for the user's privacy — do the computation locally, don't phone home — is also just... the fastest possible architecture. Removing the network isn't a trade-off against performance, it's the same decision wearing two hats.
Try Breaking It
The full calculator — entropy, keyspace detection, and the three-scenario crack-time model — is live and running client-side right now:
HypeCalc Password Strength Calculator →
Open the network tab before you type anything. Watch it stay empty the entire time you're testing passwords. That's not a claim in a privacy policy — you can verify it yourself in about ten seconds.
Discussion
A few things I keep going back and forth on:
- Should password strength meters be legally required to disclose whether the check is client-side or server-side? Right now it's basically invisible to the average user, and the stakes (an actual password) feel higher than most other "trust me" UI patterns.
- Is entropy alone even the right metric anymore, given how much modern cracking leans on dictionary and pattern-based attacks rather than pure brute force? Where do you think a "real-world" strength score should diverge from the textbook Shannon formula?
- What's a case where you specifically avoided a legitimate-looking tool because you didn't trust what it was doing with your input — password checkers, JSON formatters, base64 decoders, anything? I suspect this list is longer than most people admit. Curious to hear where people land, especially anyone who's built entropy-based tooling for an actual auth system rather than a public calculator.
Top comments (0)