import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; import { Inject, Injectable } from "@nestjs/common"; import type { EdenEnv } from "@eden/config"; import { EDEN_ENV } from "../config/config.constants.js"; const KEY_VERSION = "v1"; const IV_LENGTH_BYTES = 12; // 96 bits — AES-GCM recommended nonce size /** * Generic reversible field encryption (ADR-0005) — AES-256-GCM, random IV * per operation, authenticated (tampering makes decrypt() throw). Stored * format mirrors the legacy's `iv:tag:ciphertext` scheme with an added key * version prefix so a future root-key rotation can be applied incrementally * instead of re-encrypting everything at once: `v1:iv:tag:ciphertext` (all * three payload segments base64). * * Root key comes from EDEN_FIELD_ENCRYPTION_KEY (env-only, never in the * database) and is hashed to exactly 32 bytes via SHA-256 so any * sufficiently-long passphrase works as AES-256 key material. */ @Injectable() export class FieldEncryptionService { private readonly key: Buffer | null; constructor(@Inject(EDEN_ENV) env: EdenEnv) { this.key = env.EDEN_FIELD_ENCRYPTION_KEY ? createHash("sha256").update(env.EDEN_FIELD_ENCRYPTION_KEY).digest() : null; } encrypt(plaintext: string): string { const key = this.requireKey(); const iv = randomBytes(IV_LENGTH_BYTES); const cipher = createCipheriv("aes-256-gcm", key, iv); const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return [KEY_VERSION, iv.toString("base64"), tag.toString("base64"), ciphertext.toString("base64")].join(":"); } decrypt(stored: string): string { const key = this.requireKey(); const [version, ivB64, tagB64, ciphertextB64] = stored.split(":"); if (version !== KEY_VERSION || !ivB64 || !tagB64 || !ciphertextB64) { throw new Error("Formato de campo cifrado inválido ou versão de chave desconhecida"); } const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivB64, "base64")); decipher.setAuthTag(Buffer.from(tagB64, "base64")); const plaintext = Buffer.concat([ decipher.update(Buffer.from(ciphertextB64, "base64")), decipher.final(), ]); return plaintext.toString("utf8"); } private requireKey(): Buffer { if (!this.key) { throw new Error("EDEN_FIELD_ENCRYPTION_KEY não configurada — obrigatória para criptografia de campo reversível"); } return this.key; } }