ProtemKey

How passwords are attacked · 5 min read

Checking breached passwords with k-anonymity

A documented way to ask 'has this password been in a breach?' without revealing it, and why this site still chooses not to ask.

NIST SP 800-63B-4 requires services to compare a new password against “passwords obtained from previous breach corpuses” and to refuse matches. That raises a practical question: how do you check a password against a large set of breached passwords held by someone else without handing the password over? The Pwned Passwords range API, run by Have I Been Pwned, publishes one answer. It is a clean piece of design and worth understanding, even though this site deliberately does not use it.

The idea: hash, split, compare

The service holds a large corpus of passwords that have appeared in breaches. Its documentation says each one is stored as both a SHA-1 and an NTLM hash, alongside a count of how many times it was seen. The range API never receives a password or a full hash. Instead:

  1. Hash locally. The client computes the SHA-1 hash of the password, written as 40 hexadecimal characters.
  2. Send only the first 5 characters. The client requests https://api.pwnedpasswords.com/range/ followed by those 5 characters, the hash prefix.
  3. Receive every match for that prefix. The service replies with the remaining 35 characters (the suffix) of every hash in its corpus that begins with that prefix, each followed by a count. The documentation says a response “typically returns approximately 800 hash suffixes.”
  4. Compare locally. The client looks for its own suffix in the list. If it is there, the password is in the corpus, and the count says how often it appeared. If not, it is not in the corpus.

The service learns only the 5-character prefix. Many different hashes share any given prefix, so the prefix alone does not identify the password. This is what k-anonymity means here: the request is indistinguishable from requests for any of the other hashes in the same bucket.

Worked example

Take the password password.

  • SHA-1 of password is 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8.
  • Prefix (first 5 characters): 5BAA6.
  • Suffix (remaining 35 characters): 1E4C9B93F3F0682250B6CF8331B7EE68FD8.

The client sends GET https://api.pwnedpasswords.com/range/5BAA6 and scans the reply for a line beginning 1E4C9B93F3F0682250B6CF8331B7EE68FD8:. A very common password like this one will be there with a large count.

How much does the prefix give away? Each hex character carries 4 bits, so 5 characters carry 20 bits. There are 16^5 = 1,048,576 possible prefixes, and the documentation confirms every one of them returns a normal response. A SHA-1 hash has 160 bits, so a given prefix is shared by 2^(160 − 20) = 2^140 possible hash values. Of those, the ones that matter are the roughly 800 breached hashes in the reply, and the requester’s password is either one of those or something not in the corpus at all. The server cannot tell which.

Padding: hiding the size of the answer

The request travels over HTTPS, so its contents are encrypted. But someone observing the encrypted traffic might still see how large each response is, and since buckets differ in size, size could hint at the prefix. The API offers an optional request header for this:

Header Effect, per the documentation
Add-Padding: true Pads the response so every reply contains a random number of records between 800 and 1,000

Padded entries always have a count of 0 and can be discarded by the client.

Caveats the documentation itself raises

  • Do not check while typing. The documentation warns against “incremental searching,” sending a request after each keystroke, because a series of prefixes for p, pa, pas and so on can let an observer work out the original password. It recommends waiting until the whole password is entered.
  • Prefix length is a trade-off. The documentation explains that 5 characters is “a conscious decision made to balance privacy with performance”: a shorter prefix would mean larger, more anonymous buckets but bigger responses.
  • Absence is not strength. A password that is not in the corpus has simply not been seen in a known breach. It may still be easy to guess. Summer2024!-style passwords are covered in how long it takes to crack a password.
  • A local copy is possible. The corpus can also be downloaded in full, which lets an organization check passwords without making any network request at all.

Why protemkey.com does not make this call

The design above reveals very little, but it does involve contacting a third party: the browser would send a hash prefix to an outside server every time someone checked a password. This site has a simple rule that nothing typed into it leaves the browser, and the page’s content security policy only permits network connections back to protemkey.com itself. So the strength checker does not query Pwned Passwords or any other service.

Instead, it uses a bundled list: 10,000 common passwords from the open SecLists project, shipped with the page. When you type a password, the checker compares it against that list locally, after undoing capitals and common symbol swaps, and shows the match as a line in its working. That catches the passwords at the very front of attackers’ queues. It cannot tell you whether a less common password has appeared in a specific breach, and it does not claim to. More on how the list was chosen is in what makes a password common.

Note: If you choose to use a breach-checking service yourself, the range design above is what to look for: the password, and its full hash, should never leave your device.

How services use it

For a website, the same approach meets NIST’s requirement without building a breach corpus in-house. At sign-up or password change, the server (or the page) hashes the new password, queries by prefix, and rejects a match with the reason, as NIST requires: the service “SHALL require the subscriber to select a different secret and SHALL provide the reason for rejection.” NIST adds that the blocklist need not be huge, because its purpose is to stop passwords likely to be guessed before rate limits take effect.

Summary

  • The client hashes the password with SHA-1, sends only the first 5 hex characters, and checks the returned suffixes itself.
  • 16^5 = 1,048,576 buckets, each typically holding around 800 breached suffixes; the server cannot tell which one you were checking.
  • An optional Add-Padding: true header hides response sizes.
  • Check once the full password is entered, not on every keystroke.
  • protemkey.com does not make this request; its checker uses only a bundled list, so nothing you type is sent anywhere.

Sources

  1. Have I Been Pwned API v3 documentation: Pwned Passwords
  2. NIST SP 800-63B-4, Digital Identity Guidelines: Authentication and Authenticator Management (2025)

General explanation, not advice for a particular situation. Updated 2026-10-31.