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:
- Hash locally. The client computes the SHA-1 hash of the password, written as 40 hexadecimal characters.
- Send only the first 5 characters. The client requests
https://api.pwnedpasswords.com/range/followed by those 5 characters, the hash prefix. - 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.”
- 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
passwordis5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8. - 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,pasand 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: trueheader 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
General explanation, not advice for a particular situation. Updated 2026-10-31.