import { createHash } from "node:crypto"; const HIBP_TIMEOUT_MS = 3000; /** * Prueft ueber die Have-I-Been-Pwned-API (k-Anonymitaet: nur die ersten 5 * Hex-Zeichen des SHA-1-Hashes verlassen den Server, niemals das Passwort * selbst oder der volle Hash) ob ein Passwort in bekannten Datenlecks * vorkommt. Bei Netzwerk- oder Zeitueberschreitungsfehlern wird bewusst * "nicht kompromittiert" zurueckgegeben (fail-open) — ein Ausfall des * externen Diensts darf Registrierung oder Passwort-Reset nicht blockieren. */ export async function isPasswordBreached(password: string): Promise { try { const sha1 = createHash("sha1").update(password, "utf8").digest("hex").toUpperCase(); const prefix = sha1.slice(0, 5); const suffix = sha1.slice(5); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), HIBP_TIMEOUT_MS); try { const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, { signal: controller.signal, headers: { "Add-Padding": "true" }, }); if (!response.ok) return false; const body = await response.text(); return body.split("\n").some((line) => { const [lineSuffix, count] = line.trim().split(":"); return lineSuffix === suffix && Number(count) > 0; }); } finally { clearTimeout(timeout); } } catch { return false; } }