/** * Centralized Encryption Utility for CWE-311 Compliance * Provides AES-256-GCM encryption for sensitive data at rest * * Security Features: * - AES-256-GCM authenticated encryption * - Unique IV per encryption operation * - HMAC authentication tags * - Key rotation support * - Secure key derivation from master secret */ const crypto = require('crypto'); const logger = require('./logger'); // Encryption configuration const ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits for GCM const AUTH_TAG_LENGTH = 16; const SALT_LENGTH = 32; /** * Get master encryption key from environment or generate default * SECURITY WARNING: Always set ENCRYPTION_MASTER_KEY in production! */ function getMasterKey() { const envKey = process.env.ENCRYPTION_MASTER_KEY; if (!envKey) { logger.warn('⚠️ ENCRYPTION_MASTER_KEY not set - using default (insecure for production)'); // Use JWT_SECRET as fallback, but warn about it const fallbackKey = process.env.JWT_SECRET || 'default-insecure-key-change-in-production'; return crypto.createHash('sha256').update(fallbackKey + '-encryption-v1').digest(); } // Derive proper key from master secret return crypto.createHash('sha256').update(envKey).digest(); } /** * Derive encryption key for specific purpose using HKDF-like approach * @param {String} purpose - Purpose identifier (e.g., 'settings', 'vpn', 'api-tokens') * @returns {Buffer} Derived encryption key */ function deriveKey(purpose) { const masterKey = getMasterKey(); const info = Buffer.from(purpose + '-v1', 'utf8'); return crypto.createHmac('sha256', masterKey) .update(info) .digest(); } /** * Encrypt sensitive data with AES-256-GCM * @param {String} plaintext - Data to encrypt * @param {String} purpose - Purpose identifier for key derivation * @returns {String} Encrypted data in format: salt:iv:authTag:ciphertext (all hex encoded) */ function encrypt(plaintext, purpose = 'default') { try { if (!plaintext) { return null; } // Generate random salt and IV for this encryption operation const salt = crypto.randomBytes(SALT_LENGTH); const iv = crypto.randomBytes(IV_LENGTH); // Derive encryption key with salt for additional security const masterKey = getMasterKey(); const derivedKey = crypto.pbkdf2Sync( masterKey, salt, 100000, // iterations KEY_LENGTH, 'sha256' ); // Create cipher const cipher = crypto.createCipheriv(ALGORITHM, derivedKey, iv); // Encrypt let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); // Get authentication tag (GCM provides authenticated encryption) const authTag = cipher.getAuthTag(); // Return format: salt:iv:authTag:ciphertext return [ salt.toString('hex'), iv.toString('hex'), authTag.toString('hex'), encrypted ].join(':'); } catch (error) { logger.error('Encryption error:', { purpose, error: error.message }); throw new Error('Failed to encrypt data'); } } /** * Decrypt data encrypted with encrypt() * @param {String} encryptedData - Encrypted data in format: salt:iv:authTag:ciphertext * @param {String} purpose - Purpose identifier (must match encryption purpose) * @returns {String} Decrypted plaintext */ function decrypt(encryptedData, purpose = 'default') { try { if (!encryptedData) { return null; } // Parse encrypted data const parts = encryptedData.split(':'); if (parts.length !== 4) { throw new Error('Invalid encrypted data format'); } const salt = Buffer.from(parts[0], 'hex'); const iv = Buffer.from(parts[1], 'hex'); const authTag = Buffer.from(parts[2], 'hex'); const encrypted = parts[3]; // Validate lengths if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== AUTH_TAG_LENGTH) { throw new Error('Invalid encrypted data structure'); } // Derive the same key used for encryption const masterKey = getMasterKey(); const derivedKey = crypto.pbkdf2Sync( masterKey, salt, 100000, KEY_LENGTH, 'sha256' ); // Create decipher const decipher = crypto.createDecipheriv(ALGORITHM, derivedKey, iv); decipher.setAuthTag(authTag); // Decrypt let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { logger.error('Decryption error:', { purpose, error: error.message }); throw new Error('Failed to decrypt data'); } } /** * Encrypt sensitive settings value * Automatically detects if already encrypted */ function encryptSetting(value, key) { if (!value) return null; // Don't encrypt if already encrypted (starts with salt:iv:authTag:ciphertext format) if (typeof value === 'string' && value.split(':').length === 4) { const parts = value.split(':'); if (parts[0].length === SALT_LENGTH * 2 && parts[1].length === IV_LENGTH * 2) { return value; // Already encrypted } } const plaintext = typeof value === 'string' ? value : JSON.stringify(value); return encrypt(plaintext, `setting:${key}`); } /** * Decrypt sensitive setting value */ function decryptSetting(encryptedValue, key) { if (!encryptedValue) return null; try { const decrypted = decrypt(encryptedValue, `setting:${key}`); // Try to parse as JSON if it looks like JSON if (decrypted && (decrypted.startsWith('{') || decrypted.startsWith('['))) { try { return JSON.parse(decrypted); } catch { return decrypted; } } return decrypted; } catch (error) { // If decryption fails, value might not be encrypted (migration scenario) logger.warn(`Failed to decrypt setting ${key}, returning as-is`); return encryptedValue; } } /** * Check if encryption key is properly configured */ function isEncryptionConfigured() { return !!process.env.ENCRYPTION_MASTER_KEY; } /** * Get encryption health status */ function getEncryptionStatus() { const configured = isEncryptionConfigured(); return { configured, algorithm: ALGORITHM, keySize: KEY_LENGTH * 8, // bits status: configured ? 'secure' : 'default-key', warning: configured ? null : 'Using default encryption key - set ENCRYPTION_MASTER_KEY in production', recommendations: configured ? [] : [ 'Set ENCRYPTION_MASTER_KEY environment variable', 'Use a strong random key (at least 32 characters)', 'Store the key securely (e.g., Docker secrets, AWS Secrets Manager)' ] }; } /** * Re-encrypt data with new master key (for key rotation) * @param {String} oldEncryptedData - Data encrypted with old key * @param {String} purpose - Purpose identifier * @param {String} oldMasterKey - Old master key (optional, uses current env if not provided) * @returns {String} Data re-encrypted with current master key */ function reEncrypt(oldEncryptedData, purpose = 'default', oldMasterKey = null) { try { // Temporarily swap master key if provided const originalKey = process.env.ENCRYPTION_MASTER_KEY; if (oldMasterKey) { process.env.ENCRYPTION_MASTER_KEY = oldMasterKey; } // Decrypt with old key const plaintext = decrypt(oldEncryptedData, purpose); // Restore original key if (oldMasterKey) { process.env.ENCRYPTION_MASTER_KEY = originalKey; } // Encrypt with current key return encrypt(plaintext, purpose); } catch (error) { logger.error('Re-encryption error:', { purpose, error: error.message }); throw new Error('Failed to re-encrypt data'); } } /** * Hash sensitive data for comparison (one-way, cannot be decrypted) * Use for data that needs to be compared but not retrieved (e.g., backup codes) */ function hashSensitiveData(data) { return crypto.createHash('sha256').update(data).digest('hex'); } /** * Generate cryptographically secure random token */ function generateSecureToken(length = 32) { return crypto.randomBytes(length).toString('hex'); } module.exports = { encrypt, decrypt, encryptSetting, decryptSetting, deriveKey, isEncryptionConfigured, getEncryptionStatus, reEncrypt, hashSensitiveData, generateSecureToken, // Export for legacy VPN compatibility encryptVPN: (data) => encrypt(JSON.stringify(data), 'vpn'), decryptVPN: (data) => JSON.parse(decrypt(data, 'vpn')) };