codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
620 lines • 22.2 kB
JavaScript
/**
* Enterprise Secrets Management System
* Implements encrypted configuration storage with key rotation and access control
*/
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import { logger } from '../logger.js';
export class SecretsManager {
config;
masterKey = null;
secrets = new Map();
accessLog = [];
keyRotationTimer;
constructor(config = {}) {
this.config = {
storePath: process.env.SECRETS_STORE_PATH || './secrets',
masterKeyPath: process.env.MASTER_KEY_PATH || './master.key',
keyRotation: {
enabled: true,
intervalDays: 90,
retainOldKeys: 3,
autoRotate: false,
},
encryption: {
algorithm: 'aes-256-gcm',
keyLength: 32,
ivLength: 16,
saltLength: 32,
iterations: 100000,
},
access: {
auditLog: true,
maxAccessAttempts: 5,
requireAuthentication: true,
},
...config,
};
}
/**
* Initialize secrets manager
*/
async initialize(masterPassword) {
try {
// Ensure secrets directory exists
await fs.mkdir(this.config.storePath, { recursive: true });
// Load or generate master key
await this.loadOrGenerateMasterKey(masterPassword);
// Load existing secrets
await this.loadSecrets();
// Start key rotation timer if enabled
if (this.config.keyRotation.enabled && this.config.keyRotation.autoRotate) {
this.startKeyRotationTimer();
}
logger.info('Secrets manager initialized', {
storePath: this.config.storePath,
secretsCount: this.secrets.size,
});
}
catch (error) {
logger.error('Failed to initialize secrets manager', error);
throw error;
}
}
/**
* Store a secret securely
*/
async storeSecret(name, value, options = {}) {
try {
if (!this.masterKey) {
throw new Error('Secrets manager not initialized');
}
// Validate secret name
this.validateSecretName(name);
// Create secret config
const secret = {
name,
value,
description: options.description,
tags: options.tags || [],
expiresAt: options.expiresAt,
createdAt: new Date(),
accessCount: 0,
};
// Encrypt and store
const encrypted = await this.encryptSecretInternal(secret);
await this.saveEncryptedSecret(encrypted);
// Update in-memory cache
this.secrets.set(name, secret);
logger.info('Secret stored', {
name,
hasExpiration: !!options.expiresAt,
tags: options.tags,
});
}
catch (error) {
logger.error('Failed to store secret', error, { name });
throw error;
}
}
/**
* Encrypt a secret and return encrypted data (for testing purposes)
*/
async encryptSecret(name, value) {
try {
if (!this.masterKey) {
await this.initialize();
}
// Validate secret name
this.validateSecretName(name);
// Create secret config
const secret = {
name,
value,
createdAt: new Date(),
accessCount: 0,
};
// Store in memory for decryption
this.secrets.set(name, secret);
// Encrypt and return the encrypted secret
return await this.encryptSecretInternal(secret);
}
catch (error) {
logger.error('Failed to encrypt secret', error, { name });
throw error;
}
}
/**
* Decrypt a secret (for testing purposes)
*/
async decryptSecret(name) {
const secret = await this.getSecret(name);
if (!secret) {
throw new Error('Secret not found');
}
return secret;
}
/**
* Retrieve a secret
*/
async getSecret(name, userId) {
try {
if (!this.masterKey) {
throw new Error('Secrets manager not initialized');
}
// Check if secret exists
const secret = this.secrets.get(name);
if (!secret) {
this.logAccess(name, false, userId);
return null;
}
// Check expiration
if (secret.expiresAt && secret.expiresAt < new Date()) {
logger.warn('Attempted access to expired secret', { name, expiresAt: secret.expiresAt });
this.logAccess(name, false, userId, 'expired');
return null;
}
// Update access tracking
secret.lastAccessed = new Date();
secret.accessCount++;
// Log access
this.logAccess(name, true, userId);
logger.debug('Secret accessed', {
name,
accessCount: secret.accessCount,
userId,
});
return secret.value;
}
catch (error) {
logger.error('Failed to retrieve secret', error, { name, userId });
this.logAccess(name, false, userId, 'error');
throw error;
}
}
/**
* Update a secret
*/
async updateSecret(name, newValue, options = {}) {
try {
const existingSecret = this.secrets.get(name);
if (!existingSecret) {
throw new Error(`Secret '${name}' not found`);
}
// Create updated secret
const updatedSecret = {
...existingSecret,
value: newValue,
description: options.description ?? existingSecret.description,
tags: options.tags ?? existingSecret.tags,
expiresAt: options.expiresAt ?? existingSecret.expiresAt,
};
// Encrypt and store
const encrypted = await this.encryptSecretInternal(updatedSecret);
await this.saveEncryptedSecret(encrypted);
// Update in-memory cache
this.secrets.set(name, updatedSecret);
logger.info('Secret updated', { name });
}
catch (error) {
logger.error('Failed to update secret', error, { name });
throw error;
}
}
/**
* Delete a secret
*/
async deleteSecret(name) {
try {
if (!this.secrets.has(name)) {
return false;
}
// Remove from filesystem
const filePath = path.join(this.config.storePath, `${name}.json`);
await fs.unlink(filePath);
// Remove from memory
this.secrets.delete(name);
logger.info('Secret deleted', { name });
return true;
}
catch (error) {
logger.error('Failed to delete secret', error, { name });
throw error;
}
}
/**
* List all secret names (not values)
*/
async listSecrets(tags) {
try {
let secrets = Array.from(this.secrets.values());
// Filter by tags if specified
if (tags && tags.length > 0) {
secrets = secrets.filter(secret => secret.tags?.some(tag => tags.includes(tag)));
}
// Return metadata only (no values)
return secrets.map(secret => ({
name: secret.name,
description: secret.description,
tags: secret.tags,
expiresAt: secret.expiresAt,
createdAt: secret.createdAt,
lastAccessed: secret.lastAccessed,
accessCount: secret.accessCount,
}));
}
catch (error) {
logger.error('Failed to list secrets', error);
throw error;
}
}
/**
* Rotate master key
*/
async rotateMasterKey(newPassword) {
try {
logger.info('Starting master key rotation');
// Backup current secrets
const secretsBackup = new Map(this.secrets);
// Generate new master key
const oldMasterKey = this.masterKey;
await this.generateMasterKey(newPassword);
// Re-encrypt all secrets with new key
for (const [name, secret] of secretsBackup.entries()) {
const encrypted = await this.encryptSecretInternal(secret);
await this.saveEncryptedSecret(encrypted);
}
// Archive old master key if configured
if (this.config.keyRotation.retainOldKeys > 0) {
await this.archiveMasterKey(oldMasterKey);
}
logger.info('Master key rotation completed', {
secretsReencrypted: secretsBackup.size,
});
}
catch (error) {
logger.error('Master key rotation failed', error);
throw error;
}
}
/**
* Export secrets (encrypted) for backup
*/
async exportSecrets() {
try {
const exportData = {
version: '1.0',
timestamp: new Date().toISOString(),
secrets: await this.getAllEncryptedSecrets(),
};
return JSON.stringify(exportData, null, 2);
}
catch (error) {
logger.error('Failed to export secrets', error);
throw error;
}
}
/**
* Import secrets from backup
*/
async importSecrets(exportData) {
try {
const data = JSON.parse(exportData);
if (data.version !== '1.0') {
throw new Error('Unsupported export format version');
}
let importCount = 0;
for (const encrypted of data.secrets) {
await this.saveEncryptedSecret(encrypted);
// Decrypt and add to memory cache
const secret = await this.decryptSecretInternal(encrypted);
this.secrets.set(secret.name, secret);
importCount++;
}
logger.info('Secrets imported successfully', {
importCount,
totalSecrets: this.secrets.size,
});
}
catch (error) {
logger.error('Failed to import secrets', error);
throw error;
}
}
/**
* Get access audit log
*/
getAccessLog(hours = 24) {
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
return this.accessLog.filter(entry => entry.timestamp >= cutoff);
}
/**
* Encrypt a secret (internal method)
*/
async encryptSecretInternal(secret) {
const salt = crypto.randomBytes(this.config.encryption.saltLength);
const iv = crypto.randomBytes(this.config.encryption.ivLength);
// Derive encryption key from master key and salt
const key = crypto.pbkdf2Sync(this.masterKey, salt, this.config.encryption.iterations, this.config.encryption.keyLength, 'sha256');
// Encrypt the secret value using GCM for authenticated encryption
const cipher = crypto.createCipheriv(this.config.encryption.algorithm, key, iv);
let encrypted = cipher.update(secret.value, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return {
name: secret.name,
encryptedData: encrypted,
encryptedValue: encrypted,
iv: iv.toString('base64'),
salt: salt.toString('base64'),
authTag: authTag.toString('base64'),
algorithm: this.config.encryption.algorithm,
keyDerivation: 'pbkdf2',
metadata: {
description: secret.description,
tags: secret.tags,
expiresAt: secret.expiresAt?.toISOString(),
createdAt: secret.createdAt.toISOString(),
lastAccessed: secret.lastAccessed?.toISOString(),
accessCount: secret.accessCount,
},
};
}
/**
* Decrypt a secret (internal method)
*/
async decryptSecretInternal(encrypted) {
const salt = Buffer.from(encrypted.salt, 'base64');
const iv = Buffer.from(encrypted.iv, 'base64');
const authTag = Buffer.from(encrypted.authTag, 'base64');
// Derive decryption key
const key = crypto.pbkdf2Sync(this.masterKey, salt, this.config.encryption.iterations, this.config.encryption.keyLength, 'sha256');
// Decrypt the value using GCM for authenticated decryption
const decipher = crypto.createDecipheriv(encrypted.algorithm, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted.encryptedValue, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return {
name: encrypted.name,
value: decrypted,
description: encrypted.metadata.description,
tags: encrypted.metadata.tags || [],
expiresAt: encrypted.metadata.expiresAt ? new Date(encrypted.metadata.expiresAt) : undefined,
createdAt: new Date(encrypted.metadata.createdAt),
lastAccessed: encrypted.metadata.lastAccessed
? new Date(encrypted.metadata.lastAccessed)
: undefined,
accessCount: encrypted.metadata.accessCount,
};
}
/**
* Load or generate master key
*/
async loadOrGenerateMasterKey(password) {
try {
// Try to load existing master key
const keyExists = await fs
.access(this.config.masterKeyPath)
.then(() => true)
.catch(() => false);
if (keyExists) {
await this.loadMasterKey(password);
}
else {
await this.generateMasterKey(password);
}
}
catch (error) {
logger.error('Failed to load or generate master key', error);
throw error;
}
}
/**
* Load master key from file
*/
async loadMasterKey(password) {
const keyData = await fs.readFile(this.config.masterKeyPath, 'utf8');
if (password) {
// Decrypt master key with password
const [encryptedKey, salt, iv] = keyData.split(':');
const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const decipher = crypto.createDecipheriv('aes-256-cbc', derivedKey, Buffer.from(iv, 'hex'));
let decrypted = decipher.update(encryptedKey, 'hex', 'utf8');
decrypted += decipher.final('utf8');
this.masterKey = Buffer.from(decrypted, 'hex');
}
else {
// Use key directly (for development only)
this.masterKey = Buffer.from(keyData, 'hex');
}
}
/**
* Generate new master key
*/
async generateMasterKey(password) {
this.masterKey = crypto.randomBytes(32);
let keyData;
if (password) {
// Encrypt master key with password
const salt = crypto.randomBytes(16).toString('hex');
const iv = crypto.randomBytes(16);
const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const cipher = crypto.createCipheriv('aes-256-cbc', derivedKey, iv);
let encrypted = cipher.update(this.masterKey.toString('hex'), 'utf8', 'hex');
encrypted += cipher.final('hex');
keyData = `${encrypted}:${salt}:${iv.toString('hex')}`;
}
else {
// Store key directly (for development only)
keyData = this.masterKey.toString('hex');
}
await fs.writeFile(this.config.masterKeyPath, keyData, { mode: 0o600 });
logger.info('New master key generated', {
keyPath: this.config.masterKeyPath,
encrypted: !!password,
});
}
/**
* Load secrets from storage
*/
async loadSecrets() {
try {
const files = await fs.readdir(this.config.storePath);
const secretFiles = files.filter(file => file.endsWith('.json'));
for (const file of secretFiles) {
try {
const filePath = path.join(this.config.storePath, file);
const encryptedData = await fs.readFile(filePath, 'utf8');
const encrypted = JSON.parse(encryptedData);
const secret = await this.decryptSecretInternal(encrypted);
this.secrets.set(secret.name, secret);
}
catch (error) {
logger.error('Failed to load secret file', error, { file });
}
}
logger.info('Secrets loaded from storage', {
count: this.secrets.size,
});
}
catch (error) {
logger.error('Failed to load secrets', error);
throw error;
}
}
/**
* Save encrypted secret to file
*/
async saveEncryptedSecret(encrypted) {
const filePath = path.join(this.config.storePath, `${encrypted.name}.json`);
await fs.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
}
/**
* Get all encrypted secrets
*/
async getAllEncryptedSecrets() {
const encryptedSecrets = [];
for (const secret of this.secrets.values()) {
const encrypted = await this.encryptSecretInternal(secret);
encryptedSecrets.push(encrypted);
}
return encryptedSecrets;
}
/**
* Validate secret name
*/
validateSecretName(name) {
if (!name || typeof name !== 'string') {
throw new Error('Secret name must be a non-empty string');
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error('Secret name can only contain alphanumeric characters, underscores, and hyphens');
}
if (name.length > 100) {
throw new Error('Secret name cannot exceed 100 characters');
}
}
/**
* Log secret access
*/
logAccess(secret, success, userId, reason) {
if (this.config.access.auditLog) {
this.accessLog.push({
secret,
timestamp: new Date(),
user: userId,
success,
...(reason && { reason }),
});
// Trim access log if too large
if (this.accessLog.length > 10000) {
this.accessLog.splice(0, 1000);
}
}
}
/**
* Archive old master key
*/
async archiveMasterKey(oldKey) {
const archivePath = path.join(this.config.storePath, 'archived-keys');
await fs.mkdir(archivePath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const archiveFile = path.join(archivePath, `master-key-${timestamp}.bak`);
await fs.writeFile(archiveFile, oldKey.toString('hex'), { mode: 0o600 });
logger.info('Old master key archived', { archiveFile });
}
/**
* Start key rotation timer
*/
startKeyRotationTimer() {
const intervalMs = this.config.keyRotation.intervalDays * 24 * 60 * 60 * 1000;
this.keyRotationTimer = setInterval(async () => {
// TODO: Store interval ID and call clearInterval in cleanup
try {
logger.info('Starting automatic key rotation');
await this.rotateMasterKey();
}
catch (error) {
logger.error('Automatic key rotation failed', error);
}
}, intervalMs);
}
/**
* Stop and cleanup
*/
async stop() {
if (this.keyRotationTimer) {
clearInterval(this.keyRotationTimer);
}
// Clear sensitive data from memory
this.masterKey?.fill(0);
this.masterKey = null;
this.secrets.clear();
logger.info('Secrets manager stopped');
}
// Test helper methods
async deleteTestSecret(name) {
const exists = this.secrets.has(name);
if (exists) {
this.secrets.delete(name);
// Also try to delete from storage if it exists
try {
const secretPath = path.join(this.config.storePath, `${name}.json`);
await fs.unlink(secretPath);
}
catch {
// Ignore if file doesn't exist
}
}
return exists;
}
async rotateEncryptionKey() {
// Generate new master key
const newMasterKey = crypto.randomBytes(32);
const oldMasterKey = this.masterKey;
// Set new master key
this.masterKey = newMasterKey;
// Re-encrypt all secrets with new key
for (const [name, secret] of this.secrets.entries()) {
const encrypted = await this.encryptSecretInternal(secret);
await this.saveEncryptedSecret(encrypted);
}
// Clear old key from memory
if (oldMasterKey) {
oldMasterKey.fill(0);
}
logger.info('Encryption key rotated successfully');
}
setEncryptionKey(key) {
if (key.length < 32) {
throw new Error('Encryption key must be at least 32 bytes');
}
this.masterKey = key;
}
get secretStorage() {
// For testing access to internal storage
return this.secrets;
}
}
//# sourceMappingURL=secrets-manager.js.map