import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { createDecipheriv, hkdfSync } from "node:crypto"; const ALGORITHM = "aes-256-gcm"; const ENCRYPTED_PREFIX = "enc:"; const IV_LENGTH = 12; const AUTH_TAG_LENGTH = 16; const DERIVED_KEY_LENGTH = 32; const HKDF_SALT = "mosaic.crypto.v1"; const HKDF_INFO = "mosaic-db-secret-encryption"; @Injectable() export class EncryptionService { private key: Buffer | null = null; constructor(private readonly configService: ConfigService) {} decryptIfNeeded(value: string): string { if (!this.isEncrypted(value)) { return value; } return this.decrypt(value); } decrypt(encrypted: string): string { if (!this.isEncrypted(encrypted)) { throw new Error("Value is not encrypted"); } const payloadBase64 = encrypted.slice(ENCRYPTED_PREFIX.length); try { const payload = Buffer.from(payloadBase64, "base64"); if (payload.length < IV_LENGTH + AUTH_TAG_LENGTH) { throw new Error("Encrypted payload is too short"); } const iv = payload.subarray(0, IV_LENGTH); const authTag = payload.subarray(payload.length - AUTH_TAG_LENGTH); const ciphertext = payload.subarray(IV_LENGTH, payload.length - AUTH_TAG_LENGTH); const decipher = createDecipheriv(ALGORITHM, this.getOrCreateKey(), iv); decipher.setAuthTag(authTag); return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"); } catch { throw new Error("Failed to decrypt value"); } } isEncrypted(value: string): boolean { return value.startsWith(ENCRYPTED_PREFIX); } private getOrCreateKey(): Buffer { if (this.key !== null) { return this.key; } const secret = this.configService.get("MOSAIC_SECRET_KEY"); if (!secret) { throw new Error( "orchestrator: MOSAIC_SECRET_KEY is required. Set it in your config or via MOSAIC_SECRET_KEY." ); } if (secret.length < 32) { throw new Error("MOSAIC_SECRET_KEY must be at least 32 characters"); } this.key = Buffer.from( hkdfSync( "sha256", Buffer.from(secret, "utf8"), Buffer.from(HKDF_SALT, "utf8"), Buffer.from(HKDF_INFO, "utf8"), DERIVED_KEY_LENGTH ) ); return this.key; } }