manicanldes-backend/src/identity/password.service.ts

42 lines
1.3 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
const COST = 32768;
function derive(password: string, salt: string): Promise<Buffer> {
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<string> {
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<boolean> {
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<void> {
await derive(password, '00000000000000000000000000000000');
}
}