import { Injectable } from '@nestjs/common'; import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto'; const COST = 32768; function derive(password: string, salt: string): Promise { return new Promise((resolve, reject) => { scrypt( password, salt, 64, { N: COST, r: 8, p: 3, maxmem: 64 * 1024 * 1024 }, (error, key) => (error ? reject(error) : resolve(key)), ); }); } @Injectable() export class PasswordService { async hash(password: string): Promise { const salt = randomBytes(16).toString('hex'); const key = await derive(password, salt); return `scrypt-v1$${salt}$${key.toString('hex')}`; } async verify(password: string, encoded: string): Promise { const [version, salt, hash, extra] = encoded.split('$'); if ( version !== 'scrypt-v1' || !/^[a-f0-9]{32}$/.test(salt ?? '') || !/^[a-f0-9]{128}$/.test(hash ?? '') || extra !== undefined ) return false; return timingSafeEqual( await derive(password, salt), Buffer.from(hash, 'hex'), ); } // Equal-cost work for an unknown account; no dummy credentials are usable. async dummyVerify(password: string): Promise { await derive(password, '00000000000000000000000000000000'); } }