Aller au contenu principal

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)

  1. 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
  2. Caching

    • Cache decrypted keys in memory (Redis)
    • Invalidate on key rotation
    • Reduces database queries
  3. 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:

  1. Disable New Encryptions

    // In apiKeyService.ts
    credential.mcpApiKey = keys.mcpApiKey; // Store plain text
  2. Decrypt Existing Keys

    # Create decrypt script (reverse of encrypt-mcp-keys.ts)
    npm run decrypt-mcp-keys
  3. Remove Service Layer

    // Revert to direct model access
    const key = credential.mcpApiKey;

Best Practices

  1. Never Log Decrypted Keys

    ❌ logger.info(`API key: ${decryptedKey}`);
    ✅ logger.info("API key validated successfully");
  2. Rotate ENCRYPTION_KEY Periodically

    • Generate new key
    • Re-encrypt all keys with new key
    • Update environment variable
  3. Use Environment Variables

    # Never commit to git
    .env
    .env.local
  4. Audit Access

    // Log all key access events
    logger.info({ userId, action: "key_retrieved" });
  • /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.