/** * Crypto Service * * Handles encryption/decryption for sensitive data. */ import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; @Injectable() export class CryptoService { private readonly logger = new Logger(CryptoService.name); private readonly algorithm = "aes-256-gcm"; private readonly encryptionKey: Buffer; constructor(private readonly config: ConfigService) { const keyHex = this.config.get("ENCRYPTION_KEY"); if (!keyHex) { throw new Error("ENCRYPTION_KEY environment variable is required for private key encryption"); } // Validate key is 64 hex characters (32 bytes for AES-256) if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) { throw new Error("ENCRYPTION_KEY must be 64 hexadecimal characters (32 bytes)"); } this.encryptionKey = Buffer.from(keyHex, "hex"); // Validate key works by performing encrypt/decrypt round-trip // This prevents silent data loss if the key is changed after data is encrypted try { const testValue = "encryption_key_validation_test"; const encrypted = this.encrypt(testValue); const decrypted = this.decrypt(encrypted); if (decrypted !== testValue) { throw new Error("Encryption key validation failed: round-trip mismatch"); } } catch (error) { const errorMsg = error instanceof Error ? error.message : "Unknown encryption key validation error"; throw new Error( `ENCRYPTION_KEY validation failed: ${errorMsg}. ` + "If you recently changed the key, existing encrypted data cannot be decrypted. " + "See docs/design/credential-security.md for key rotation procedures." ); } this.logger.log("Crypto service initialized with AES-256-GCM encryption"); } /** * Encrypt sensitive data (e.g., private keys) * Returns base64-encoded string with format: iv:authTag:encrypted */ encrypt(plaintext: string): string { try { // Generate random IV (12 bytes for GCM) const iv = randomBytes(12); // Create cipher const cipher = createCipheriv(this.algorithm, this.encryptionKey, iv); // Encrypt data let encrypted = cipher.update(plaintext, "utf8", "hex"); encrypted += cipher.final("hex"); // Get auth tag const authTag = cipher.getAuthTag(); // Return as iv:authTag:encrypted (all hex-encoded) return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } catch (error) { this.logger.error("Encryption failed", error); throw new Error("Failed to encrypt data"); } } /** * Decrypt sensitive data * Expects format: iv:authTag:encrypted (all hex-encoded) */ decrypt(encrypted: string): string { try { // Parse encrypted data const parts = encrypted.split(":"); if (parts.length !== 3) { throw new Error("Invalid encrypted data format"); } const ivHex = parts[0]; const authTagHex = parts[1]; const encryptedData = parts[2]; if (!ivHex || !authTagHex || !encryptedData) { throw new Error("Invalid encrypted data format"); } const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); // Create decipher const decipher = createDecipheriv(this.algorithm, this.encryptionKey, iv); decipher.setAuthTag(authTag); // Decrypt data const decryptedBuffer = decipher.update(encryptedData, "hex"); const finalBuffer = decipher.final(); const decrypted = Buffer.concat([decryptedBuffer, finalBuffer]).toString("utf8"); return decrypted; } catch (error) { // Security: Do not log error details which may contain sensitive data // Only log error type/code without stack trace or encrypted content const errorType = error instanceof Error ? error.constructor.name : "Unknown"; this.logger.error(`Decryption failed: ${errorType}`); throw new Error("Failed to decrypt data"); } } }