import { createCipheriv, randomBytes, scrypt as nodeScrypt, timingSafeEqual } from "node:crypto"; import { promisify } from "node:util"; const scrypt = promisify(nodeScrypt); const LOCAL_PASSWORD_KEY_LENGTH = 64; export type LocalPasswordHash = { hash: string; salt: string; }; export async function hashLocalPassword(password: string, salt = randomBytes(16).toString("hex")): Promise { const derived = await scrypt(password, salt, LOCAL_PASSWORD_KEY_LENGTH) as Buffer; return { hash: derived.toString("hex"), salt }; } export async function verifyLocalPassword(password: string, hash: string, salt: string): Promise { if (!password || !hash || !salt) return false; const derived = await scrypt(password, salt, LOCAL_PASSWORD_KEY_LENGTH) as Buffer; const expected = Buffer.from(hash, "hex"); return expected.length === derived.length && timingSafeEqual(expected, derived); } export function prepareAuthPassword(password: string, input: { passwordEncrypted?: boolean; passwordEncryptionKey?: string; }): string { if (input.passwordEncrypted) return password; const key = input.passwordEncryptionKey?.trim(); if (!key) return password; return encryptPasswordCFB(password, key); } export function encryptPasswordCFB(password: string, key: string): string { const keyBytes = Buffer.from(key); if (![16, 24, 32].includes(keyBytes.length)) { throw new Error("password encryption key must be 16, 24, or 32 bytes"); } const algorithm = `aes-${keyBytes.length * 8}-cfb`; const cipher = createCipheriv(algorithm, keyBytes, keyBytes); return Buffer.concat([cipher.update(password, "utf8"), cipher.final()]).toString("base64"); }