uptime-kuma/server/password-util.js
2026-01-18 17:46:53 +01:00

97 lines
3.1 KiB
JavaScript

/**
* Password validation utility following NIST SP 800-63B guidelines
* @module password-util
*/
const crypto = require("crypto");
const axios = require("axios");
/**
* Minimum password length as per NIST recommendations
*/
const MIN_PASSWORD_LENGTH = 12;
/**
* Check if password appears in Have I Been Pwned database using k-anonymity
* @param {string} password - The password to check
* @returns {Promise<{ breached: boolean, count: number }>} Whether password is breached and count
*/
async function checkPasswordBreached(password) {
try {
// Generate SHA-1 hash of password
const hash = crypto.createHash("sha1").update(password).digest("hex").toUpperCase();
const prefix = hash.substring(0, 5);
const suffix = hash.substring(5);
// Query HIBP API with first 5 characters (k-anonymity)
const response = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`, {
timeout: 5000,
});
// Check if our hash suffix appears in the response
const hashes = response.data.split("\r\n");
for (const line of hashes) {
const [hashSuffix, count] = line.split(":");
if (hashSuffix === suffix) {
return { breached: true, count: parseInt(count, 10) };
}
}
return { breached: false, count: 0 };
} catch (error) {
// If HIBP is unavailable, don't block the password
console.warn("Failed to check password against HIBP:", error.message);
return { breached: false, count: 0 };
}
}
/**
* Validates a password according to NIST SP 800-63B guidelines.
*
* NIST guidelines state:
* - Passwords should have a minimum length (8-12 characters recommended)
* - Composition rules (requiring specific character types) SHALL NOT be imposed
* - All printable ASCII characters and Unicode characters should be allowed
*
* This implementation enforces only minimum length, allowing all character compositions.
* @param {string} password - The password to validate
* @param {boolean} checkBreached - Whether to check against breach database (optional, default: false)
* @returns {Promise<{ ok: boolean, msg?: string, warning?: string }>} Validation result
*/
async function validatePassword(password, checkBreached = false) {
if (!password) {
return {
ok: false,
msg: "Password cannot be empty"
};
}
if (password.length < MIN_PASSWORD_LENGTH) {
return {
ok: false,
msg: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long`
};
}
// Optional: Check against breach database (non-blocking warning)
if (checkBreached) {
const breachResult = await checkPasswordBreached(password);
if (breachResult.breached) {
return {
ok: true,
warning: {msg: "passwordFoundInDataBreach", meta: breachResult.count}
};
}
}
return {
ok: true
};
}
module.exports = {
validatePassword,
checkPasswordBreached,
MIN_PASSWORD_LENGTH
};