Password Breach Checker — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Breach Checker - 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 process.
//
// Language: C# 12 / .NET 8 (standard library only)
// Source: CosmoDev polyglot showcase port of the Breach Checker tool,
// ported from src/lib/breach-checker.ts (the canonical TypeScript
// implementation).
// License: display source - part of CosmoDev's polyglot tool pages.
//
// API: https://api.pwnedpasswords.com/range/{PREFIX} - free, no key. Returns
// one "SUFFIX:COUNT" line per hash sharing the prefix (~800 candidates); the
// suffix match happens locally. The TS reference takes an injectable fetch
// function so tests can stub the network; this port takes an optional
// HttpClient for the same reason (inject a shared client for connection
// reuse in production code).
using System.Security.Cryptography;
using System.Text;
/// <summary>Outcome of one Pwned Passwords k-anonymity lookup.</summary>
/// <param name="Breached">True when the exact hash suffix appeared in the API's candidate list.</param>
/// <param name="Count">How many times the password appeared in breaches. 0 = never seen. -1 = lookup failed.</param>
/// <param name="HashPrefix">First 5 chars of the uppercase SHA-1 hex - the only part sent to the API.</param>
/// <param name="HashSuffix">Remaining 35 chars of the hash, matched locally against the response.</param>
/// <param name="Error">Set when the lookup failed (network error or non-success response).</param>
/// <param name="Candidates">How many candidate suffixes the API returned (all checked locally).</param>
public sealed record BreachResult(
bool Breached,
int Count,
string HashPrefix,
string HashSuffix,
string? Error = null,
int? Candidates = null);
public static class BreachChecker
{
public const string HibpRangeUrl = "https://api.pwnedpasswords.com/range/";
/// <summary>SHA-1 of a UTF-8 string as uppercase hex (the format HIBP expects).</summary>
public static string Sha1Hex(string input) =>
Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(input)));
/// <summary>Split a 40-char uppercase hash into the 5-char k-anonymity prefix and the 35-char suffix.</summary>
public static (string Prefix, string Suffix) SplitHash(string hash)
{
string h = (hash ?? string.Empty).ToUpperInvariant();
return (h[..5], h[5..]);
}
/// <summary>
/// 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.
/// </summary>
public static int ParseRangeBody(string body, string suffix)
{
if (body is null || suffix.Length == 0) return 0;
foreach (string line in body.Split('\n', '\r'))
{
int idx = line.IndexOf(':');
if (idx == -1) continue;
if (line[..idx].Trim() == suffix)
{
string raw = line[(idx + 1)..].Trim();
return int.TryParse(raw, out int count) && count >= 0 ? count : 0;
}
}
return 0;
}
/// <summary>Count the "SUFFIX:COUNT" candidate lines in a range response.</summary>
public static int CountCandidates(string body)
{
if (body is null) return 0;
int n = 0;
foreach (string line in body.Split('\n', '\r'))
{
int idx = line.IndexOf(':');
if (idx != -1 && line[..idx].Trim().Length > 0) n++;
}
return n;
}
/// <summary>
/// Check a password against the Pwned Passwords corpus using k-anonymity.
/// Only the prefix is sent over the network; the suffix match is local.
/// Never throws - a failed lookup returns Count = -1 with Error set.
/// </summary>
public static async Task<BreachResult> CheckBreachAsync(
string password, HttpClient? http = null, CancellationToken ct = default)
{
var (prefix, suffix) = SplitHash(Sha1Hex(password));
BreachResult failure = new(Breached: false, Count: -1, HashPrefix: prefix, HashSuffix: suffix);
HttpClient client = http ?? new HttpClient();
try
{
using HttpResponseMessage response = await client.GetAsync(HibpRangeUrl + prefix, ct);
if (!response.IsSuccessStatusCode)
{
return failure with
{
Error = $"The breach database returned HTTP {(int)response.StatusCode}",
};
}
string body = await response.Content.ReadAsStringAsync(ct);
int count = ParseRangeBody(body, suffix);
return new BreachResult(
Breached: count > 0,
Count: count,
HashPrefix: prefix,
HashSuffix: suffix,
Error: null,
Candidates: CountCandidates(body));
}
catch (Exception e)
{
// The TS reference distinguishes Error instances from anything
// else; every .NET exception carries a Message, so it is used
// directly.
return failure with { Error = e.Message };
}
finally
{
if (http is null) client.Dispose();
}
}
}
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 →