Use k-anonymity to check if a password has been compromised — without ever sending the plaintext password over the wire.
curl and sha1sum (or Python with hashlib)You never send the full password or even the full hash to the API. Here is how it works:
"password" becomes 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8).5BAA6) as the prefix.This means the API never learns which password you are checking. The server cannot distinguish your query from hundreds of other hashes sharing the same prefix.
First, hash the password locally. Let's check the famously breached password "password":
# Hash locally (never send plaintext!)
echo -n "password" | sha1sum
# Output: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
# Send the full SHA-1 hash to the API
curl -s -X POST /api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"sha1Hash": "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8"
}' | python3 -m json.tool
Expected response:
{
"sha1Prefix": "5BAA6",
"found": true,
"breachCount": 9545824,
"prevalence": "EXTREMELY_COMMON",
"recommendation": "REJECT",
"message": "This password has appeared in 9,545,824 data breaches. It should never be used.",
"processingTimeMs": 18
}
found |
Whether the exact hash was found in breach databases. |
breachCount |
Number of times this password appeared across all known breaches. |
prevalence |
Human-readable severity: EXTREMELY_COMMON, VERY_COMMON, COMMON, UNCOMMON, RARE. |
recommendation |
REJECT (force password change), WARN (suggest change), or ACCEPT (not found in breaches). |
# Hash a strong password locally
echo -n "j&4Kx!mP9qR2vZ#w" | sha1sum
# Output: a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
curl -s -X POST /api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"sha1Hash": "A3F2B8C1D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9"}'
{
"sha1Prefix": "A3F2B",
"found": false,
"breachCount": 0,
"prevalence": "NOT_FOUND",
"recommendation": "ACCEPT",
"message": "This password has not been found in any known data breaches.",
"processingTimeMs": 12
}
You can also send just the first 5 hex characters if you prefer to do the full-hash comparison client-side:
curl -s -X POST /api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"sha1Prefix": "5BAA6"}'
The API returns all matching suffixes so your client can compare locally — the server never learns the full hash.
curl -s -X POST /api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"sha1Hash": "ZZZZ_NOT_HEX"}'
{
"error": "VALIDATION_ERROR",
"message": "sha1Hash: must be a valid 40-character hexadecimal SHA-1 hash",
"status": 400
}
{
"error": "VALIDATION_ERROR",
"message": "Either sha1Hash or sha1Prefix must be provided",
"status": 400
}
Breach checks are rate-limited to prevent enumeration attacks. Respect 429 responses and the Retry-After header.
import hashlib
import requests
API_BASE = ""
HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "YOUR_API_KEY"
}
def is_password_breached(plaintext_password):
"""Check if a password appears in breach databases.
The plaintext is NEVER sent to the API."""
sha1_hash = hashlib.sha1(
plaintext_password.encode("utf-8")
).hexdigest().upper()
resp = requests.post(
f"{API_BASE}/api/v1/check",
json={"sha1Hash": sha1_hash},
headers=HEADERS, verify=False
)
resp.raise_for_status()
data = resp.json()
return data["found"], data.get("breachCount", 0)
# Usage in registration
password = input("Choose a password: ")
found, count = is_password_breached(password)
if found:
print(f"This password appeared in {count:,} breaches. Choose another.")
else:
print("Password is safe to use.")