import { ValueTransformer } from "typeorm"; import * as crypto from "crypto"; const ENCRYPTION_KEY = process.env.SECRET_ENCRYPTION_KEY || "clave-32-bytes-segura"; const ALGORITHM = "aes-256-cbc"; // KeyObject de 32 bytes const KEY = crypto.createSecretKey( crypto.createHash("sha256").update(String(ENCRYPTION_KEY)).digest() ); function looksLikeCBC(encoded: string) { // formato: iv:cipherHex return /^[0-9a-fA-F]+:[0-9a-fA-F]+$/.test(encoded); } function decryptValue(value: any): any { if (typeof value !== "string" || !looksLikeCBC(value)) return value; const [ivHex, contentHex] = value.split(":"); const iv = Buffer.from(ivHex, "hex"); const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv); const dec = Buffer.concat([ decipher.update(Buffer.from(contentHex, "hex")), decipher.final(), ]); return dec.toString("utf8"); } function encryptValue(plain: any): string | null { if (plain === null || plain === undefined) return null; const text = String(plain); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv); const enc = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]); return iv.toString("hex") + ":" + enc.toString("hex"); } /** Recorre profundo y encripta solo claves `encrypted_*` */ function deepEncrypt(input: any): any { if (input === null || input === undefined) return input; if (Array.isArray(input)) { const arr: any[] = []; for (let i = 0; i < input.length; i++) { arr[i] = deepEncrypt(input[i]); } return arr; } if (typeof input === "object") { const out: { [k: string]: any } = {}; for (const k in input) { const v = input[k]; if (k.length >= 10 && k.substr(0, 10) === "encrypted_") { out[k] = encryptValue(v); } else { out[k] = deepEncrypt(v); } } return out; } // primitivos return input; } /** Recorre profundo y desencripta solo claves `encrypted_*` */ function deepDecrypt(input: any): any { if (input === null || input === undefined) return input; if (Array.isArray(input)) { const arr: any[] = []; for (let i = 0; i < input.length; i++) { arr[i] = deepDecrypt(input[i]); } return arr; } if (typeof input === "object") { const out: { [k: string]: any } = {}; for (const k in input) { const v = input[k]; if (k.length >= 10 && k.substr(0, 10) === "encrypted_") { try { out[k] = decryptValue(v); } catch { out[k] = v; // si está corrupto, lo dejamos como está } } else { out[k] = deepDecrypt(v); } } return out; } // primitivos return input; } export const JsonEncryptionTransformer: ValueTransformer = { to(value: any): string { if (typeof value !== "object" || value === null) return JSON.stringify({}); return JSON.stringify(deepEncrypt(value)); }, from(dbValue: string): any { try { const parsed = JSON.parse(dbValue); return deepDecrypt(parsed); } catch { // si no es JSON válido, no tumbar todo return {}; } }, };