mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
+maxRetryAttempts, padding & safer error handling. Improved readability & comments.
This commit is contained in:
@@ -1,47 +1,83 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { createHash } from "crypto"; // added types from @types/node
|
import { createHash } from "crypto"; // added types from @types/node
|
||||||
|
|
||||||
export const checkIsPasswordBreached = async (password: string) => {
|
|
||||||
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
|
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
|
||||||
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
|
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
|
||||||
// if there is a match, that password has been involved in a password breach and should NOT be accepted
|
// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
|
||||||
|
// returns a hash table of 800-1000 results
|
||||||
|
// padding has been added to prevent MiTM attacker determining which hash table was called by the response size
|
||||||
|
// the last 35 chars of the password hash are compared client-side against the table
|
||||||
|
// if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted
|
||||||
// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
|
// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
|
||||||
// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
|
// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
|
||||||
|
|
||||||
|
// The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
|
||||||
|
// "When processing requests to establish and change memorized secrets, verifiers SHALL compare
|
||||||
|
// the prospective secrets against a list that contains values known to be commonly-used, expected,
|
||||||
|
// or compromised. For example, the list MAY include, but is not limited to:
|
||||||
|
// • Passwords obtained from previous breach corpuses.
|
||||||
|
// • Dictionary words.
|
||||||
|
// • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
|
||||||
|
// • Context-specific words, such as the name of the service, the username, and derivatives
|
||||||
|
// thereof."
|
||||||
|
|
||||||
|
export const checkIsPasswordBreached = async (password: string) => {
|
||||||
const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP
|
const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP
|
||||||
|
const maxRetryAttempts = 3;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textEncoder = new TextEncoder();
|
const textEncoder = new TextEncoder();
|
||||||
const encodedPwd = textEncoder.encode(password);
|
const encodedPwd = textEncoder.encode(password);
|
||||||
const hash = createHash("sha1").update(encodedPwd).digest();
|
const hash = createHash("sha1").update(encodedPwd).digest();
|
||||||
|
const hashedPwd = hash.toString("hex").toUpperCase();
|
||||||
|
const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five hash chars are sent
|
||||||
|
const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`;
|
||||||
|
|
||||||
|
let response;
|
||||||
|
let retryAttempt = 0;
|
||||||
|
|
||||||
const hashedPwd = Array.from(new Uint8Array(hash))
|
while (retryAttempt < maxRetryAttempts) {
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
try {
|
||||||
.join("")
|
response = await axios.get(rangedHashTableUri, {
|
||||||
.toUpperCase();
|
headers: {
|
||||||
|
"Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ONLY the first five SHA-1 hash chars need to be sent (must be over HTTPS)
|
if (response.status === 200) {
|
||||||
const hashedPwdToSend = hashedPwd.slice(0, 5);
|
break;
|
||||||
|
} else {
|
||||||
const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`);
|
retryAttempt++;
|
||||||
const responseData = response.data.toUpperCase();
|
}
|
||||||
|
} catch (err) {
|
||||||
// compare against the API's ranged db's hash table
|
if (!axios.isAxiosError(err)) {
|
||||||
const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40));
|
throw err;
|
||||||
|
}
|
||||||
// Clear the hashed password from memory as a precaution
|
retryAttempt++;
|
||||||
const zeroBuffer = new Uint8Array(encodedPwd.length);
|
}
|
||||||
encodedPwd.set(zeroBuffer);
|
|
||||||
|
|
||||||
return isBreachedPassword; // boolean: true indicates the password has been involved in a data breach
|
|
||||||
} catch (err: any) {
|
|
||||||
if (axios.isAxiosError(err) && err.response && err.response.status === 429) {
|
|
||||||
console.error("Received a 429 response from the Pwnd Passwords API");
|
|
||||||
// Handle the 429 error here (not 100% sure what the rate limits are for the password API but looks like <10 calls/min)
|
|
||||||
// an error here should probably not cause the setting/resetting/changing password to fail (unless desired)
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response && response.status === 200) {
|
||||||
|
const responseData = response.data.toUpperCase();
|
||||||
|
// compare last 35 hash chars to the returned ranged hash table
|
||||||
|
// returns a boolean: true indicates the password has been involved in a data breach (ie. pwnd)
|
||||||
|
const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40));
|
||||||
|
|
||||||
|
// Clear the hashed password from memory as a precaution
|
||||||
|
const zeroBuffer = new Uint8Array(encodedPwd.length);
|
||||||
|
encodedPwd.set(zeroBuffer);
|
||||||
|
|
||||||
|
return isBreachedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API`
|
||||||
|
);
|
||||||
|
return false; // better to return a safe response if no breach can be determined
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("An unexpected error has occurred:", err.message);
|
||||||
|
return false; // Return a safe response in case of unexpected errors
|
||||||
|
// the HIBP API could return 400 (empty string supplied), 429 or 503 if Cloudflare edge node is down)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user