Files
jayjennings c69eccb5c1
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
feat: implement incident reporting and management system
- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents.
- Implement visibility rules for users based on roles (reporters, supervisors).
- Create tests for incident reporting functionality to ensure correctness and reliability.
- Introduce site and area management routes, including validation for area hierarchy.
- Establish user management routes with role-based access control for creating and updating users.
- Add setup routes for initializing the application with an organization and admin user.
- Implement shared enums and schemas for consistent validation and data handling across the application.
- Create a test helper for setting up an in-memory database and API client for testing.
- Add TypeScript configuration for server-side code with strict type checking and module resolution.
2026-10-06 10:51:18 -07:00

35 lines
1.7 KiB
TypeScript

import { randomBytes, scrypt, timingSafeEqual, type ScryptOptions } from 'node:crypto'
// scrypt parameters from the OWASP Password Storage Cheat Sheet (N=2^15, r=8, p=3: ~32 MiB per hash).
const params = { N: 2 ** 15, r: 8, p: 3 }
const keyLength = 64
function derive(password: string, salt: Buffer, options: ScryptOptions): Promise<Buffer> {
return new Promise((resolve, reject) =>
scrypt(password.normalize('NFKC'), salt, keyLength, { ...options, maxmem: 64 * 1024 * 1024 }, (error, key) => (error ? reject(error) : resolve(key))),
)
}
/** Returns `scrypt$N$r$p$salt$hash` so parameters can be raised later without breaking old hashes. */
export async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16)
const key = await derive(password, salt, params)
return ['scrypt', params.N, params.r, params.p, salt.toString('base64'), key.toString('base64')].join('$')
}
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
const [algorithm, N, r, p, salt, hash] = stored.split('$')
if (algorithm !== 'scrypt' || !salt || !hash) return false
const expected = Buffer.from(hash, 'base64')
const actual = await derive(password, Buffer.from(salt, 'base64'), { N: Number(N), r: Number(r), p: Number(p) })
return actual.length === expected.length && timingSafeEqual(actual, expected)
}
/** Burns comparable time when the user doesn't exist, so login timing doesn't reveal valid emails. */
let dummyHash: Promise<string> | undefined
export async function verifyAgainstDummy(password: string): Promise<false> {
dummyHash ??= hashPassword('dummy-password-for-timing')
await verifyPassword(password, await dummyHash)
return false
}