Skip to content
Finowl

Security

Password breach checks with k-anonymity: how they stay private

How hash-prefix range lookups let you check a password against breach data without revealing it, why padding matters, and how Finowl handles passwords.

Checking whether a password has appeared in a data breach is one of the most effective ways to stop weak credentials, but it raises an obvious question: how do you check a password without handing it to someone else? The answer is a technique called k-anonymity, built on hash-prefix range lookups. This post explains how it works, why padding matters, and what Finowl does with the passwords you send.

The problem with naive password checks

The simplest possible design is to send the password to a service and ask "is this in your list?" That works, but it means the service sees every password your users choose. Even with good intentions, that creates a juicy target: logs, crash reports and network captures can all leak secrets.

A slightly better design is to hash the password first and send the full hash. This hides the plaintext, but not well. Common passwords have well-known hashes, and anyone who sees a full unsalted hash can look it up in a precomputed table or crack it quickly with a dictionary. A full hash is effectively the password for most real-world choices.

What you want is a design where the service can help you answer the question without ever learning which password you asked about.

How k-anonymity range lookups work

k-anonymity is a privacy property: a record is k-anonymous if it is indistinguishable from at least k-1 other records. Applied to password checks, the idea is to send a query that matches many possible passwords, so the server cannot tell which one you care about.

Step 1: hash the password

The password is hashed with SHA-1, producing a 40-character hexadecimal string. For example, a password might hash to something like 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8.

SHA-1 is not used here for password storage, where it would be a poor choice. It is used as a consistent fingerprint so that the same password always maps to the same lookup key in the breach dataset.

Step 2: send only a short prefix

Instead of sending the whole hash, the client sends only the first 5 characters, such as 5BAA6. Five hexadecimal characters give 16^5, or about one million, possible prefixes. Every one of those prefixes is shared by a large number of hashes in a big breach dataset, and by an effectively unlimited number of possible passwords.

Step 3: receive the matching range

The server returns every hash suffix in its dataset that starts with that prefix, along with how often each one was seen in breaches. A typical response contains hundreds of suffixes.

Step 4: compare locally

The client compares the remaining 35 characters of its own hash against the returned list. If one matches, the password is breached and the attached count says how often it has been seen. If none match, it has not appeared in the dataset.

At no point did the server see the full hash, let alone the password. It only learned that the client was interested in one of the many hashes that share a prefix.

Why padding matters

Prefix lookups hide the password, but there is a subtler leak: response size. Different prefixes match different numbers of suffixes. If an observer can see the encrypted response length, they could narrow down which prefix was requested, and in principle correlate that with other information.

Padding closes that gap. The server adds a random number of fake entries to each response, typically with a count of zero, so that every response falls within a similar size range. The client simply ignores entries with a zero count. The result is that the size of the response no longer says much about which prefix was queried.

Padding is a small detail, but it is the difference between a design that is private in theory and one that holds up against an attacker who can watch traffic.

What Finowl does

Finowl exposes the password check as a single endpoint, POST /v1/breaches/password, so you do not have to implement hashing, prefix requests, suffix matching or padding yourself.

When you send a password, Finowl:

  1. Hashes it with SHA-1.
  2. Looks up only the 5-character hash prefix against Finowl Intelligence, using a k-anonymity range search with padding.
  3. Compares the suffixes and returns whether the password was found and how many times.

The password is processed transiently. It is never logged, never stored, and never returned in the response. You can read more about this on the password breach feature page and in the data privacy documentation.

A request in practice

bash
curl -X POST https://api.finowl.co/v1/breaches/password \
  -H "Authorization: Bearer $FINOWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "password": "correct horse battery staple" }'

A breached password returns a count of how often it has been seen:

json
{ "breached": true, "count": 133211 }

A password that has not appeared in breach data returns:

json
{ "breached": false, "count": 0 }

Passwords can be up to 4096 characters. Each check costs one credit, and every account gets 100 free credits a month, with more available on the pricing page.

A Python example

python
import os
import requests

def password_breach_count(password: str) -> int:
    resp = requests.post(
        "https://api.finowl.co/v1/breaches/password",
        headers={
            "Authorization": f"Bearer {os.environ['FINOWL_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={"password": password},
        timeout=5,
    )
    resp.raise_for_status()
    return resp.json()["count"]

if __name__ == "__main__":
    count = password_breach_count("P@ssw0rd")
    print("Seen in breaches" if count else "Not found", count)

Client-side prefix lookups versus a server-side API

You might wonder why Finowl accepts the plaintext password over TLS rather than requiring you to send a prefix. Both approaches have trade-offs.

Doing the prefix work yourself

Hashing and prefix matching in your own backend means the password never leaves your infrastructure. The cost is code you have to write, test and maintain, including the padding handling and comparison logic.

Using a server-side check

A single API call is simpler to integrate and harder to get wrong. Your password already travels from the user's browser to your backend over TLS; sending it to a check endpoint over TLS, where it is hashed and looked up by prefix and never persisted, keeps the exposure narrow. What matters most is what happens to the value after it arrives, which is why the transient, never-logged handling is the key guarantee.

Whichever design you pick, the rules on your side are the same: never log request bodies that contain passwords, make sure error reporting tools scrub them, and do not cache results keyed by the password.

What the count tells you

The count is how many times the password appeared across breach data. A very high count means the password is common and will be near the top of any attacker's guessing list. A count of one or two means it leaked at least once and is in the hands of whoever holds that dataset.

For blocking decisions, any non-zero count is usually enough to reject a new password. Some teams use the count only for messaging, for example telling users that a password "has appeared in data breaches many times." The guide on blocking breached passwords at signup covers UX and code for that flow, and credential stuffing and breached passwords explains why leaked passwords are so dangerous even when they look strong.

Frequently asked questions

What is a k-anonymity password check?

It is a lookup where only a short prefix of the password's hash is used to query breach data. Many hashes share that prefix, so the lookup cannot reveal which specific password was being checked.

Does Finowl store the passwords I send?

No. Finowl hashes the password, looks up only the 5-character prefix, and processes it transiently. The password is never logged, stored or returned.

Why does the lookup use SHA-1 if SHA-1 is weak?

SHA-1 is used only as a consistent fingerprint for matching against breach data, not for storing passwords. You should still store your own users' passwords with a slow, salted algorithm designed for that purpose.

What does padding protect against?

Padding adds fake entries to range responses so their size does not reveal which prefix was requested. Without it, an observer watching encrypted traffic could learn more about the query.

Is any non-zero count a reason to reject a password?

For new passwords, generally yes. Any appearance in breach data means the password is known to attackers and is likely to be tried in automated attacks.

All articles