Skip to content

Password Breach Checker — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! breach-checker — SHA-1 hashing + k-anonymity prefix splitting for Pwned
//! Passwords lookups.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/breach-checker.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! k-anonymity: only the first 5 characters of the SHA-1 hash ever leave the
//! machine. The API at https://api.pwnedpasswords.com/range/{PREFIX} returns
//! one "SUFFIX:COUNT" line per hash sharing the prefix (~800 candidates);
//! the suffix match happens locally.
//!
//! Per the polyglot task spec, this snippet covers the pure logic only —
//! SHA-1 hashing, prefix/suffix splitting, and range-body parsing. The HTTP
//! fetch (fetch() in TS, std.http.Client in Zig) is deliberately omitted.

const std = @import("std");

pub const Sha1 = std.crypto.hash.Sha1;

/// Base URL of the Pwned Passwords range API (the fetch itself is out of scope).
pub const hibp_range_url = "https://api.pwnedpasswords.com/range/";

/// The k-anonymity prefix length — the only part of the hash sent to the API.
pub const prefix_length: usize = 5;

pub const HashParts = struct {
    /// First 5 chars of the uppercase SHA-1 hex — the only part sent to the API.
    prefix: [prefix_length]u8,
    /// Remaining 35 chars of the hash, matched locally against the response.
    suffix: [35]u8,
};

/// SHA-1 of a UTF-8 string as uppercase hex (the format HIBP expects).
/// `out` must be 40 bytes. Mirrors the TS `sha1Hex`.
pub fn sha1Hex(out: *[40]u8, input: []const u8) void {
    var digest: [Sha1.digest_length]u8 = undefined;
    Sha1.hash(input, &digest, .{});
    const hex = std.fmt.bytesToHex(digest, .upper);
    @memcpy(out, &hex);
}

/// Split a 40-char uppercase hash into the 5-char k-anonymity prefix and the
/// 35-char suffix. Input shorter than 40 chars is an error (the TS version
/// would silently return short slices; a real hash is always 40 hex chars).
pub fn splitHash(out: *HashParts, hash: []const u8) error{InvalidHashLength}!void {
    if (hash.len != 40) return error.InvalidHashLength;
    for (0..prefix_length) |i| out.prefix[i] = std.ascii.toUpper(hash[i]);
    for (0..35) |i| out.suffix[i] = std.ascii.toUpper(hash[5 + i]);
}

/// Search an HIBP range response for a hash suffix and return its breach count.
/// Returns 0 when the suffix is not present. Tolerates LF and CRLF line
/// endings, blank lines, and leading/trailing whitespace per line.
pub fn parseRangeBody(body: []const u8, suffix: []const u8) u64 {
    if (suffix.len == 0) return 0;
    var lines = std.mem.splitScalar(u8, body, '\n');
    while (lines.next()) |raw_line| {
        var line = raw_line;
        if (line.len > 0 and line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
        const idx = std.mem.indexOfScalar(u8, line, ':') orelse continue;
        if (std.mem.eql(u8, trim(line[0..idx]), suffix)) {
            const count_text = trim(line[idx + 1 ..]);
            const count = std.fmt.parseInt(u64, count_text, 10) catch return 0;
            return count;
        }
    }
    return 0;
}

/// Count the "SUFFIX:COUNT" candidate lines in a range response.
pub fn countCandidates(body: []const u8) usize {
    var n: usize = 0;
    var lines = std.mem.splitScalar(u8, body, '\n');
    while (lines.next()) |raw_line| {
        var line = raw_line;
        if (line.len > 0 and line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
        const idx = std.mem.indexOfScalar(u8, line, ':') orelse continue;
        if (trim(line[0..idx]).len != 0) n += 1;
    }
    return n;
}

fn trim(s: []const u8) []const u8 {
    return std.mem.trim(u8, s, " \t\r\n");
}

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 →