- 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.
35 lines
1.7 KiB
TypeScript
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
|
|
}
|