Password Range API
The range API implements a k-anonymity model that lets you check passwords against known breaches without exposing the password—or even its full hash—to our servers.
The k-anonymity approach
Sending a full password hash to any external service creates a risk: an attacker who intercepts the hash can use rainbow tables to reverse it. The range-prefix model solves this by sending only a partial hash.
With a 5-character hex prefix, each request maps to roughly 2^20 (~1 million) possible hashes. The server returns all matches for that bucket, and your application finds the exact match locally. An observer (including LeakJar itself) cannot determine which suffix you're looking for.
Step 1: Hash the password
Compute the SHA-1 hash of the password and convert it to uppercase hexadecimal. This should happen on your server, never in client-side code.
import { createHash } from "crypto";
function sha1Hex(password: string): string {
return createHash("sha1")
.update(password, "utf8")
.digest("hex")
.toUpperCase();
}Step 2: Extract the prefix
Take the first 5 characters of the hex-encoded hash. This is your range prefix.
const hash = sha1Hex("user-password");
const prefix = hash.slice(0, 5); // e.g. "CBFDA"
const suffix = hash.slice(5); // remaining 35 charsStep 3: Query the range endpoint
Send the prefix to the LeakJar range API. The response contains every suffix in the breach dataset that shares the same prefix, along with the number of times each appeared.
const res = await fetch(
`https://api.leakjar.com/api/demo/passwords/range/${prefix}`,
{
headers: {
Authorization: `Bearer ${process.env.LEAKJAR_API_KEY}`,
},
}
);
const data = await res.json();
// data.suffixes: Array<{ suffix: string; count: number }>Step 4: Compare locally
Search the returned suffixes for a match against your remaining hash characters. If found, the password has been exposed; the count tells you how many times.
interface SuffixEntry {
suffix: string;
count: number;
}
function checkBreach(
suffix: string,
suffixes: SuffixEntry[]
): { breached: boolean; count: number } {
const match = suffixes.find(
(s) => s.suffix === suffix
);
return match
? { breached: true, count: match.count }
: { breached: false, count: 0 };
}
// Usage
const result = checkBreach(suffix, data.suffixes);
if (result.breached) {
console.log(`Password seen ${result.count} times in breaches.`);
}Complete flow
Here is the entire check as a single function you can drop into a registration or password-reset handler:
import { createHash } from "crypto";
interface BreachResult {
breached: boolean;
count: number;
}
export async function checkPassword(
password: string
): Promise<BreachResult> {
const hash = createHash("sha1")
.update(password, "utf8")
.digest("hex")
.toUpperCase();
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const res = await fetch(
`https://api.leakjar.com/api/demo/passwords/range/${prefix}`,
{
headers: {
Authorization: `Bearer ${process.env.LEAKJAR_API_KEY}`,
},
}
);
if (!res.ok) {
throw new Error(`LeakJar API error: ${res.status}`);
}
const data = await res.json();
const match = data.suffixes.find(
(s: { suffix: string }) => s.suffix === suffix
);
return match
? { breached: true, count: match.count }
: { breached: false, count: 0 };
}