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
179 lines • 7.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.E2EEClient = void 0;
const crypto_1 = require("../utils/crypto");
class E2EEClient {
constructor(config) {
// Validate configuration
if (!config.serverKeys || Object.keys(config.serverKeys).length === 0) {
throw new Error('serverKeys must be provided with at least one key');
}
// Build server keys map
this.serverKeys = { ...config.serverKeys };
}
/**
* Get server public key for a specific keyId
*/
getServerPublicKey(keyId) {
const publicKey = this.serverKeys[keyId];
if (!publicKey) {
throw new Error(`Server public key not found for keyId: ${keyId}`);
}
return publicKey;
}
/**
* Encrypt request data using hybrid encryption (AES-CBC + RSA)
* @param data - Data to encrypt
* @param keyId - Key ID to use for encryption
* @returns Promise<{ encryptedData: string, encryptedKey: string, iv: string, originalAesKey: Buffer, originalIv: Buffer }>
*/
async encryptRequest(data, keyId) {
try {
const dataString = JSON.stringify(data);
const serverPublicKey = this.getServerPublicKey(keyId);
// Encrypt the data using hybrid encryption
const encryptionResult = await (0, crypto_1.encrypt)(dataString, serverPublicKey);
return {
encryptedData: encryptionResult.encryptedData,
encryptedKey: encryptionResult.aesKey.toString('base64'),
iv: encryptionResult.iv.toString('base64'),
originalAesKey: encryptionResult.originalAesKey, // Use the original AES key for response decryption
originalIv: encryptionResult.iv, // Store the original IV for response decryption
};
}
catch (error) {
throw new Error(`Request encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Decrypt response data using AES-CBC
* @param encryptedData - Encrypted data (base64)
* @param aesKey - AES key (Buffer)
* @param iv - Initialization vector (Buffer)
* @returns Promise<any>
*/
async decryptResponse(encryptedData, aesKey, iv) {
try {
const decryptedData = (0, crypto_1.decryptAES)(encryptedData, aesKey, iv);
return JSON.parse(decryptedData);
}
catch (error) {
throw new Error(`Response decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Make an encrypted HTTP request
* @param requestConfig - Request configuration
* @returns Promise<E2EEClientResponse>
*/
async request(requestConfig) {
try {
const { url, method, data, headers = {}, keyId } = requestConfig;
// Validate keyId is provided
if (!keyId) {
throw new Error('keyId is required for encrypted requests');
}
// Prepare request headers
const requestHeaders = {
'Content-Type': 'application/json',
...headers,
};
let requestBody = '';
let aesKey;
let iv;
// Always generate encryption headers for E2EE support
// This allows the server to encrypt responses even for GET requests or requests without body
const { encryptedData, encryptedKey, iv: ivString, originalAesKey, originalIv, } = await this.encryptRequest(data || {}, keyId);
// Set encryption headers
requestHeaders['x-custom-key'] = encryptedKey;
requestHeaders['x-custom-iv'] = ivString;
requestHeaders['x-key-id'] = keyId;
// Store AES key and IV for response decryption
aesKey = originalAesKey;
iv = originalIv;
// Set encrypted data as request body only if data was provided AND method is not GET
if (data && method.toUpperCase() !== 'GET') {
requestBody = encryptedData;
}
// Make the HTTP request
const fetchOptions = {
method,
headers: requestHeaders,
};
if (data) {
fetchOptions.body = requestBody;
}
const response = await fetch(url, fetchOptions);
// Parse response headers
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// Get response data
const responseData = await response.text();
// Decrypt response if we have the AES key and IV
let decryptedData = responseData;
if (aesKey && iv && responseData) {
try {
decryptedData = await this.decryptResponse(responseData, aesKey, iv);
}
catch (error) {
console.warn('Failed to decrypt response, returning raw data:', error);
decryptedData = responseData;
}
}
return {
data: decryptedData,
headers: responseHeaders,
status: response.status,
statusText: response.statusText,
};
}
catch (error) {
throw new Error(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Generate a new key pair (for testing purposes only)
* Note: In production, clients don't generate their own keys
*/
static async generateKeyPair(keySize = 2048) {
const { generateKeyPair } = await Promise.resolve().then(() => __importStar(require('../utils/crypto')));
return generateKeyPair(keySize);
}
}
exports.E2EEClient = E2EEClient;
//# sourceMappingURL=e2ee-client.js.map