Methodology
How risk scores, severity, and privacy are calculated.
Need help or want to report an issue? Contact us.
For an email address, the overall risk score is a deterministic function of three factors. Each factor is computed independently and summed, then capped at 100:
riskScore = min(frequency + recency + sensitivity, 100)
frequencymax 40min(numBreaches × 8, 40)
Number of distinct breaches the email appears in.
recencymax 30< 6 months → 30 · < 1 year → 20 · < 2 years → 10 · older → 5
Based on the most recent breach date.
sensitivitymax 30(avg(sensitivityWeight(breach.dataClasses), capped at 15) ÷ 15) × 30
Average sensitivity of exposed data classes across all breaches.
The qualitative bucket is then derived from the numeric score:
Password scoring is based on how often the password has been seen in breach corpora collected by Have I Been Pwned (Pwned Passwords). It is a single-dimension score, so the factor breakdown is hidden in the UI (the API still returns a factorsobject with all three contributions equal to zero, for schema consistency).
Independent strength estimate (zxcvbn). Alongside the breach lookup, a second, independent signal is shown: a local estimate of password strength produced by zxcvbn (Wheeler, 2016). zxcvbn returns an integer score from 0 (Very Weak) to 4 (Very Strong) based on dictionary matches, keyboard patterns, and entropy, and yields an estimated offline crack-time. This computation runs entirely in the browser — the password is never sent to the server for strength analysis (the breach lookup separately uses k-anonymity, sending only a 5-char SHA-1 prefix). A password can be Strong by zxcvbn but still appear in HIBP, or Weak yet not (yet) appear in any leak — the two signals answer different questions and are best read together.
| Times seen | Score | Bucket |
|---|---|---|
| ≥ 100,000 | 95 | High |
| ≥ 10,000 | 80 | High |
| ≥ 1,000 | 60 | Medium |
| ≥ 100 | 40 | Medium |
| 1 – 99 | 25 | Low |
| 0 | 0 | None |
Each individual breach also receives its own 0–100 severity score so you can prioritise which breach to act on first. The formula is:
severity = min(sensitivity + recency + scale, 100)
where:
sensitivity = (sensitivityWeight(dataClasses) / 15) × 50 // 0 – 50
recency = age-bucketed points (see "recency" above) // 0 – 30
scale = PwnCount-bucketed points // 0 – 20| Accounts in breach (PwnCount) | Scale points |
|---|---|
| ≥ 100,000,000 | 20 |
| ≥ 10,000,000 | 15 |
| ≥ 1,000,000 | 10 |
| ≥ 10,000 | 5 |
| < 10,000 | 0 |
Every data class exposed in a breach contributes a weight to that breach's sensitivity sum (capped at 15 per breach so a single breach cannot dominate the average):
- Passwords
- Password hints
- Security questions and answers
- Credit cards
- Banking details
- Payment histories
- Social security numbers
- Government issued IDs
- Phone numbers
- Physical addresses
- Dates of birth
- Financial data
- Health & fitness data
- Medical records
- Bank account numbers
- IP addresses
- Everything else (e.g. usernames, email addresses, names)
Password lookups never transmit the raw password — or even its full hash — to any external service. Following HIBP's published k-anonymity protocol, only the first 5 characters of the SHA-1 hash leave the server. The remote service returns the list of all hash suffixes that share that prefix (typically ~800 entries per range). The match check happens locally:
- 1. Browser submits the password to PDEC over TLS — it never reaches a third party.
- 2. PDEC computes
SHA-1(password)locally (40 hex characters). - 3. Only the first 5 hex characters of the hash are sent to HIBP (
GET /range/AABBC). - 4. HIBP returns ~800 hash suffixes that share that prefix, each with a count.
- 5. PDEC compares the suffix locally — if it matches, return the count to the user; otherwise no exposure.
This guarantees that the breach API never sees the user's password or its full hash, and cannot tell which specific password was checked from any single request — only that someone checked something whose hash begins with that 5-character prefix.
- HIBP Public Breach List (
/api/v3/breaches) — curated metadata for all known breaches: name, date, affected account count, exposed data classes. Cached server-side for 1 hour to minimise upstream load. - XposedOrNot (
/v1/check-email/<email>) — free per-email lookup that returns the names of breaches the address appears in. Combined with the HIBP metadata above to produce the breach details shown. - HIBP Pwned Passwords (
/range/<5-char prefix>) — k-anonymity range lookup over a corpus of more than 850 million previously breached passwords.
All external HTTP calls have an 8–10 second timeout to fail fast under upstream degradation.
- No identifier (email or password) is persisted to disk or any database.
- Server logs explicitly redact request bodies and identifiers — only method, URL path, status, and duration are recorded.
- For password checks, only the first 5 characters of a SHA-1 hash are transmitted externally.
- The frontend masks the checked identifier after the lookup completes (passwords become the literal string
"a password"in client memory). - Helmet sets safe HTTP headers; per-IP rate limiting protects the upstream APIs and the user from abuse.
- Ali, J. (2018). Validating leaked passwords with k-Anonymity. blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity
- Hunt, T. Have I Been Pwned API v3 documentation. haveibeenpwned.com/API/v3
- National Institute of Standards and Technology (2017). NIST Special Publication 800-63B — Digital Identity Guidelines: Authentication and Lifecycle Management. pages.nist.gov/800-63-3/sp800-63b.html
- European Parliament & Council (2016). Regulation (EU) 2016/679 (General Data Protection Regulation), Article 32 — Security of processing. gdpr-info.eu/art-32-gdpr