import { Injectable } from "@nestjs/common"; import * as argon2 from "argon2"; /** * Argon2id password hashing with versioned parameters (ADR-0005). Bumping * CURRENT_ALGO_VERSION and its entry in PARAMS_BY_VERSION lets us raise * cost over time without invalidating existing hashes — verification * always uses the parameters embedded in argon2's own encoded hash * string, and a stored user is only re-hashed with the new parameters on * their next successful login (see needsRehash()). */ const PARAMS_BY_VERSION: Record = { 1: { type: argon2.argon2id, memoryCost: 19456, // 19 MiB — OWASP minimum recommendation for argon2id timeCost: 2, parallelism: 1, }, }; export const CURRENT_ALGO_VERSION = 1; @Injectable() export class PasswordService { async hash(plain: string): Promise<{ hash: string; algoVersion: number }> { const params = PARAMS_BY_VERSION[CURRENT_ALGO_VERSION]; const hash = await argon2.hash(plain, params); return { hash, algoVersion: CURRENT_ALGO_VERSION }; } async verify(hash: string, plain: string): Promise { try { return await argon2.verify(hash, plain); } catch { // Malformed/foreign hash — treat as verification failure, never throw // into the caller (which would otherwise leak a 500 instead of 401). return false; } } needsRehash(algoVersion: number): boolean { return algoVersion !== CURRENT_ALGO_VERSION; } }