fix(#84): address critical security issues in federation identity
Implemented comprehensive security fixes for federation instance identity: CRITICAL SECURITY FIXES: 1. Private Key Encryption at Rest (AES-256-GCM) - Implemented CryptoService with AES-256-GCM encryption - Private keys encrypted before database storage - Decrypted only when needed in-memory - Master key stored in ENCRYPTION_KEY environment variable - Updated schema comment to reflect actual encryption method 2. Admin Authorization on Key Regeneration - Created AdminGuard for system-level admin operations - Requires workspace ownership for admin privileges - Key regeneration restricted to admin users only - Proper authorization checks before sensitive operations 3. Private Key Never Exposed in API Responses - Changed regenerateKeypair return type to PublicInstanceIdentity - Service method strips private key before returning - Added tests to verify private key exclusion - Controller returns only public identity ADDITIONAL SECURITY IMPROVEMENTS: 4. Audit Logging for Key Regeneration - Created FederationAuditService - Logs all keypair regeneration events - Includes userId, instanceId, and timestamp - Marked as security events for compliance 5. Input Validation for INSTANCE_URL - Validates URL format (must be HTTP/HTTPS) - Throws error on invalid URLs - Prevents malformed configuration 6. Added .env.example - Documents all required environment variables - Includes INSTANCE_NAME, INSTANCE_URL - Includes ENCRYPTION_KEY with generation instructions - Clear security warnings for production use TESTING: - Added 11 comprehensive crypto service tests - Updated 8 federation service tests for encryption - Updated 5 controller tests for security verification - Total: 24 tests passing (100% success rate) - Verified private key never exposed in responses - Verified encryption/decryption round-trip - Verified admin authorization requirements FILES CREATED: - apps/api/src/federation/crypto.service.ts (encryption) - apps/api/src/federation/crypto.service.spec.ts (tests) - apps/api/src/federation/audit.service.ts (audit logging) - apps/api/src/auth/guards/admin.guard.ts (authorization) - apps/api/.env.example (configuration template) FILES MODIFIED: - apps/api/prisma/schema.prisma (updated comment) - apps/api/src/federation/federation.service.ts (encryption integration) - apps/api/src/federation/federation.controller.ts (admin guard, audit) - apps/api/src/federation/federation.module.ts (new providers) - All test files updated for new security requirements CODE QUALITY: - All tests passing (24/24) - TypeScript compilation: PASS - ESLint: PASS - Test coverage maintained at 100% Fixes #84 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
97
apps/api/src/federation/crypto.service.ts
Normal file
97
apps/api/src/federation/crypto.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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<string>("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");
|
||||
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) {
|
||||
this.logger.error("Decryption failed", error);
|
||||
throw new Error("Failed to decrypt data");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user