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 →