https://github.com/mgoyal98/e2ee-adapter
A plug-and-play TypeScript package providing End-to-End Encryption (E2EE) middleware for Express.js and NestJS applications using hybrid encryption (AES-CBC + RSA).
https://github.com/mgoyal98/e2ee-adapter
aes-cbc cryptography e2ee encryption end-to-end-encryption express hybrid-encryption key-exchange middleware nestjs no-code-e2ee plug-and-play rsa secure-communication security typescript zero-config
Last synced: about 2 months ago
JSON representation
A plug-and-play TypeScript package providing End-to-End Encryption (E2EE) middleware for Express.js and NestJS applications using hybrid encryption (AES-CBC + RSA).
- Host: GitHub
- URL: https://github.com/mgoyal98/e2ee-adapter
- Owner: mgoyal98
- License: mit
- Created: 2025-08-03T13:45:16.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2025-08-04T09:51:38.000Z (2 months ago)
- Last Synced: 2025-08-04T17:25:16.040Z (2 months ago)
- Topics: aes-cbc, cryptography, e2ee, encryption, end-to-end-encryption, express, hybrid-encryption, key-exchange, middleware, nestjs, no-code-e2ee, plug-and-play, rsa, secure-communication, security, typescript, zero-config
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/e2ee-adapter
- Size: 214 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# E2EE Adapter
> "60% of this code was written by an AI (Cursor), but I am not a passive coder — every line has been verified, fixed, and tested by me (a human)."
A **plug-and-play** TypeScript package providing End-to-End Encryption (E2EE) middleware for Express.js and NestJS applications using hybrid encryption (AES-CBC + RSA).
## 🚀 Features
- **Hybrid Encryption**: AES-CBC for data encryption + RSA for key exchange
- **Express.js Middleware**: Easy integration with Express applications
- **NestJS Interceptor**: Seamless integration with NestJS applications
- **Client SDK**: TypeScript client for making encrypted requests
- **Header-based Flow**: Secure transmission using custom headers
- **Automatic Key Management**: Server generates and manages RSA key pairs
- **Full Bidirectional Encryption**: Request and response encryption support
- **GET Request Encryption**: Encrypt responses for GET requests (even with empty request bodies)
- **Multi-Domain Support**: Multiple encryption keys for different domains/tenants
- **Custom Header Configuration**: Configurable header names for flexibility
- **Path & Method Exclusion**: Exclude specific paths and HTTP methods from encryption
- **Enforcement Modes**: Strict or flexible encryption enforcement
- **Empty Request Body Support**: Encrypt responses for requests without data
- **Callback Hooks**: Error handling, decryption, and encryption callbacks
- **Utility Functions**: Key generation, encryption, and decryption utilities
- **Automatic Response Encryption**: Transparent response encryption using `res.send()`
- **Plug-and-Play Integration**: Zero configuration required for basic setup## 🔐 Security Features
- **AES-256-CBC**: Symmetric encryption for data
- **RSA-2048-OAEP**: Asymmetric encryption for key exchange
- **Random IV Generation**: Unique initialization vectors for each request
- **Secure Key Exchange**: RSA encryption for AES key transmission## 📦 Installation
### For Express.js Applications
```bash
npm install e2ee-adapter express
```### For NestJS Applications
```bash
npm install e2ee-adapter @nestjs/common rxjs
```## 🏗️ Architecture
The middleware implements a secure hybrid encryption flow that supports both request encryption and response encryption for all HTTP methods, including GET requests with empty bodies:
```
+-------------------------+ +-------------------------+
| CLIENT | | SERVER |
+-------------------------+ +-------------------------+
| ▲
| 1. Fetch server's public key from /e2ee.json |
| → key: RSA public PEM |
| → key_id: version info |
| |
| 2. Generate AES key (32 bytes) and IV (16 bytes) |
| |
| 3. Encrypt request payload using: |
| AES-CBC(payload, AES_key, IV) |
| |
| 4. Encrypt AES key using server's RSA public key |
| RSA_encrypt(AES_key, server_pubkey) |
| |
| 5. Send HTTPS request: |
| ---------------------------------------------- |
| Headers: |
| x-custom-key: RSA_encrypted_AES_key (base64) |
| x-custom-iv: IV (base64) |
| x-key-id: key_id |
| Content-Type: application/json |
| Body: |
| Encrypted AES payload (base64) |
| ---------------------------------------------- |
| |
| ▼
| +------------------------------------------+
| | 6. Decrypt x-custom-key using RSA private|
| | AES_key = RSA_decrypt(x-custom-key) |
| +------------------------------------------+
| ▼
| +------------------------------------------+
| | 7. Decrypt payload using AES-CBC |
| | plaintext = AES_decrypt(body, AES_key,|
| | IV)|
| +------------------------------------------+
| ▼
| +------------------------------------------+
| | 8. Process request, prepare response JSON |
| +------------------------------------------+
| ▼
| +------------------------------------------+
| | 9. Encrypt response using AES-CBC |
| | response_encrypted = AES_encrypt( |
| | JSON, AES_key, IV) |
| +------------------------------------------+
| ▼
| +------------------------------------------+
| | 10. Send encrypted response |
| | Body: response_encrypted (base64) |
| +------------------------------------------+
| ▲
| 11. Decrypt response using AES_key and IV |
| plaintext_response = AES_decrypt(body, key, IV) |
| |
+-------------------------+ +-------------------------+
```## 🛠️ Usage
### 🚀 Plug-and-Play Setup
The E2EE adapter is designed to be **plug-and-play** - you can get started with minimal configuration:
```typescript
// Express.js - Just add the middleware
import { createE2EEMiddleware, generateMultipleKeyPairs } from 'e2ee-adapter';const keys = await generateMultipleKeyPairs(['domain1']);
const e2eeMiddleware = createE2EEMiddleware({ config: { keys } });
app.use(e2eeMiddleware); // That's it!// NestJS - Just add the interceptor
import { E2EEInterceptor } from 'e2ee-adapter';@UseInterceptors(E2EEInterceptor)
@Controller('api')
export class UsersController {
// Your endpoints are now encrypted!
}
```### ⚠️ Important: Middleware/Interceptor Priority
**The E2EE middleware and interceptor should be the first one to be used in your application stack.** This ensures that:
- **Request decryption** happens before any other middleware processes the request
- **Response encryption** happens after your application logic but before any response middleware
- **Security headers** are properly set and maintained throughout the request lifecycle
- **No data leakage** occurs through other middleware that might log or process request/response data**For Express.js:** Place the E2EE middleware as early as possible in your middleware stack, typically right after basic middleware like `express.json()` and `express.urlencoded()`.
**For NestJS:** Apply the E2EE interceptor globally or at the controller level to ensure it runs before other interceptors and guards.
### Module Paths
The package provides specific module paths for middleware, client, and interceptor:
```typescript
// Main exports (everything)
import { createE2EEMiddleware, E2EEClient, E2EEInterceptor, generateMultipleKeyPairs } from 'e2ee-adapter';// Specific modules (optional)
import { createE2EEMiddleware } from 'e2ee-adapter/middleware';
import { E2EEClient } from 'e2ee-adapter/client';
import { E2EEInterceptor } from 'e2ee-adapter/interceptor';
```### Express.js Setup
```typescript
import express from 'express';
import { generateMultipleKeyPairs } from 'e2ee-adapter';
import { createE2EEMiddleware } from 'e2ee-adapter/middleware';const app = express();
// Generate multiple RSA key pairs
const keys = await generateMultipleKeyPairs(['domain1', 'domain2', 'domain3']);// Create E2EE middleware
const e2eeMiddleware = createE2EEMiddleware({
config: {
keys,
enableRequestDecryption: true,
enableResponseEncryption: true,
excludePaths: ['/health', '/keys', '/e2ee.json'],
excludeMethods: ['GET', 'HEAD', 'OPTIONS'],
enforced: false // Allow both encrypted and non-encrypted requests
},
onError: (error, req, res) => {
console.error('E2EE Error:', error.message);
},
onDecrypt: (decryptedData, req) => {
console.log('Request decrypted successfully');
},
onEncrypt: (encryptedData, res) => {
console.log('Response encrypted successfully');
}
});// Apply middleware (IMPORTANT: Place this early in your middleware stack)
app.use(e2eeMiddleware);// Add server configuration endpoint
app.get('/e2ee.json', (req, res) => {
res.json({
keys: {
domain1: keys.domain1.publicKey,
domain2: keys.domain2.publicKey,
domain3: keys.domain3.publicKey
},
keySize: 2048
});
});// Protected endpoints
app.post('/api/users', (req, res) => {
// req.body contains decrypted data
const user = { id: Date.now(), ...req.body };
// The middleware will automatically encrypt the response if encryption context is available
res.send({ success: true, user });
});// GET endpoint with encrypted response
app.get('/api/users/:id', (req, res) => {
const user = { id: req.params.id, name: 'John Doe' };
res.send({ success: true, user }); // Will be automatically encrypted
});
```### NestJS Setup
**Important: Configure bodyParser for plain text requests**
Before setting up the E2EE interceptor, you need to configure your NestJS application to handle plain text requests:
```typescript
import * as bodyParser from 'body-parser';// Add this to your main.ts or app.module.ts
app.use(bodyParser.text({ type: 'text/plain' }));
```**E2EE Interceptor Setup:**
```typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { E2EEInterceptor } from 'e2ee-adapter/interceptor';@Injectable()
export class E2EEInterceptor extends E2EEInterceptor {
constructor() {
super({
config: {
keys: {
domain1: {
privateKey: process.env.E2EE_DOMAIN1_PRIVATE_KEY,
publicKey: process.env.E2EE_DOMAIN1_PUBLIC_KEY
},
domain2: {
privateKey: process.env.E2EE_DOMAIN2_PRIVATE_KEY,
publicKey: process.env.E2EE_DOMAIN2_PUBLIC_KEY
}
},enableRequestDecryption: true,
enableResponseEncryption: true,
enforced: true // Strictly require encryption for all requests
}
});
}
}// Apply to controller or globally (IMPORTANT: Apply early in the interceptor chain)
@UseInterceptors(E2EEInterceptor)
@Controller('api')
export class UsersController {
@Post('users')
createUser(@Body() userData: any) {
// userData is automatically decrypted
return { success: true, user: userData };
}
}
```### Client Usage
```typescript
import { E2EEClient } from 'e2ee-adapter/client';// Create client with multiple server keys
const client = new E2EEClient({
serverKeys: {
domain1: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----',
domain2: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----'
}
});// Make encrypted requests
const response = await client.request({
url: 'https://api.example.com/api/users',
method: 'POST',
data: {
name: 'John Doe',
email: 'john@example.com'
},
keyId: 'domain1' // Required: specify which key to use
});console.log(response.data); // Automatically decrypted response
```## 📋 API Reference
### Configuration Options
```typescript
interface KeyPair {
/** RSA public key in PEM format */
publicKey: string;
/** RSA private key in PEM format */
privateKey: string;
}interface KeyStore {
/** Mapping of keyId to key pair */
[keyId: string]: KeyPair;
}interface E2EEConfig {
/** Multiple keys store for multi-domain support */
keys: KeyStore;
/** Custom key header name (default: x-custom-key) */
customKeyHeader?: string;
/** Custom IV header name (default: x-custom-iv) */
customIVHeader?: string;
/** Key ID header name (default: x-key-id) */
keyIdHeader?: string;
/** Enable request decryption (default: true) */
enableRequestDecryption?: boolean;
/** Enable response encryption (default: true) */
enableResponseEncryption?: boolean;
/** Paths to exclude from encryption (default: ['/health', '/keys', '/e2ee.json']) */
excludePaths?: string[];
/** HTTP methods to exclude from encryption (default: ['GET', 'HEAD', 'OPTIONS']) */
excludeMethods?: string[];
/** If true, strictly enforce encryption for all requests. If false, only check for encryption after identifying headers (default: false) */
enforced?: boolean;
/** If true, allow empty request bodies while still enabling encrypted responses (default: false) */
allowEmptyRequestBody?: boolean;
}interface E2EEMiddlewareOptions {
/** E2EE configuration */
config: E2EEConfig;
/** Error callback for handling E2EE errors */
onError?: (error: Error, req: any, res: any) => void;
/** Callback triggered when request is successfully decrypted */
onDecrypt?: (decryptedData: DecryptedData, req: any) => void;
/** Callback triggered when response is successfully encrypted */
onEncrypt?: (encryptedData: EncryptedData, res: any) => void;
}
```### Client Configuration
```typescript
interface E2EEClientConfig {
/** Multiple server keys for multi-domain support */
serverKeys: { [keyId: string]: string };
/** Key ID for versioning */
keyId?: string;
}
```### Response Encryption
The middleware automatically encrypts responses when encryption context is available. Simply use the standard `res.send()` method:
```typescript
// Encrypted response (when E2EE context exists)
app.post('/api/data', (req, res) => {
const data = { message: 'Hello World' };
res.send(data); // Automatically encrypted
});// Encrypted response for GET requests
app.get('/api/data', (req, res) => {
const data = { message: 'Hello World' };
res.send(data); // Automatically encrypted if E2EE context exists
});// Non-encrypted response (bypasses E2EE)
app.get('/api/public', (req, res) => {
const data = { message: 'Public data' };
res.json(data); // Never encrypted
});
```## 🔧 Examples
### Complete Express.js Example
See `examples/express-server/server.js` for a complete working example.
### Complete Client Example
See `examples/client-example/client.js` for a complete working example.
### Vanilla JavaScript Client Example
See `examples/vanilla-js-client/` for a browser-based vanilla JavaScript client with interactive UI.
### Complete NestJS Example
See `examples/nestjs-server/` for a complete NestJS application with proper architecture, DTOs, entities, and global E2EE interceptor configuration.
## 🚀 Quick Start
1. **Install the package:**
```bash
# For Express.js
npm install e2ee-adapter express
# For NestJS
npm install e2ee-adapter @nestjs/common rxjs
```2. **Generate multiple key pairs:**
```typescript
import { generateMultipleKeyPairs } from 'e2ee-adapter';
const keys = await generateMultipleKeyPairs(['domain1', 'domain2', 'domain3']);
```3. **Set up Express middleware:**
```typescript
import { createE2EEMiddleware } from 'e2ee-adapter/middleware';
const e2eeMiddleware = createE2EEMiddleware({
config: {
keys
}
});
// IMPORTANT: Place this early in your middleware stack
app.use(e2eeMiddleware);
```4. **Create client:**
```typescript
import { E2EEClient } from 'e2ee-adapter/client';
const client = new E2EEClient({
serverKeys: {
domain1: keys.domain1.publicKey,
domain2: keys.domain2.publicKey,
domain3: keys.domain3.publicKey
}
});
```5. **Make encrypted requests:**
```typescript
// POST request with encrypted data
const response = await client.request({
url: 'http://localhost:3000/api/users',
method: 'POST',
data: { name: 'John Doe' },
keyId: 'domain1' // Required: specify which key to use
});// GET request with encrypted response (no request body needed)
const userResponse = await client.request({
url: 'http://localhost:3000/api/users/123',
method: 'GET',
keyId: 'domain1' // Required: specify which key to use
});
```## 🌐 Multi-Domain Support
The library supports multiple encryption keys for different domains or tenants sharing the same server infrastructure. This is useful for:
- **Multi-tenant applications** where each tenant has their own encryption keys
- **Domain-specific encryption** where different domains use different keys
- **Key rotation** where new keys can be added while old ones remain valid
- **Isolation** ensuring data encrypted with one key cannot be decrypted with another
- **Explicit key selection** requiring users to specify which key to use for each request### How it works:
1. **Server Configuration**: Configure multiple key pairs with unique keyIds
2. **Client Configuration**: Store public keys for all domains you need to communicate with
3. **Request-Level Key Selection**: Specify which keyId to use for each request (required)
4. **Automatic Key Resolution**: Server automatically selects the correct private key based on the keyId header### Example Use Case:
```typescript
// Server setup for multiple domains
const keys = await generateMultipleKeyPairs(['tenant1', 'tenant2', 'tenant3']);const middleware = createE2EEMiddleware({
config: { keys }
});// Client setup for multiple domains
const client = new E2EEClient({
serverKeys: {
tenant1: keys.tenant1.publicKey,
tenant2: keys.tenant2.publicKey,
tenant3: keys.tenant3.publicKey
}
});// Use different keys for different requests (keyId is required)
await client.request({ url: '/api/data', method: 'POST', data: data1, keyId: 'tenant1' });
await client.request({ url: '/api/data', method: 'POST', data: data2, keyId: 'tenant2' });
```## 🔒 Security Considerations
- **Key Management**: Store private keys securely and never expose them
- **Key Rotation**: Implement key rotation mechanisms for production use
- **HTTPS**: Always use HTTPS in production to protect against MITM attacks
- **Key Size**: Use 2048-bit RSA keys minimum for production
- **Algorithm**: The middleware uses RSA-OAEP with SHA-256 for optimal security
- **Key Isolation**: Ensure proper key isolation between different domains/tenants## 🛡️ Enforcement Mode
The library supports two enforcement modes to control how encryption is handled:
### Non-Enforced Mode (Default: `enforced: false`)
- Only processes requests that include encryption headers
- Allows both encrypted and non-encrypted requests to coexist
- Useful for gradual migration or mixed environments
- Requests without encryption headers are passed through unchanged### Enforced Mode (`enforced: true`)
- Strictly requires all requests to include encryption headers
- Rejects requests without proper encryption headers with a 400 error
- Ensures complete end-to-end encryption compliance
- Recommended for production environments with strict security requirements### Example Configuration:
```typescript
// Non-enforced mode (default) - allows mixed requests
const middleware = createE2EEMiddleware({
config: {
keys,
enforced: false // or omit this line
}
});// Enforced mode - requires all requests to be encrypted
const middleware = createE2EEMiddleware({
config: {
keys,
enforced: true
}
});
```### Use Cases:
- **Development/Testing**: Use non-enforced mode to test both encrypted and non-encrypted endpoints
- **Gradual Migration**: Start with non-enforced mode and gradually migrate clients
- **Production**: Use enforced mode to ensure all traffic is encrypted
- **Mixed Environments**: Use non-enforced mode when some clients cannot support encryption## 🔧 Utility Functions
The package provides several utility functions for key generation and encryption operations:
```typescript
import {
generateKeyPair,
generateMultipleKeyPairs,
encrypt,
decrypt,
encryptAES,
decryptAES,
decryptAESKey
} from 'e2ee-adapter';// Generate a single RSA key pair
const keyPair = await generateKeyPair(2048);// Generate multiple key pairs for different domains
const keys = await generateMultipleKeyPairs(['domain1', 'domain2', 'domain3']);// Encrypt data using hybrid encryption
const encrypted = await encrypt('sensitive data', publicKey);// Decrypt data
const decrypted = await decrypt(encryptedData, encryptedKey, iv, privateKey);// AES-only encryption/decryption
const aesEncrypted = encryptAES('data', aesKey, iv);
const aesDecrypted = decryptAES(encryptedData, aesKey, iv);// Decrypt AES key from headers (for empty request bodies)
const { aesKey, iv } = await decryptAESKey(encryptedKey, iv, privateKey);
```## 🔐 GET Request Encryption
**One of the key features of this library is the ability to encrypt responses for GET requests, even when they have no request body.** This is particularly useful for:
- **Secure Data Retrieval**: GET requests that return sensitive data
- **API Endpoints**: Public APIs that need to return encrypted responses
- **Stateless Operations**: Operations that don't require request data but need secure responses### How GET Request Encryption Works:
1. **Client sends GET request** with encryption headers (AES key encrypted with RSA)
2. **No request body needed** - the encryption context is established via headers
3. **Server processes request** and generates response data
4. **Response is automatically encrypted** using the AES key from headers
5. **Client decrypts response** using the same AES key### Example GET Request with Encrypted Response:
```typescript
// Server endpoint
app.get('/api/users/:id', (req, res) => {
const user = { id: req.params.id, name: 'John Doe', email: 'john@example.com' };
res.send(user); // Automatically encrypted response
});// Client request
const response = await client.request({
url: 'https://api.example.com/api/users/123',
method: 'GET',
keyId: 'domain1' // No data needed, but encryption headers required
});console.log(response.data); // Automatically decrypted user data
```## 📤 Empty Request Body Support
The library supports encrypted responses even for requests with empty bodies (like GET requests or POST requests without data). This is useful when you want to:
- **GET requests with encrypted responses**: Retrieve data securely without sending any request body
- **POST requests without data**: Submit forms or trigger actions that don't require request data
- **API endpoints that only return data**: Endpoints that don't accept input but should return encrypted responses### Configuration:
```typescript
// Enable empty request body support
const middleware = createE2EEMiddleware({
config: {
keys,
allowEmptyRequestBody: true, // Enable this feature
enableRequestDecryption: true,
enableResponseEncryption: true
}
});
```### How it works:
1. **Client sends request** with encryption headers but no request body
2. **Server processes request** by decrypting the AES key from headers
3. **Server generates response** and encrypts it using the decrypted AES key
4. **Client receives encrypted response** and decrypts it### Example Use Cases:
```typescript
// GET request with encrypted response
app.get('/api/users', (req, res) => {
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
res.send(users); // Response will be automatically encrypted
});// POST request without body but with encrypted response
app.post('/api/health-check', (req, res) => {
const status = { status: 'healthy', timestamp: Date.now() };
res.send(status); // Response will be automatically encrypted
});
```### Client Example:
```typescript
// GET request with encrypted response
const response = await client.request({
url: 'https://api.example.com/api/users',
method: 'GET',
keyId: 'domain1' // No data needed, but encryption headers required
});console.log(response.data); // Automatically decrypted response
// POST request without body but with encrypted response
const healthResponse = await client.request({
url: 'https://api.example.com/api/health-check',
method: 'POST',
keyId: 'domain1' // No data needed, but encryption headers required
});console.log(healthResponse.data); // Automatically decrypted response
```## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.