Docs
API Reference

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.

hash.tstypescript
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.

prefix.tstypescript
const hash = sha1Hex("user-password");
const prefix = hash.slice(0, 5);   // e.g. "CBFDA"
const suffix = hash.slice(5);       // remaining 35 chars

Step 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.

query.tstypescript
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.

compare.tstypescript
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:

check-password.tstypescript
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 };
}
Note: This is a mock endpoint for demonstration purposes. In the demo environment, the API returns synthetic breach data. Production endpoints operate against real-world breach datasets.