check-pwnedpasswords
Version:
A simple Node.js module that checks against the https://haveibeenpwned.com database through its https://api.pwnedpasswords.com API.
57 lines (51 loc) • 1.65 kB
JavaScript
const crypto = require('crypto');
/**
* Invokes the Have I Been Pwned API v2 endpoint and returns the results.
*
* @param {string} password
* @param {number} timeout
* @return {Promise}
*/
module.exports = async (password, timeout = 5000) => {
const isInvalidPasswordInput = !password || typeof password !== 'string';
const isInvalidTimeoutInput = !timeout || typeof timeout !== 'number' || timeout <= 0 || timeout > Number.MAX_SAFE_INTEGER;
if (isInvalidPasswordInput || isInvalidTimeoutInput) {
throw new Error('Invalid input.');
}
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const range = hash.slice(0, 5);
const remainder = hash.slice(5);
const apiUrl = `https://api.pwnedpasswords.com/range/${range}`;
let fetchTimeout;
try {
const controller = new AbortController();
const signal = controller.signal;
fetchTimeout = setTimeout(() => {
controller.abort();
}, timeout);
const res = await fetch(apiUrl, {signal});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const resultText = await res.text();
const match = resultText.split('\r\n').find((hashRemainder) => hashRemainder.startsWith(remainder));
let pwned = false;
let occurrences = 0;
if (match) {
pwned = true;
occurrences = parseInt(match.split(':')[1], 10);
}
return {
pwned,
occurrences,
};
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Timed out after ${timeout}ms.`);
} else {
throw error;
}
} finally {
clearTimeout(fetchTimeout);
}
};