import { BinaryLike, randomBytes, scrypt, ScryptOptions, timingSafeEqual, } from "node:crypto"; import { promisify } from "node:util"; import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { KEY_LENGTH, SCRYPT_PARAMS } from "../constants/hash.constants"; const scryptAsync = promisify< BinaryLike, BinaryLike, number, ScryptOptions, Buffer >(scrypt); @Injectable() export class HashService { private readonly pepper: string; constructor(private readonly configService: ConfigService) { this.pepper = this.configService.get("HASH_PEPPER"); } private applyPepper(input: string): string { return this.pepper ? `${input}${this.pepper}` : input; } private generateSalt(bytes: number = 16): string { return randomBytes(bytes).toString("hex"); } private async scrypt(password: string): Promise<[string, string]> { const salt = this.generateSalt(); const pepperedInput = this.applyPepper(password); const hashedPassword = await scryptAsync( pepperedInput, salt, KEY_LENGTH, SCRYPT_PARAMS, ); return [salt, hashedPassword.toString("hex")]; } private async verifyScrypt( password: string, salt: string, hashedPassword: string, ): Promise { const pepperedInput = this.applyPepper(password); const derived = await scryptAsync( pepperedInput, salt, KEY_LENGTH, SCRYPT_PARAMS, ); const storedBuffer = Buffer.from(hashedPassword, "hex"); if (derived.length !== storedBuffer.length) return false; return timingSafeEqual(derived, storedBuffer); } }