42 lines
1.3 KiB
TypeScript
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');
|
|
}
|
|
}
|