Queue password checks and shorten busy retries (Refs #95)
CI / checks (push) Successful in 4m36s
CI / checks (push) Successful in 4m36s
This commit is contained in:
@@ -3,7 +3,51 @@ import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
||||
// OWASP's scrypt baseline: 128 MiB, r=8, p=1. Bound concurrent work so
|
||||
// unauthenticated password attempts cannot allocate unlimited memory.
|
||||
const parameters = { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
const maxActive = 2;
|
||||
const maxQueued = 16;
|
||||
const queueTimeoutMs = 2000;
|
||||
let active = 0;
|
||||
/** @type {Array<{resolve: () => void, timer: ReturnType<typeof setTimeout>}>} */
|
||||
const queue = [];
|
||||
|
||||
export class PasswordServiceBusyError extends Error {
|
||||
constructor() {
|
||||
super('Password service is busy. Try again shortly.');
|
||||
this.name = 'PasswordServiceBusyError';
|
||||
this.retryAfterSeconds = 2;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
function acquireSlot() {
|
||||
if (active < maxActive) {
|
||||
active++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Bound waiting requests as well as running hashes during sustained overload.
|
||||
if (queue.length >= maxQueued) return Promise.reject(new PasswordServiceBusyError());
|
||||
return new Promise((resolve, reject) => {
|
||||
const entry = {
|
||||
resolve,
|
||||
timer: setTimeout(() => {
|
||||
queue.splice(queue.indexOf(entry), 1);
|
||||
reject(new PasswordServiceBusyError());
|
||||
}, queueTimeoutMs)
|
||||
};
|
||||
queue.push(entry);
|
||||
});
|
||||
}
|
||||
|
||||
function releaseSlot() {
|
||||
const next = queue.shift();
|
||||
if (next) {
|
||||
clearTimeout(next.timer);
|
||||
// Transfer the occupied slot directly so new requests cannot jump the queue.
|
||||
next.resolve();
|
||||
} else {
|
||||
active--;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {unknown} password */
|
||||
export function validatePassword(password) {
|
||||
@@ -14,17 +58,18 @@ export function validatePassword(password) {
|
||||
}
|
||||
|
||||
/** @param {string} password @param {string} salt @returns {Promise<Buffer>} */
|
||||
function derive(password, salt) {
|
||||
if (active >= 2)
|
||||
return Promise.reject(new Error('Password service is busy. Try again shortly.'));
|
||||
active++;
|
||||
return new Promise((resolve, reject) => {
|
||||
scrypt(password, salt, 64, parameters, (error, result) => {
|
||||
active--;
|
||||
if (error) reject(error);
|
||||
else resolve(result);
|
||||
async function derive(password, salt) {
|
||||
await acquireSlot();
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
scrypt(password, salt, 64, parameters, (error, result) => {
|
||||
if (error) reject(error);
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {unknown} password */
|
||||
|
||||
Reference in New Issue
Block a user