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:
- You send your password to a server
- The server checks if it's in the breach database
- 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
passwordvariable is hashed immediately with_hash_password() - Only
hash_prefix(5 characters) goes over the network - The comparison with
hash_suffixhappens 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:
- Intercept your HTTPS request
- Know your username/email (to associate the query with you)
- Brute-force through ~500 candidate passwords
- 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:
- A malicious server operator (Have I Been Pwned is run by a Microsoft Regional Director with 15+ years of reputation)
- You checking multiple variations of the same password
- 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:

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
123456orpassword) - 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
- Troy Hunt: Understanding Have I Been Pwned's Use of SHA-1 and k-Anonymity
- Cloudflare: Validating Leaked Passwords with k-Anonymity
- Jack Cable: Attacks on Have I Been Pwned's model of k-anonymity
- Mozilla Security Blog: Scanning for Breached Accounts with k-Anonymity
- Have I Been Pwned API Documentation
Questions about our security architecture or how we handle your passwords? Email us at security@dosel.app.