API Key Encryption Implementation
Overview
This document describes the comprehensive encryption system implemented for securing sensitive API keys in the database. All API keys (AI providers and MCP) are now encrypted using AES-256-GCM before storage.
Architecture
Encryption Utility
File: /src/utils/encryption.util.ts
- Algorithm: AES-256-GCM (Galois/Counter Mode)
- Key Size: 256 bits (32 bytes)
- IV Size: 16 bytes (randomly generated per encryption)
- Authentication Tag: 16 bytes (integrity verification)
Format: iv:authTag:encryptedData (all base64 encoded)
Key Methods:
encrypt(text: string): string
// Generates unique IV, encrypts with auth tag
// Returns: "base64_iv:base64_tag:base64_encrypted"
decrypt(encryptedText: string): string
// Splits format, verifies auth tag, decrypts
// Throws on tampering or invalid format
isEncrypted(text: string): boolean
// Checks if text matches encrypted format pattern
Environment Variable:
ENCRYPTION_KEY="iQmJAYyTUCkwVMbUpmdVDo0ZhMTrkujMwl+TBtKpr/I="
Service Layer
File: /src/services/core/apiKey.service.ts
Centralized service for managing all API keys with automatic encryption/decryption.
Key Methods:
async updateApiKeys(userId: string, keys: {
openaiApiKey?: string;
mistralApiKey?: string;
anthropicApiKey?: string;
googleApiKey?: string;
mcpApiKey?: string;
}): Promise<void>
// Encrypts provided keys and updates credential
// Sets corresponding *CreatedAt timestamps
// Handles undefined to remove keys
async getApiKeys(userId: string): Promise<{
openaiApiKey?: string;
mistralApiKey?: string;
anthropicApiKey?: string;
googleApiKey?: string;
mcpApiKey?: string;
}>
// Retrieves and decrypts all API keys for user
// Returns plain text keys for immediate use
async hasApiKeys(userId: string): Promise<boolean>
// Checks if user has any API keys configured
// Returns true if any key exists (encrypted or not)
async deleteApiKeys(userId: string): Promise<void>
// Removes all API keys and timestamps
// Complete cleanup for user
MCP Authentication Service
File: /src/services/core/mcpAuth.service.ts
Updated to use apiKeyService and encryptionUtil for MCP key operations.
Key Changes:
async generateMCPApiKey(credentialId: string): Promise<string>
// Generates "mcp_" prefixed key
// Uses apiKeyService.updateApiKeys() for encrypted storage
// Returns plain text key (show once)
async validateApiKey(apiKey: string)
// Finds all credentials with mcpApiKey
// Decrypts each and compares to provided key
// Supports both encrypted and unencrypted (migration)
// Returns user info on match
async revokeApiKey(credentialId: string): Promise<void>
// Uses apiKeyService to clear encrypted key
async getApiKey(credentialId: string): Promise<string | null>
// Uses apiKeyService.getApiKeys() to retrieve decrypted key
Note: validateApiKey() uses a search-and-decrypt approach since each encryption generates a unique IV, making direct database queries impossible.
Credential Service
File: /src/services/core/credential.service.ts
Updated to use mcpAuthService.getApiKey() for decryption before masking.
async getMCPApiKey(credentialId: string)
// Retrieves encrypted key from database
// Decrypts via mcpAuthService.getApiKey()
// Masks decrypted key: "mcp_1234...abcd5678"
// Returns masked key with metadata
Security Features
1. Authenticated Encryption
- GCM mode provides both confidentiality and integrity
- Auth tag prevents tampering detection
- Any modification to encrypted data causes decryption failure
2. Unique IVs
- Each encryption generates a new random IV
- Prevents pattern analysis even for identical keys
- Makes database queries by encrypted value impossible
3. Service Layer Isolation
- Encryption logic centralized in
encryptionUtil - All key access goes through
apiKeyService - Models remain clean data containers
- Easy to test and audit
4. Backward Compatibility
isEncrypted()checks detect unencrypted legacy keys- Graceful handling during migration period
- No breaking changes to existing flows
Usage Examples
Updating AI Provider Keys
// Controller
import apiKeyService from "../services/core/apiKey.service";
// Store encrypted keys
await apiKeyService.updateApiKeys(userId, {
openaiApiKey: "sk-proj-abc123...",
mistralApiKey: "mist-xyz789...",
});
// Retrieve decrypted keys
const keys = await apiKeyService.getApiKeys(userId);
// keys.openaiApiKey = "sk-proj-abc123..." (plain text)
Generating MCP Keys
import mcpAuthService from "../services/core/mcpAuth.service";
// Generate and encrypt
const apiKey = await mcpAuthService.generateMCPApiKey(credentialId);
console.log(`Save this key: ${apiKey}`); // mcp_abc123... (plain text)
// Database stores encrypted: "iv:tag:encrypted"
Validating MCP Keys
// Middleware or controller
const user = await mcpAuthService.validateApiKey(providedKey);
// Automatically decrypts all keys and compares
// Returns user info on match
Migration
Encrypting Existing Keys
Script: /src/scripts/encrypt-mcp-keys.ts
# Ensure ENCRYPTION_KEY is set in .env
npm run encrypt-mcp-keys
# Output:
# 🔐 MCP API Key Encryption Migration
# ✓ Connected to database
# Found 5 credential(s) with MCP API keys
# 🔒 admin: Encrypted successfully
# 🔒 john.doe: Encrypted successfully
# ✓ jane.smith: Already encrypted
# 🔒 api.user: Encrypted successfully
# 🔒 test.user: Encrypted successfully
# 📊 Migration Summary:
# Total credentials: 5
# Newly encrypted: 4
# Already encrypted: 1
# ✅ Migration completed successfully!
Features:
- Detects already encrypted keys (idempotent)
- Preserves key functionality during migration
- Reports detailed progress
- Safe to run multiple times
Database Schema
Before Encryption
{
mcpApiKey: "mcp_1234567890abcdef...",
openaiApiKey: "sk-proj-abc123...",
mistralApiKey: "mist-xyz789...",
// ...
}
After Encryption
{
mcpApiKey: "ZjNkMmU...==:YWJjZGVm...==:eHl6MTIz...==",
openaiApiKey: "YTFiMmMz...==:ZGVmZ2hp...==:anBxcnN0...==",
mistralApiKey: "bW5vcHFy...==:c3R1dnd4...==:eXphYmNk...==",
// Format: iv:authTag:encryptedData (all base64)
}
Performance Considerations
MCP Key Validation
- Issue: Must decrypt all keys to find match (no indexed query)
- Impact: O(n) where n = number of credentials with MCP keys
- Mitigation:
- Early exit on match
- Most deployments have < 100 users
- Validation happens once per session
Benchmark (estimated):
- 10 users: < 10ms
- 100 users: < 50ms
- 1000 users: < 500ms
Optimization Options (Future)
-
Hash-Based Lookup
mcpApiKeyHash: "sha256(apiKey)" // Index this field- Add indexed hash field for O(1) lookups
- Keep encrypted value for retrieval
- Validate hash, then decrypt on match
-
Caching
- Cache decrypted keys in memory (Redis)
- Invalidate on key rotation
- Reduces database queries
-
Rate Limiting
- Limit validation attempts per IP
- Prevents brute force enumeration
Error Handling
Decryption Failures
try {
const decrypted = encryptionUtil.decrypt(encrypted);
} catch (error) {
// Possible causes:
// - Wrong ENCRYPTION_KEY
// - Corrupted data
// - Tampering detected (auth tag mismatch)
// - Invalid format
}
Missing Keys
const keys = await apiKeyService.getApiKeys(userId);
if (!keys.openaiApiKey) {
throw new Error("OpenAI API key not configured");
}
Testing
Manual Testing
# 1. Generate MCP key
npm run generate-mcp-key
# 2. Verify encryption in database
mongo
> db.credentials.findOne({ userName: "admin" }, { mcpApiKey: 1 })
{ mcpApiKey: "abc123...:def456...:ghi789..." } // Encrypted format
# 3. Test validation
curl -H "Authorization: Bearer mcp_your_key_here" http://localhost:3000/api/mcp/info
# 4. Verify decryption in logs
# Should show successful authentication without errors
Unit Tests (Future)
describe("encryptionUtil", () => {
it("should encrypt and decrypt correctly", () => {
const original = "mcp_test_key_12345";
const encrypted = encryptionUtil.encrypt(original);
const decrypted = encryptionUtil.decrypt(encrypted);
expect(decrypted).toBe(original);
});
it("should detect encrypted format", () => {
const encrypted = encryptionUtil.encrypt("test");
expect(encryptionUtil.isEncrypted(encrypted)).toBe(true);
expect(encryptionUtil.isEncrypted("plain_text")).toBe(false);
});
});
Rollback Plan
If encryption causes issues:
-
Disable New Encryptions
// In apiKeyService.ts
credential.mcpApiKey = keys.mcpApiKey; // Store plain text -
Decrypt Existing Keys
# Create decrypt script (reverse of encrypt-mcp-keys.ts)
npm run decrypt-mcp-keys -
Remove Service Layer
// Revert to direct model access
const key = credential.mcpApiKey;
Best Practices
-
Never Log Decrypted Keys
❌ logger.info(`API key: ${decryptedKey}`);
✅ logger.info("API key validated successfully"); -
Rotate ENCRYPTION_KEY Periodically
- Generate new key
- Re-encrypt all keys with new key
- Update environment variable
-
Use Environment Variables
# Never commit to git
.env
.env.local -
Audit Access
// Log all key access events
logger.info({ userId, action: "key_retrieved" });
Related Files
/src/utils/encryption.util.ts- Core encryption/src/services/core/apiKey.service.ts- Key management/src/services/core/mcpAuth.service.ts- MCP authentication/src/services/core/credential.service.ts- Credential operations/src/models/credential.model.ts- Data model/src/scripts/encrypt-mcp-keys.ts- Migration script/src/scripts/generate-mcp-api-key.ts- Key generation CLI
Environment Setup
# .env
ENCRYPTION_KEY="iQmJAYyTUCkwVMbUpmdVDo0ZhMTrkujMwl+TBtKpr/I="
# Generate new key (if needed)
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Conclusion
The encryption system provides enterprise-grade security for API keys while maintaining:
- ✅ Backward compatibility
- ✅ Clean architecture (service layer)
- ✅ Easy testing and maintenance
- ✅ Safe migration path
- ✅ Comprehensive error handling
All sensitive API keys are now protected at rest with AES-256-GCM encryption.