DEV Community

Cover image for I Built a Password Strength Calculator That Never Sends Your Password Anywhere Here's the Entropy Math
G S
G S

Posted on

I Built a Password Strength Calculator That Never Sends Your Password Anywhere Here's the Entropy Math

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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),
  };
}
Enter fullscreen mode Exit fullscreen mode

Notes on the design:

  • detectPoolSize uses 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.
  • formatDuration walks 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 NaN or 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

A couple of things worth calling out beyond the obvious useMemo recalculation pattern:

  • autoComplete="off" and spellCheck={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:

  1. 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.
  2. 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?
  3. 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)