Skip to content

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.

// 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++17 (standard library + OpenSSL for SHA-1)
// Ported from src/lib/breach-checker.ts (the canonical TypeScript
// implementation). 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 network call is injectable (FetchFn) so
// the pure logic stays unit-testable without a socket, mirroring the TS
// signature's `fetchFn` parameter.

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/evp.h>

namespace breach {

constexpr const char* HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";

/** Minimal response shape the injected fetch must provide (the TS `Response`). */
struct FetchResponse {
  bool ok = false;        ///< HTTP 2xx
  int status = 0;         ///< the HTTP status code
  std::string body;       ///< the response text
};

/// Injectable transport: URL in, response out. Throw to signal a network error.
using FetchFn = std::function<FetchResponse(const std::string& url)>;

struct BreachResult {
  bool breached = false;   ///< true when the exact hash suffix appeared in the candidate list
  long long count = 0;     ///< appearances in breaches; 0 = never seen, -1 = lookup failed
  std::string hashPrefix;  ///< first 5 chars of the uppercase SHA-1 hex — the only part sent
  std::string hashSuffix;  ///< remaining 35 chars, matched locally against the response
  std::string error;       ///< set when the lookup failed (network error or non-200)
  long long candidates = 0;///< candidate suffixes the API returned (all checked locally)
};

static std::string toUpper(std::string s) {
  std::transform(s.begin(), s.end(), s.begin(),
                 [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
  return s;
}

static std::string trim(const std::string& s) {
  size_t b = s.find_first_not_of(" \t\r\n");
  if (b == std::string::npos) return "";
  size_t e = s.find_last_not_of(" \t\r\n");
  return s.substr(b, e - b + 1);
}

static std::vector<std::string> splitLines(const std::string& body) {
  std::vector<std::string> lines;
  std::string cur;
  for (char c : body) { // tolerates LF and CRLF (the TS split(/\r?\n/))
    if (c == '\n') {
      if (!cur.empty() && cur.back() == '\r') cur.pop_back();
      lines.push_back(cur);
      cur.clear();
    } else {
      cur += c;
    }
  }
  lines.push_back(cur);
  return lines;
}

/** SHA-1 of a UTF-8 string as uppercase hex (the format HIBP expects).
 *  std::string already carries UTF-8 bytes — no TextEncoder step needed. */
std::string sha1Hex(const std::string& input) {
  unsigned char digest[EVP_MAX_MD_SIZE];
  unsigned int len = 0;
  if (EVP_Digest(input.data(), input.size(), digest, &len, EVP_sha1(), nullptr) != 1) {
    throw std::runtime_error("SHA-1 is not available in this OpenSSL build");
  }
  static const char* HEX = "0123456789ABCDEF";
  std::string out;
  out.reserve(len * 2);
  for (unsigned int i = 0; i < len; i++) {
    out += HEX[digest[i] >> 4];
    out += HEX[digest[i] & 0x0f];
  }
  return out;
}

struct HashSplit {
  std::string prefix; ///< 5 chars
  std::string suffix; ///< 35 chars
};

/** Split a 40-char uppercase hash into the k-anonymity prefix and suffix. */
HashSplit splitHash(const std::string& hash) {
  const std::string h = toUpper(hash);
  return HashSplit{h.substr(0, 5), h.size() > 5 ? h.substr(5) : std::string()};
}

/**
 * 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.
 */
long long parseRangeBody(const std::string& body, const std::string& suffix) {
  if (suffix.empty()) return 0;
  for (const std::string& line : splitLines(body)) {
    const size_t idx = line.find(':');
    if (idx == std::string::npos) continue;
    if (trim(line.substr(0, idx)) == suffix) {
      const std::string countStr = trim(line.substr(idx + 1));
      long long count = 0;
      const char* begin = countStr.c_str();
      char* end = nullptr;
      count = std::strtoll(begin, &end, 10);
      if (end == begin) return 0; // NaN in the TS Number.parseInt sense
      return count < 0 ? 0 : count;
    }
  }
  return 0;
}

/** Count the "SUFFIX:COUNT" candidate lines in a range response. */
long long countCandidates(const std::string& body) {
  long long n = 0;
  for (const std::string& line : splitLines(body)) {
    const size_t idx = line.find(':');
    if (idx != std::string::npos && !trim(line.substr(0, idx)).empty()) n++;
  }
  return n;
}

/**
 * Check a password against the Pwned Passwords corpus using k-anonymity.
 * Only `hashPrefix` is sent over the network; the suffix match is local.
 * Never throws — a failed lookup returns { breached=false, count=-1, error }.
 */
BreachResult checkBreach(const std::string& password, const FetchFn& fetchFn) {
  const HashSplit parts = splitHash(sha1Hex(password));
  BreachResult result;
  result.hashPrefix = parts.prefix;
  result.hashSuffix = parts.suffix;
  result.count = -1;

  FetchResponse response;
  try {
    response = fetchFn(std::string(HIBP_RANGE_URL) + parts.prefix);
  } catch (const std::exception& e) {
    result.error = e.what();
    return result;
  } catch (...) {
    result.error = "Network request failed";
    return result;
  }
  if (!response.ok) {
    result.error = "The breach database returned HTTP " + std::to_string(response.status);
    return result;
  }

  const long long count = parseRangeBody(response.body, parts.suffix);
  result.breached = count > 0;
  result.count = count;
  result.candidates = countCandidates(response.body);
  return result;
}

} // namespace breach

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 →