93 lines
2.9 KiB
JavaScript
93 lines
2.9 KiB
JavaScript
const crypto = require('crypto');
|
|
const axios = require('axios');
|
|
|
|
class HaveIBeenPwnedAPI {
|
|
constructor(config = {}) {
|
|
this.baseUrl = 'https://api.pwnedpasswords.com';
|
|
this.userAgent = config.userAgent || 'PasswordSecurityChecker';
|
|
}
|
|
|
|
/**
|
|
* Generates a SHA-1 hash of the password
|
|
* @param {string} password - The password to hash
|
|
* @returns {string} The uppercase SHA-1 hash
|
|
*/
|
|
generateHash(password) {
|
|
return crypto
|
|
.createHash('sha1')
|
|
.update(password)
|
|
.digest('hex')
|
|
.toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Checks if a password has been exposed in known data breaches
|
|
* @param {string} password - The password to check
|
|
* @returns {Promise<{isCompromised: boolean, timesExposed: number}>}
|
|
*/
|
|
async checkPassword(password) {
|
|
try {
|
|
// Generate hash and get first 5 characters for k-anonymity
|
|
const hash = this.generateHash(password);
|
|
const hashPrefix = hash.substring(0, 5);
|
|
const hashSuffix = hash.substring(5);
|
|
|
|
// Make request to HIBP API
|
|
const response = await axios.get(`${this.baseUrl}/range/${hashPrefix}`, {
|
|
headers: {
|
|
'User-Agent': this.userAgent
|
|
}
|
|
});
|
|
|
|
// Parse response and check if password hash suffix exists
|
|
const hashes = response.data.split('\n');
|
|
const match = hashes.find(h => h.split(':')[0] === hashSuffix);
|
|
|
|
if (match) {
|
|
const timesExposed = parseInt(match.split(':')[1]);
|
|
return {
|
|
isCompromised: true,
|
|
timesExposed
|
|
};
|
|
}
|
|
|
|
return {
|
|
isCompromised: false,
|
|
timesExposed: 0
|
|
};
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to check password: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates a password against HIBP and custom rules
|
|
* @param {string} password - The password to validate
|
|
* @param {Object} options - Validation options
|
|
* @param {number} options.maxExposures - Maximum allowed exposures (default: 0)
|
|
* @returns {Promise<{isValid: boolean, reason?: string}>}
|
|
*/
|
|
async validatePassword(password, options = { maxExposures: 0 }) {
|
|
try {
|
|
const result = await this.checkPassword(password);
|
|
|
|
if (result.timesExposed > options.maxExposures) {
|
|
return {
|
|
isValid: false,
|
|
reason: `Password has been exposed ${result.timesExposed} times in data breaches`
|
|
};
|
|
}
|
|
|
|
return {
|
|
isValid: true
|
|
};
|
|
|
|
} catch (error) {
|
|
throw new Error(`Password validation failed: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = HaveIBeenPwnedAPI;
|