fix(19): WR-03 make scrypt hashing async (threadpool) to avoid event-loop starvation DoS

This commit is contained in:
Lucas Berger
2026-06-17 20:30:28 -04:00
parent 322929aebe
commit 30ad25c026
8 changed files with 81 additions and 42 deletions
+18 -16
View File
@@ -4,6 +4,9 @@
* Uses node:crypto scrypt under the hood; no external dependencies.
* All tests run without MariaDB or any external service.
*
* WR-03: hashPassword / verifyPassword are now async (promisify(scrypt), threadpool) —
* all assertions await them.
*
* Test suite (TDD RED → GREEN — Plan 19-01 Task 1):
* Test 1: correct password verifies true
* Test 2: wrong password verifies false
@@ -16,33 +19,32 @@ import { describe, it, expect } from 'vitest';
import { hashPassword, verifyPassword } from '../../src/auth/localCredentials.js';
describe('hashPassword / verifyPassword', () => {
it('Test 1: verifyPassword(hashPassword(pw), pw) === true (round-trip)', () => {
const encoded = hashPassword('hunter2');
const result = verifyPassword(encoded, 'hunter2');
it('Test 1: verifyPassword(hashPassword(pw), pw) === true (round-trip)', async () => {
const encoded = await hashPassword('hunter2');
const result = await verifyPassword(encoded, 'hunter2');
expect(result).toBe(true);
});
it('Test 2: verifyPassword(hashPassword(pw), wrong) === false', () => {
const encoded = hashPassword('hunter2');
const result = verifyPassword(encoded, 'wrong-password');
it('Test 2: verifyPassword(hashPassword(pw), wrong) === false', async () => {
const encoded = await hashPassword('hunter2');
const result = await verifyPassword(encoded, 'wrong-password');
expect(result).toBe(false);
});
it('Test 3: two hashPassword calls on same input produce different encoded strings (unique salt)', () => {
const encoded1 = hashPassword('x');
const encoded2 = hashPassword('x');
it('Test 3: two hashPassword calls on same input produce different encoded strings (unique salt)', async () => {
const encoded1 = await hashPassword('x');
const encoded2 = await hashPassword('x');
expect(encoded1).not.toBe(encoded2);
});
it('Test 4: verifyPassword returns false (never throws) on a malformed stored hash', () => {
expect(() => verifyPassword('not-a-valid-hash', 'x')).not.toThrow();
expect(verifyPassword('not-a-valid-hash', 'x')).toBe(false);
expect(verifyPassword('', 'x')).toBe(false);
expect(verifyPassword('scrypt$bad$data', 'x')).toBe(false);
it('Test 4: verifyPassword returns false (never throws) on a malformed stored hash', async () => {
await expect(verifyPassword('not-a-valid-hash', 'x')).resolves.toBe(false);
await expect(verifyPassword('', 'x')).resolves.toBe(false);
await expect(verifyPassword('scrypt$bad$data', 'x')).resolves.toBe(false);
});
it('Test 5: encoded string has scrypt$N$r$p$salt$hash shape (6 $-delimited segments)', () => {
const encoded = hashPassword('testpassword');
it('Test 5: encoded string has scrypt$N$r$p$salt$hash shape (6 $-delimited segments)', async () => {
const encoded = await hashPassword('testpassword');
const segments = encoded.split('$');
expect(segments).toHaveLength(6);
expect(segments[0]).toBe('scrypt');