forked from Yara724/api
YARA-1035
This commit is contained in:
167
src/features/auth/providers/hash.provider.ts
Normal file
167
src/features/auth/providers/hash.provider.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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 {
|
||||
CURRENT_ALGORITHM,
|
||||
CURRENT_VERSION,
|
||||
KEY_LENGTH,
|
||||
KEY_LENGTH_FOR_OTP,
|
||||
SCRYPT_PARAMS,
|
||||
SCRYPT_PARAMS_FOR_OTP,
|
||||
} from "src/common/auth/constants";
|
||||
import { PasswordHash } from "src/common/auth/types";
|
||||
|
||||
const scryptAsync = promisify<
|
||||
BinaryLike,
|
||||
BinaryLike,
|
||||
number,
|
||||
ScryptOptions,
|
||||
Buffer
|
||||
>(scrypt);
|
||||
|
||||
@Injectable()
|
||||
export class HashService {
|
||||
private readonly pepper: string;
|
||||
private readonly current_alogrithm: string;
|
||||
private readonly current_version: number;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.pepper = this.configService.get<string>("HASH_PEPPER") ?? "";
|
||||
this.current_alogrithm = CURRENT_ALGORITHM;
|
||||
this.current_version = CURRENT_VERSION;
|
||||
}
|
||||
|
||||
private applyPepper(input: string): string {
|
||||
return this.pepper ? `${input}${this.pepper}` : input;
|
||||
}
|
||||
|
||||
private generateSalt(bytes: number = 16): string {
|
||||
return randomBytes(bytes).toString("hex");
|
||||
}
|
||||
|
||||
private isLegacyPasswordFormat(stored: unknown): stored is string {
|
||||
return typeof stored === "string" && stored.includes(":");
|
||||
}
|
||||
|
||||
private isVersionedPasswordFormat(stored: unknown): stored is PasswordHash {
|
||||
return (
|
||||
typeof stored === "object" &&
|
||||
stored !== null &&
|
||||
"hash" in stored &&
|
||||
"salt" in stored &&
|
||||
"algorithm" in stored &&
|
||||
"version" in stored
|
||||
);
|
||||
}
|
||||
|
||||
private async scrypt(input: string): Promise<[string, string]> {
|
||||
const salt = this.generateSalt();
|
||||
const pepperedInput = this.applyPepper(input);
|
||||
const derived = await scryptAsync(
|
||||
pepperedInput,
|
||||
salt,
|
||||
KEY_LENGTH,
|
||||
SCRYPT_PARAMS,
|
||||
);
|
||||
return [salt, derived.toString("hex")];
|
||||
}
|
||||
|
||||
private async verifyScrypt(
|
||||
password: string,
|
||||
salt: string,
|
||||
hashHex: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const pepperedInput = this.applyPepper(password);
|
||||
const derived = await scryptAsync(
|
||||
pepperedInput,
|
||||
salt,
|
||||
KEY_LENGTH,
|
||||
SCRYPT_PARAMS,
|
||||
);
|
||||
const storedBuffer = Buffer.from(hashHex, "hex");
|
||||
if (derived.length !== storedBuffer.length) return false;
|
||||
return timingSafeEqual(derived, storedBuffer);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async needsRehash(stored: string | PasswordHash): Promise<boolean> {
|
||||
if (this.isLegacyPasswordFormat(stored)) return true;
|
||||
if (!this.isVersionedPasswordFormat(stored)) return true;
|
||||
|
||||
return (
|
||||
stored.algorithm !== this.current_alogrithm ||
|
||||
stored.version !== this.current_version
|
||||
);
|
||||
}
|
||||
|
||||
async hashPassword(password: string): Promise<PasswordHash> {
|
||||
const [salt, hash] = await this.scrypt(password);
|
||||
|
||||
return {
|
||||
hash,
|
||||
salt,
|
||||
algorithm: CURRENT_ALGORITHM,
|
||||
version: CURRENT_VERSION,
|
||||
params: { ...SCRYPT_PARAMS, keyLength: KEY_LENGTH },
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
async verifyPassword(
|
||||
password: string,
|
||||
stored: string | PasswordHash,
|
||||
): Promise<boolean> {
|
||||
if (this.isLegacyPasswordFormat(stored)) {
|
||||
const [salt, hash] = stored.split(":");
|
||||
return this.verifyScrypt(password, salt, hash);
|
||||
}
|
||||
|
||||
if (!this.isVersionedPasswordFormat(stored)) return false;
|
||||
|
||||
switch (stored.algorithm) {
|
||||
case "scrypt":
|
||||
return this.verifyScrypt(password, stored.salt, stored.hash);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hashOtp(otp: string): Promise<string> {
|
||||
const salt = this.generateSalt(8);
|
||||
const derived = await scryptAsync(
|
||||
otp,
|
||||
salt,
|
||||
KEY_LENGTH_FOR_OTP,
|
||||
SCRYPT_PARAMS_FOR_OTP,
|
||||
);
|
||||
return `${salt}:${derived.toString("hex")}`;
|
||||
}
|
||||
|
||||
async verifyOtp(otp: string, stored: string): Promise<boolean> {
|
||||
const [salt, hashHex] = stored.split(":");
|
||||
if (!salt || !hashHex) return false;
|
||||
try {
|
||||
const derived = await scryptAsync(
|
||||
otp,
|
||||
salt,
|
||||
KEY_LENGTH_FOR_OTP,
|
||||
SCRYPT_PARAMS_FOR_OTP,
|
||||
);
|
||||
const storedBuffer = Buffer.from(hashHex, "hex");
|
||||
if (derived.length !== storedBuffer.length) return false;
|
||||
return timingSafeEqual(derived, storedBuffer);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user