anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
552 lines • 21.1 kB
JavaScript
"use strict";
/**
* Secure Credential Management for MCP
*
* Handles secure storage, rotation, and access control for LLM provider credentials
*/
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.CredentialManager = void 0;
const events_1 = require("events");
const crypto = __importStar(require("crypto"));
const types_1 = require("../types");
// Using MemoryStorageProvider directly for credential storage
const memory_storage_provider_1 = require("../../storage/providers/memory-storage-provider");
/**
* Secure Credential Manager
*/
class CredentialManager extends events_1.EventEmitter {
constructor(config, storageProvider) {
super();
this.config = config;
this.credentials = new Map();
this.encryptionKey = null;
this.encryptionConfig = {
algorithm: 'aes-256-gcm',
keyDerivationFunction: 'pbkdf2',
iterations: 100000,
saltLength: 32,
ivLength: 16,
tagLength: 16
};
this.rotationTimers = new Map();
this.validationCache = new Map();
this.storageProvider = storageProvider || new memory_storage_provider_1.MemoryStorageProvider();
this.initializeEncryption();
this.loadCredentials();
}
/**
* Initialize encryption
*/
async initializeEncryption() {
if (!this.config.encryption) {
return;
}
// In production, this master key should come from a secure key management service
// For now, we'll derive it from a passphrase
const passphrase = process.env.MCP_CREDENTIAL_PASSPHRASE || 'default-passphrase-change-me';
const salt = crypto.randomBytes(this.encryptionConfig.saltLength);
this.encryptionKey = crypto.pbkdf2Sync(passphrase, salt, this.encryptionConfig.iterations, 32, 'sha256');
}
/**
* Load credentials from storage
*/
async loadCredentials() {
try {
// Use internal storage for credentials
const storedCredentials = this.storageProvider._storage?.get('mcp:credentials');
if (storedCredentials) {
const credentials = JSON.parse(storedCredentials);
for (const cred of credentials) {
// Decrypt if necessary
if (cred.encryptedValue && this.config.encryption) {
cred.value = await this.decrypt(cred.encryptedValue);
}
// Convert dates
cred.metadata.createdAt = new Date(cred.metadata.createdAt);
cred.metadata.updatedAt = new Date(cred.metadata.updatedAt);
if (cred.metadata.expiresAt) {
cred.metadata.expiresAt = new Date(cred.metadata.expiresAt);
}
if (cred.metadata.rotatedAt) {
cred.metadata.rotatedAt = new Date(cred.metadata.rotatedAt);
}
if (cred.metadata.lastUsedAt) {
cred.metadata.lastUsedAt = new Date(cred.metadata.lastUsedAt);
}
this.credentials.set(cred.id, cred);
// Setup rotation if configured
if (this.config.rotation?.enabled) {
this.scheduleRotation(cred);
}
}
}
}
catch (error) {
this.emit('error', new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_CONFIG,
message: `Failed to load credentials: ${error.message}`,
timestamp: new Date(),
retryable: false
}));
}
}
/**
* Save credentials to storage
*/
async saveCredentials() {
const credentialsToStore = Array.from(this.credentials.values()).map(cred => {
const stored = { ...cred };
// Encrypt if necessary
if (this.config.encryption && this.encryptionKey) {
stored.encryptedValue = this.encrypt(cred.value);
delete stored.value; // Don't store plaintext
}
return stored;
});
// Use internal storage for credentials
this.storageProvider._storage = this.storageProvider._storage || new Map();
this.storageProvider._storage.set('mcp:credentials', JSON.stringify(credentialsToStore));
}
/**
* Add credential
*/
async addCredential(providerId, type, value, metadata) {
const id = `cred-${providerId}-${Date.now()}`;
const credential = {
id,
providerId,
type,
value,
metadata: {
createdAt: new Date(),
updatedAt: new Date(),
usageCount: 0,
...metadata
}
};
// Validate if configured
if (this.config.validation?.validateOnLoad) {
const validation = await this.validateCredential(credential);
credential.validation = {
validated: true,
validatedAt: new Date(),
validationError: validation.error
};
if (!validation.valid) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_CREDENTIALS,
message: `Invalid credential: ${validation.error}`,
timestamp: new Date(),
provider: providerId,
retryable: false
});
}
}
this.credentials.set(id, credential);
await this.saveCredentials();
// Schedule rotation if configured
if (this.config.rotation?.enabled) {
this.scheduleRotation(credential);
}
this.emit('credential_added', credential);
return credential;
}
/**
* Get credential for provider
*/
async getCredential(providerId) {
// Find most recent valid credential for provider
const providerCredentials = Array.from(this.credentials.values())
.filter(cred => cred.providerId === providerId)
.sort((a, b) => b.metadata.createdAt.getTime() - a.metadata.createdAt.getTime());
for (const credential of providerCredentials) {
// Check expiration
if (credential.metadata.expiresAt && credential.metadata.expiresAt < new Date()) {
continue;
}
// Validate on use if configured
if (this.config.validation?.validateOnUse) {
const validation = await this.validateCredential(credential);
if (!validation.valid) {
credential.validation = {
validated: true,
validatedAt: new Date(),
validationError: validation.error
};
await this.saveCredentials();
continue;
}
}
// Update usage statistics
credential.metadata.lastUsedAt = new Date();
credential.metadata.usageCount++;
await this.saveCredentials();
return credential;
}
return null;
}
/**
* Update credential
*/
async updateCredential(credentialId, updates) {
const credential = this.credentials.get(credentialId);
if (!credential) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_CREDENTIALS,
message: `Credential not found: ${credentialId}`,
timestamp: new Date(),
retryable: false
});
}
if (updates.value) {
credential.value = updates.value;
credential.metadata.rotatedAt = new Date();
}
if (updates.metadata) {
credential.metadata = { ...credential.metadata, ...updates.metadata };
}
credential.metadata.updatedAt = new Date();
// Re-validate if value changed
if (updates.value && this.config.validation?.validateOnLoad) {
const validation = await this.validateCredential(credential);
credential.validation = {
validated: true,
validatedAt: new Date(),
validationError: validation.error
};
}
await this.saveCredentials();
this.emit('credential_updated', credential);
return credential;
}
/**
* Delete credential
*/
async deleteCredential(credentialId) {
const credential = this.credentials.get(credentialId);
if (!credential) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_CREDENTIALS,
message: `Credential not found: ${credentialId}`,
timestamp: new Date(),
retryable: false
});
}
// Cancel rotation timer
const timer = this.rotationTimers.get(credentialId);
if (timer) {
clearTimeout(timer);
this.rotationTimers.delete(credentialId);
}
this.credentials.delete(credentialId);
this.validationCache.delete(credentialId);
await this.saveCredentials();
this.emit('credential_deleted', credential);
}
/**
* Rotate credential
*/
async rotateCredential(credentialId, newValue) {
const oldCredential = this.credentials.get(credentialId);
if (!oldCredential) {
return {
rotated: false,
oldCredentialId: credentialId,
error: 'Credential not found'
};
}
try {
// Create new credential
const newCredential = await this.addCredential(oldCredential.providerId, oldCredential.type, newValue, {
...oldCredential.metadata,
description: `Rotated from ${credentialId}`
});
// Mark old credential as rotated
oldCredential.metadata.expiresAt = new Date();
await this.saveCredentials();
// Delete old credential after retention period
if (this.config.rotation?.retentionCount) {
// Keep only the configured number of old credentials
const providerCredentials = Array.from(this.credentials.values())
.filter(cred => cred.providerId === oldCredential.providerId)
.sort((a, b) => b.metadata.createdAt.getTime() - a.metadata.createdAt.getTime());
if (providerCredentials.length > this.config.rotation.retentionCount) {
const toDelete = providerCredentials.slice(this.config.rotation.retentionCount);
for (const cred of toDelete) {
await this.deleteCredential(cred.id);
}
}
}
this.emit('credential_rotated', {
oldCredential,
newCredential
});
return {
rotated: true,
oldCredentialId: credentialId,
newCredentialId: newCredential.id
};
}
catch (error) {
return {
rotated: false,
oldCredentialId: credentialId,
error: error.message
};
}
}
/**
* Validate credential
*/
async validateCredential(credential) {
// Check cache first
if (this.config.validation?.cacheValidation) {
const cached = this.validationCache.get(credential.id);
if (cached) {
return cached;
}
}
const result = { valid: true };
try {
// Basic validation
if (!credential.value || credential.value.trim().length === 0) {
result.valid = false;
result.error = 'Credential value is empty';
}
// Type-specific validation
switch (credential.type) {
case 'api_key':
if (credential.value.length < 32) {
result.valid = false;
result.error = 'API key too short';
}
break;
case 'oauth_token':
// Check token format
if (!credential.value.startsWith('Bearer ') && !credential.value.includes('.')) {
result.valid = false;
result.error = 'Invalid OAuth token format';
}
break;
case 'certificate':
// Basic certificate validation
if (!credential.value.includes('BEGIN CERTIFICATE')) {
result.valid = false;
result.error = 'Invalid certificate format';
}
break;
}
// Cache result
if (this.config.validation?.cacheValidation) {
this.validationCache.set(credential.id, result);
// Clear cache after timeout
setTimeout(() => {
this.validationCache.delete(credential.id);
}, this.config.validation?.validationTimeout || 300000); // 5 minutes default
}
}
catch (error) {
result.valid = false;
result.error = error.message;
}
return result;
}
/**
* Schedule credential rotation
*/
scheduleRotation(credential) {
if (!this.config.rotation?.enabled || !this.config.rotation.interval) {
return;
}
// Clear existing timer
const existingTimer = this.rotationTimers.get(credential.id);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Calculate next rotation time
const lastRotation = credential.metadata.rotatedAt || credential.metadata.createdAt;
const nextRotation = new Date(lastRotation.getTime() + this.config.rotation.interval);
const timeUntilRotation = Math.max(0, nextRotation.getTime() - Date.now());
// Notify before rotation if configured
if (this.config.rotation.notifyBefore) {
const notifyTime = timeUntilRotation - this.config.rotation.notifyBefore;
if (notifyTime > 0) {
setTimeout(() => {
this.emit('rotation_due', {
credential,
dueIn: this.config.rotation.notifyBefore
});
}, notifyTime);
}
}
// Schedule rotation
const timer = setTimeout(() => {
this.emit('rotation_required', credential);
this.rotationTimers.delete(credential.id);
}, timeUntilRotation);
this.rotationTimers.set(credential.id, timer);
}
/**
* Encrypt value
*/
encrypt(value) {
if (!this.encryptionKey) {
throw new Error('Encryption key not initialized');
}
const iv = crypto.randomBytes(this.encryptionConfig.ivLength);
const cipher = crypto.createCipheriv(this.encryptionConfig.algorithm, this.encryptionKey, iv);
let encrypted = cipher.update(value, 'utf8', 'base64');
encrypted += cipher.final('base64');
const tag = cipher.getAuthTag();
// Combine IV, tag, and encrypted data
const combined = Buffer.concat([
iv,
tag,
Buffer.from(encrypted, 'base64')
]);
return combined.toString('base64');
}
/**
* Decrypt value
*/
decrypt(encryptedValue) {
if (!this.encryptionKey) {
throw new Error('Encryption key not initialized');
}
const combined = Buffer.from(encryptedValue, 'base64');
// Extract IV, tag, and encrypted data
const iv = combined.slice(0, this.encryptionConfig.ivLength);
const tag = combined.slice(this.encryptionConfig.ivLength, this.encryptionConfig.ivLength + this.encryptionConfig.tagLength);
const encrypted = combined.slice(this.encryptionConfig.ivLength + this.encryptionConfig.tagLength);
const decipher = crypto.createDecipheriv(this.encryptionConfig.algorithm, this.encryptionKey, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted.toString('base64'), 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Get credential statistics
*/
getStatistics() {
const stats = {
total: this.credentials.size,
byProvider: {},
byType: {},
expired: 0,
rotating: 0,
validated: 0,
failed: 0
};
const now = new Date();
for (const credential of this.credentials.values()) {
// Count by provider
stats.byProvider[credential.providerId] = (stats.byProvider[credential.providerId] || 0) + 1;
// Count by type
stats.byType[credential.type] = (stats.byType[credential.type] || 0) + 1;
// Count expired
if (credential.metadata.expiresAt && credential.metadata.expiresAt < now) {
stats.expired++;
}
// Count rotating
if (this.rotationTimers.has(credential.id)) {
stats.rotating++;
}
// Count validated
if (credential.validation?.validated) {
stats.validated++;
if (credential.validation.validationError) {
stats.failed++;
}
}
}
return stats;
}
/**
* Export credentials (for backup)
*/
async exportCredentials(includeValues = false) {
const credentials = Array.from(this.credentials.values()).map(cred => {
const exported = {
id: cred.id,
providerId: cred.providerId,
type: cred.type,
metadata: cred.metadata,
validation: cred.validation
};
if (includeValues) {
exported.value = cred.value;
}
return exported;
});
return JSON.stringify(credentials, null, 2);
}
/**
* Import credentials
*/
async importCredentials(data, overwrite = false) {
const credentials = JSON.parse(data);
let imported = 0;
for (const cred of credentials) {
if (!overwrite && this.credentials.has(cred.id)) {
continue;
}
await this.addCredential(cred.providerId, cred.type, cred.value, cred.metadata);
imported++;
}
return imported;
}
/**
* Shutdown credential manager
*/
async shutdown() {
// Clear all rotation timers
for (const timer of this.rotationTimers.values()) {
clearTimeout(timer);
}
this.rotationTimers.clear();
// Clear caches
this.validationCache.clear();
// Save final state
await this.saveCredentials();
// Clear sensitive data
this.credentials.clear();
this.encryptionKey = null;
this.removeAllListeners();
}
}
exports.CredentialManager = CredentialManager;
exports.default = CredentialManager;
//# sourceMappingURL=credential-manager.js.map