Skip to content

Password Breach Checker — Java source

Check if a password has appeared in known data breaches using k-anonymity. Only the first 5 characters of the SHA-1 hash are sent - your full password never leaves your browser.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Password breach lookup via Have I Been Pwned's Pwned Passwords API, using
// k-anonymity: only the first 5 characters of the SHA-1 hash ever leave the
// machine.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/breach-checker.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// Per the polyglot porting rules, this snippet carries the SHA-1 hashing and
// prefix-splitting logic only — the HTTP fetch against
// https://api.pwnedpasswords.com/range/{PREFIX} (free, no key) is transport,
// not pure logic, and is intentionally omitted. The range response is one
// "SUFFIX:COUNT" line per hash sharing the prefix (~800 candidates); the
// suffix match happens locally via parseRangeBody.

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Locale;

public final class BreachChecker {

    /** The verdict of one lookup (count -1 = lookup failed). */
    public record BreachResult(
            boolean breached,
            long count,
            /** First 5 chars of the uppercase SHA-1 hex — the only part sent to the API. */
            String hashPrefix,
            /** Remaining 35 chars of the hash, matched locally against the response. */
            String hashSuffix,
            /** Set when the lookup failed; null otherwise. */
            String error) {
    }

    public static final String HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";

    private BreachChecker() {
    }

    /** SHA-1 of a UTF-8 string as uppercase hex (the format HIBP expects). */
    public static String sha1Hex(String input) throws Exception {
        byte[] digest = MessageDigest.getInstance("SHA-1").digest(input.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder(digest.length * 2);
        for (byte b : digest) {
            sb.append(String.format("%02X", b));
        }
        return sb.toString().toUpperCase(Locale.ROOT);
    }

    /** The 5-char k-anonymity prefix and the 35-char suffix of a hash. */
    public record HashParts(String prefix, String suffix) {
    }

    /** Split a 40-char uppercase hash into the 5-char prefix and the 35-char suffix. */
    public static HashParts splitHash(String hash) {
        String h = hash.toUpperCase(Locale.ROOT);
        return new HashParts(h.substring(0, 5), h.substring(5));
    }

    /**
     * Search an HIBP range response for a hash suffix and return its breach count.
     * Never throws; returns 0 when the suffix is not present. Tolerates LF and
     * CRLF line endings, blank lines, and leading/trailing whitespace per line.
     */
    public static long parseRangeBody(String body, String suffix) {
        if (body == null || suffix == null || suffix.isEmpty()) {
            return 0;
        }
        for (String line : body.split("\\r?\\n")) {
            int idx = line.indexOf(':');
            if (idx == -1) {
                continue;
            }
            if (line.substring(0, idx).trim().equals(suffix)) {
                try {
                    long count = Long.parseLong(line.substring(idx + 1).trim());
                    return count < 0 ? 0 : count;
                } catch (NumberFormatException e) {
                    return 0;
                }
            }
        }
        return 0;
    }

    /** Count the "SUFFIX:COUNT" candidate lines in a range response. */
    public static int countCandidates(String body) {
        if (body == null) {
            return 0;
        }
        int n = 0;
        for (String line : body.split("\\r?\\n")) {
            int idx = line.indexOf(':');
            if (idx != -1 && !line.substring(0, idx).trim().isEmpty()) {
                n++;
            }
        }
        return n;
    }

    /**
     * Assemble the local part of a breach check: hash the password, split it,
     * and evaluate a fetched range body. The fetch itself is transport and lives
     * in the caller (java.net.http.HttpClient GET HIBP_RANGE_URL + prefix).
     */
    public static BreachResult evaluate(String password, String rangeBody) throws Exception {
        HashParts parts = splitHash(sha1Hex(password));
        long count = parseRangeBody(rangeBody, parts.suffix());
        return new BreachResult(count > 0, count, parts.prefix(), parts.suffix(), null);
    }
}

Also available in 9 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →