UNPKG

e2ee-adapter

Version:

Plug-and-play End-to-End Encryption middleware for Express.js and NestJS using hybrid AES-CBC + RSA encryption with secure key exchange

275 lines 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldProcessRequest = shouldProcessRequest; exports.hasEncryptionHeaders = hasEncryptionHeaders; exports.getKeyPair = getKeyPair; exports.extractAESKeyFromHeaders = extractAESKeyFromHeaders; exports.decryptRequest = decryptRequest; exports.encryptResponse = encryptResponse; exports.mergeConfigWithDefaults = mergeConfigWithDefaults; exports.validateConfig = validateConfig; exports.createE2EEError = createE2EEError; exports.processRequest = processRequest; exports.handleRequestDecryption = handleRequestDecryption; exports.setupResponseEncryptionContext = setupResponseEncryptionContext; exports.handleResponseEncryption = handleResponseEncryption; const crypto_1 = require("./crypto"); /** * Check if request should be processed by E2EE */ function shouldProcessRequest(req, config) { // Skip excluded paths if (config.excludePaths.some((path) => req.path.startsWith(path))) { return false; } // Skip excluded methods if (config.excludeMethods.includes(req.method.toUpperCase())) { return false; } return true; } /** * Check if request has encryption headers */ function hasEncryptionHeaders(req, config) { const encryptedKeyHeader = req.headers[config.customKeyHeader.toLowerCase()]; const ivHeader = req.headers[config.customIVHeader.toLowerCase()]; const keyIdHeader = req.headers[config.keyIdHeader.toLowerCase()]; return !!(encryptedKeyHeader && ivHeader && keyIdHeader); } /** * Get key pair for a specific keyId */ function getKeyPair(keyId, config, createError) { const keyPair = config.keys[keyId]; if (!keyPair) { throw createError(`Key pair not found for keyId: ${keyId}`, 'INVALID_KEY_ID', 400); } return keyPair; } /** * Extract AES key from headers for response encryption (without decryption) */ async function extractAESKeyFromHeaders(req, config, createError) { const encryptedKeyHeader = req.headers[config.customKeyHeader.toLowerCase()]; const ivHeader = req.headers[config.customIVHeader.toLowerCase()]; const keyIdHeader = req.headers[config.keyIdHeader.toLowerCase()]; if (!encryptedKeyHeader || !ivHeader || !keyIdHeader) { throw createError('Missing encryption headers', 'MISSING_ENCRYPTION_HEADERS'); } const keyPair = getKeyPair(keyIdHeader, config, createError); // Decrypt only the AES key from the header (no data decryption) const { aesKey, iv } = await (0, crypto_1.decryptAESKey)(encryptedKeyHeader, ivHeader, keyPair.privateKey); return { aesKey, iv }; } /** * Decrypt request using headers */ async function decryptRequest(req, config, createError) { try { // Extract headers const encryptedKeyHeader = req.headers[config.customKeyHeader.toLowerCase()]; const ivHeader = req.headers[config.customIVHeader.toLowerCase()]; const keyIdHeader = req.headers[config.keyIdHeader.toLowerCase()]; if (!encryptedKeyHeader || !ivHeader) { throw createError('Missing encryption headers', 'MISSING_ENCRYPTION_HEADERS'); } if (!keyIdHeader) { throw createError('Missing keyId header', 'MISSING_KEY_ID_HEADER'); } // Handle empty request body case if (!req.body || typeof req.body !== 'string') { if (config.allowEmptyRequestBody) { // For empty request bodies, extract AES key from headers for response encryption const { aesKey, iv } = await extractAESKeyFromHeaders(req, config, createError); const decryptedData = { data: {}, // Empty object for empty request body timestamp: Date.now(), aesKey, iv, }; return decryptedData; } else { throw createError('Missing encrypted data in request body', 'MISSING_ENCRYPTED_DATA'); } } // Get the appropriate key pair based on keyId const keyPair = getKeyPair(keyIdHeader, config, createError); // Decrypt the data const decryptionResult = await (0, crypto_1.decrypt)(req.body, encryptedKeyHeader, ivHeader, keyPair.privateKey); const decryptedData = { data: JSON.parse(decryptionResult.decryptedData), timestamp: Date.now(), ...(decryptionResult.aesKey && { aesKey: decryptionResult.aesKey }), ...(decryptionResult.iv && { iv: decryptionResult.iv }), }; return decryptedData; } catch (error) { if (error instanceof Error && 'code' in error) { throw error; // Re-throw E2EE errors } throw createError(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'DECRYPTION_FAILED'); } } /** * Encrypt response data */ async function encryptResponse(data, aesKey, iv, createError) { try { const dataString = JSON.stringify(data); return (0, crypto_1.encryptAES)(dataString, aesKey, iv); } catch (error) { throw createError(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'ENCRYPTION_FAILED'); } } /** * Merge configuration with defaults */ function mergeConfigWithDefaults(config) { return { keys: config.keys, customKeyHeader: config.customKeyHeader || 'x-custom-key', customIVHeader: config.customIVHeader || 'x-custom-iv', keyIdHeader: config.keyIdHeader || 'x-key-id', enableRequestDecryption: config.enableRequestDecryption !== false, enableResponseEncryption: config.enableResponseEncryption !== false, excludePaths: config.excludePaths || ['/health', '/keys', '/e2ee.json'], excludeMethods: config.excludeMethods || ['GET', 'HEAD', 'OPTIONS'], enforced: config.enforced || false, allowEmptyRequestBody: config.allowEmptyRequestBody || false, }; } /** * Validate configuration */ function validateConfig(config) { if (!config.keys || Object.keys(config.keys).length === 0) { throw new Error('At least one key pair must be provided in config.keys'); } } /** * Create E2EE error with consistent structure */ function createE2EEError(message, code, statusCode = 400) { const error = new Error(message); error.code = code; error.statusCode = statusCode; return error; } /** * Process request and determine if it should be handled by E2EE */ function processRequest(req, config, createError) { // Check if request should be processed if (!shouldProcessRequest(req, config)) { return { shouldContinue: true }; } // Check enforcement mode if (config.enforced) { // In enforced mode, all requests must be encrypted if (!hasEncryptionHeaders(req, config)) { throw createError('Encryption is enforced. All requests must include encryption headers.', 'ENCRYPTION_ENFORCED', 400); } } else { // In non-enforced mode, only process requests that have encryption headers if (!hasEncryptionHeaders(req, config)) { return { shouldContinue: true }; } } return { shouldContinue: false }; } /** * Handle request decryption and setup encryption context */ async function handleRequestDecryption(req, config, createError, onDecrypt) { // Decrypt request if there's a string body or if empty body is allowed if (config.enableRequestDecryption && typeof req.body === 'string') { const decryptedData = await decryptRequest(req, config, createError); req.body = decryptedData.data; const e2eeContext = { decryptedData, originalBody: req.body, aesKey: decryptedData.aesKey, iv: decryptedData.iv, }; // Call onDecrypt callback if provided if (onDecrypt) { onDecrypt(decryptedData, req); } return e2eeContext; } else if (config.enableRequestDecryption && hasEncryptionHeaders(req, config) && (typeof req.body === 'undefined' || Object.keys(req.body)?.length === 0 || !req.body) && !config.allowEmptyRequestBody) { // If request has encryption headers but empty body is not allowed, throw error throw createError('Missing encrypted data in request body', 'MISSING_ENCRYPTED_DATA', 400); } else if (config.enableRequestDecryption && config.allowEmptyRequestBody && (!req.body || Object.keys(req.body)?.length === 0 || typeof req.body === 'undefined')) { // Handle empty request body with encryption headers for response encryption const { aesKey, iv } = await extractAESKeyFromHeaders(req, config, createError); const e2eeContext = { decryptedData: { data: {}, timestamp: Date.now(), aesKey, iv, }, originalBody: {}, aesKey, iv, }; // Call onDecrypt callback if provided if (onDecrypt) { onDecrypt(e2eeContext.decryptedData, req); } return e2eeContext; } else if (config.enableRequestDecryption) { throw createError('Invalid request body', 'INVALID_REQUEST_BODY', 400); } return undefined; } /** * Setup encryption context for response-only encryption */ async function setupResponseEncryptionContext(req, config, createError) { const { aesKey, iv } = await extractAESKeyFromHeaders(req, config, createError); const e2eeContext = { decryptedData: { data: {}, timestamp: Date.now(), aesKey, iv, }, originalBody: {}, aesKey, iv, }; return e2eeContext; } /** * Handle response encryption with consistent error handling */ async function handleResponseEncryption(data, e2eeContext, createError, onEncrypt, res) { if (!e2eeContext || !e2eeContext.aesKey || !e2eeContext.iv) { throw createError('Missing encryption context for response', 'MISSING_ENCRYPTION_CONTEXT', 500); } const encryptedData = await encryptResponse(data, e2eeContext.aesKey, e2eeContext.iv, createError); // Call onEncrypt callback if provided if (onEncrypt) { onEncrypt({ data: encryptedData, timestamp: Date.now() }, res); } return encryptedData; } //# sourceMappingURL=e2ee-common.js.map