← All posts
1Password2SHA-1 hash35-char prefix4500 hashes5Local match6Result
9 min read

How we check if your passwords are breached without ever seeing them

Dosel checks your credentials against 900+ million breached passwords using k-anonymity—a cryptographic protocol that ensures your passwords never leave your machine. Here's exactly how it works, with real code.

securityprivacyk-anonymityHaveIBeenPwnedbreach-detection

When you import your passwords into Dosel, we immediately check each one against a database of over 900 million breached passwords. If any of your passwords have appeared in data breaches, we flag them with a red "EXPOSED" badge and show you exactly how many times they've been compromised.

But here's what makes this different: your passwords never leave your machine.

Not to our servers. Not to the breach database. Not anywhere. This isn't marketing speak—it's cryptographic fact. Let me show you exactly how it works.

The problem: How do you check breaches without exposing passwords?

Traditional password checking would work like this:

  1. You send your password to a server
  2. The server checks if it's in the breach database
  3. The server tells you yes or no

The obvious problem? You've just sent your password to a third party. Even if you trust them today, what happens if their logs get hacked tomorrow? Or if a rogue employee decides to collect passwords?

This is why most people don't check their passwords against breach databases—the cure seems worse than the disease.

The solution: K-anonymity

In 2018, security researcher Troy Hunt (creator of Have I Been Pwned) partnered with Cloudflare to implement a clever cryptographic protocol called k-anonymity for password checking.

Here's how it works:

Step 1: Hash the password locally

Your password is converted to a SHA-1 hash on your machine. For example, the password password123 becomes:

CBFDAC6008F9CAB4083784CBD1874F76618D2A97

This hash is a one-way function—you can't reverse it to get the original password.

Step 2: Send only the first 5 characters

Instead of sending the full 40-character hash, we send only the first 5 characters to the API:

CBFDA

Step 3: Receive ~500 matching hashes

The API returns all breached password hashes that start with those 5 characters—typically around 500 results. For example:

C6008F9CAB4083784CBD1874F76618D2A97:47205
C7A9D19E2B83DC65F97F2CD9C29C4C8FA22:12
C8012B23AA24CC5F2ADEF20E2F4B9A89123:891
... (497 more)

The number after each hash is how many times that password appeared in breaches.

Step 4: Check locally

Your device checks if the suffix of your password's hash (C6008F9CAB4083784CBD1874F76618D2A97) matches any of the returned results. If it does, your password has been breached.

The key insight: The API has no idea which of the ~500 hashes you were looking for. It could be any of them. This is k-anonymity—your query is hidden among hundreds of other possibilities.

Real code from Dosel

Here's the actual implementation from our codebase. This runs entirely on your Mac:

class PwnedPasswordChecker:
    """
    Check passwords against Have I Been Pwned database using k-anonymity.

    This class implements privacy-preserving password breach detection:
    1. Hashes password locally with SHA-1
    2. Sends only first 5 characters of hash to HIBP API
    3. Receives ~800 hash suffixes from API
    4. Checks locally if full hash matches any returned suffix

    The password and full hash are NEVER transmitted over the network.
    """

    API_URL = "https://api.pwnedpasswords.com/range/{hash_prefix}"

    def _hash_password(self, password: str) -> str:
        """SHA-1 hash a password (never logged or transmitted)."""
        return hashlib.sha1(password.encode('utf-8')).hexdigest().upper()

    async def check_password(self, password: str) -> Tuple[bool, int]:
        """
        Check if password has been found in data breaches.

        Uses k-anonymity model to ensure privacy:
        - Password is hashed locally
        - Only first 5 hash characters sent to API
        - API returns ~800 hash suffixes
        - Full hash checked locally against response
        """
        # Hash password locally
        full_hash = self._hash_password(password)

        # Split hash for k-anonymity
        hash_prefix = full_hash[:5]
        hash_suffix = full_hash[5:]

        # Query HIBP API with only first 5 characters
        url = self.API_URL.format(hash_prefix=hash_prefix)
        headers = {'Add-Padding': 'true'}  # Privacy padding

        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as response:
                text = await response.text()

                # Check if our hash suffix is in the response
                for line in text.splitlines():
                    response_suffix, count_str = line.split(':')
                    if response_suffix == hash_suffix:
                        # Password found in breach database
                        return (True, int(count_str))

                # Password not found
                return (False, 0)

Notice what's happening:

  • The password variable is hashed immediately with _hash_password()
  • Only hash_prefix (5 characters) goes over the network
  • The comparison with hash_suffix happens locally
  • The actual password is never in a network request

Addressing the security research

We believe in transparency, so let's address the academic criticisms of k-anonymity head-on.

Criticism 1: "20 bits of information are leaked"

The concern: By sending 5 hex characters (20 bits), you're narrowing down the password space.

The reality: Yes, this reduces possibilities from "infinite" to roughly 1-in-500. But here's the math: an attacker would need to:

  1. Intercept your HTTPS request
  2. Know your username/email (to associate the query with you)
  3. Brute-force through ~500 candidate passwords
  4. Hope one of them is actually yours

If an attacker has this level of access to your traffic, they could just keylog your actual password. K-anonymity isn't protecting against nation-state attackers with full network access—it's protecting against the breach database operator and casual interception.

Criticism 2: "Multiple similar passwords reveal patterns"

The concern: Security researcher Jack Cable demonstrated that checking multiple similar passwords (like password1, password2, password3) could theoretically narrow down your actual password.

The reality: This attack requires:

  1. A malicious server operator (Have I Been Pwned is run by a Microsoft Regional Director with 15+ years of reputation)
  2. You checking multiple variations of the same password
  3. Sophisticated cross-reference analysis

We mitigate this further:

  • We check each unique password only once (deduplicated)
  • We use padding to obscure response sizes
  • All checks happen in a batch, not incrementally as you type

Criticism 3: "Password managers shouldn't use this"

The concern: Some researchers suggested password managers should avoid HIBP until the protocol is "redesigned."

The reality: This recommendation came with a key qualifier—"at minimum, password managers should notify users that password information is revealed to a third party server."

We do better than that:

  • We explain the k-anonymity protocol (you're reading this)
  • The code is visible in our implementation
  • We use padding to prevent response-size analysis
  • Your passwords stay on your machine; only a 5-character hash prefix leaves

The bottom line

No security protocol is perfect. K-anonymity leaks a small amount of information to enable a massive practical benefit: checking your passwords against 900+ million known breaches without sending those passwords anywhere.

The alternative—not checking at all—is far worse. 94% of passwords are reused, and breach databases are the first thing attackers try.

Why this is state of the art

K-anonymity for password checking was developed by Cloudflare and Troy Hunt in 2018, then formally analyzed by Cornell University researchers in 2020. It's used by:

  • 1Password (Watchtower feature)
  • Bitwarden (Data breach reports)
  • Mozilla Firefox (Firefox Monitor)
  • Apple (Safari password monitoring)
  • Google Chrome (Password checkup)

When security researchers at Cloudflare, password managers handling millions of credentials, and browser vendors all converge on the same protocol, that's a strong signal you're looking at best practice.

The protocol has been enhanced since 2018:

  • Padding added in 2020: All responses now contain 800-1,000 results regardless of actual matches, preventing response-size analysis
  • HTTPS required: All queries are encrypted in transit
  • No logging: HIBP explicitly doesn't log queries (and the 5-character prefix wouldn't be useful anyway)

What you see in Dosel

When you import your password CSV, we automatically run every password through breach detection. If any are compromised, you'll see:

Password breach detection in Dosel

The red "EXPOSED" badge indicates this password was found in data breaches. Hovering shows the exact breach count and severity.

The severity levels are:

  • Critical: Found in 1,000,000+ breaches (like 123456 or password)
  • High: Found in 10,000-1,000,000 breaches
  • Medium: Found in 100-10,000 breaches
  • Low: Found in fewer than 100 breaches

Even a "low" severity password should be changed—it's in attacker dictionaries and will be tried against your accounts.

What makes our implementation different

1. Zero-knowledge architecture

The breach check is just one part of our broader security model. Your passwords:

  • Never leave your Mac
  • Are never stored on our servers
  • Are cleared from memory after use

We can't see your passwords even if we wanted to.

2. We show you the evidence

We don't just say "bad password." We show you:

  • Exact breach count
  • Severity classification
  • Recommendation to change immediately

3. Then we help you fix it

Knowing a password is breached is only half the battle. Dosel's AI agent can then navigate to each affected site and change the password for you—without you having to click through dozens of password reset flows.

Try it yourself

Import any password manager CSV into Dosel. In seconds, you'll see which of your passwords have been compromised—with the cryptographic guarantee that we never saw them.

Download Dosel and check your passwords today. The free tier handles 5 password changes per month, but breach detection is unlimited.

Your passwords are probably in more breaches than you think. Ours found 23 compromised credentials in the first import we tested. The only way to know is to check.

Sources

Questions about our security architecture or how we handle your passwords? Email us at security@dosel.app.


Protect your passwords with AI-powered automation.

Download Dosel