import crypto from "crypto"; import { env } from "../config/env"; const PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*"; type EncryptedEnvelope = { __enc: "v1"; iv: string; tag: string; data: string; }; function getEncryptionKey() { const seed = env.SETTINGS_ENCRYPTION_KEY ?? env.JWT_SECRET; return crypto.createHash("sha256").update(seed).digest(); } function normalizeSecret(value: string) { return crypto.createHash("sha256").update(value).digest("hex"); } export function generateSecurePassword(length = 20) { const bytes = crypto.randomBytes(length * 2); let output = ""; for (let i = 0; i < bytes.length && output.length < length; i += 1) { output += PASSWORD_ALPHABET[bytes[i] % PASSWORD_ALPHABET.length]; } return output; } export function hashToken(token: string) { return normalizeSecret(token); } export function timingSafeEqualHash(candidate: string, storedHash: string) { const candidateHash = Buffer.from(normalizeSecret(candidate), "utf8"); const knownHash = Buffer.from(storedHash, "utf8"); if (candidateHash.length !== knownHash.length) return false; return crypto.timingSafeEqual(candidateHash, knownHash); } export function generateRecoveryCodes(count = 8) { const codes: string[] = []; for (let i = 0; i < count; i += 1) { const raw = crypto.randomBytes(5).toString("hex").toUpperCase(); codes.push(`${raw.slice(0, 5)}-${raw.slice(5, 10)}`); } return codes; } export function hashRecoveryCodes(codes: string[]) { return codes.map((code) => normalizeSecret(code.trim().toUpperCase())); } export function consumeRecoveryCode(input: string, hashes: string[]) { const normalized = normalizeSecret(input.trim().toUpperCase()); const matchIndex = hashes.findIndex((hash) => hash === normalized); if (matchIndex < 0) { return { matched: false, remainingHashes: hashes }; } const remainingHashes = [...hashes.slice(0, matchIndex), ...hashes.slice(matchIndex + 1)]; return { matched: true, remainingHashes }; } function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope { return ( typeof value === "object" && value !== null && (value as Record).__enc === "v1" && typeof (value as Record).iv === "string" && typeof (value as Record).tag === "string" && typeof (value as Record).data === "string" ); } export function encryptJson(value: unknown): EncryptedEnvelope { const key = getEncryptionKey(); const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); const payload = Buffer.from(JSON.stringify(value), "utf8"); const encrypted = Buffer.concat([cipher.update(payload), cipher.final()]); const tag = cipher.getAuthTag(); return { __enc: "v1", iv: iv.toString("base64"), tag: tag.toString("base64"), data: encrypted.toString("base64") }; } export function decryptJson(value: unknown): T { if (!isEncryptedEnvelope(value)) { return value as T; } const key = getEncryptionKey(); const iv = Buffer.from(value.iv, "base64"); const tag = Buffer.from(value.tag, "base64"); const encrypted = Buffer.from(value.data, "base64"); const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); decipher.setAuthTag(tag); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8"); return JSON.parse(decrypted) as T; }