trojanhorse-js
Version:
A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.
552 lines (461 loc) • 16.7 kB
text/typescript
/**
* KeyVault - Secure API Key Management for TrojanHorse.js
* Secure key storage with proper encryption
*/
import { CryptoEngine, RealEncryptionResult } from './CryptoEngine';
import {
ApiKeyConfig,
ApiKeyObject,
SecureVaultOptions,
SecurityError,
AuthenticationError
} from '../types';
export class KeyVault {
private cryptoEngine: CryptoEngine;
private encryptedVault: RealEncryptionResult | null = null;
private decryptedKeys: Record<string, string> | null = null; // Always store as strings internally
private isLocked: boolean = true;
private options: SecureVaultOptions;
private autoLockTimer: NodeJS.Timeout | null = null;
private lastAccessTime: Date | null = null;
private failedAttempts: number = 0;
private maxFailedAttempts: number = 5;
private lockoutDuration: number = 300000; // 5 minutes
constructor(options: SecureVaultOptions = {}) {
this.cryptoEngine = new CryptoEngine();
this.options = {
algorithm: 'AES-256-GCM',
keyDerivation: 'Argon2id',
iterations: 65536,
saltBytes: 32,
autoLock: true,
lockTimeout: 300000,
requireMFA: false,
...options
};
}
/**
* Extract string API key from either string or object format
*/
private extractApiKey(keyData: string | ApiKeyObject | undefined): string | undefined {
if (!keyData) {
return undefined;
}
if (typeof keyData === 'string') {
return keyData;
}
if (typeof keyData === 'object') {
return keyData.key || keyData.secret || keyData.token;
}
return undefined;
}
/**
* Normalize ApiKeyConfig to string-only format for internal storage
*/
private normalizeApiKeys(apiKeys: ApiKeyConfig): Record<string, string> {
const normalized: Record<string, string> = {};
for (const [provider, keyData] of Object.entries(apiKeys)) {
const stringKey = this.extractApiKey(keyData);
if (stringKey) {
normalized[provider] = stringKey;
}
}
return normalized;
}
/**
* Create a new encrypted vault with API keys
*/
public async createVault(password: string, apiKeys: ApiKeyConfig): Promise<RealEncryptionResult> {
this.validatePassword(password);
this.validateApiKeys(apiKeys);
try {
// Encrypt the API keys
const vault = await this.cryptoEngine.encrypt(apiKeys, password, this.options);
// Store the encrypted vault
this.encryptedVault = vault;
this.decryptedKeys = this.normalizeApiKeys(apiKeys);
this.isLocked = false;
this.updateAccessTime();
// Set up auto-lock
this.setupAutoLock();
return vault;
} catch (error) {
throw new SecurityError('Failed to create vault', { originalError: error });
}
}
/**
* Load an existing encrypted vault
*/
public loadVault(vault: RealEncryptionResult): void {
if (!this.cryptoEngine.validateEncryptionParams(vault)) {
throw new SecurityError('Invalid vault structure');
}
this.encryptedVault = vault;
this.isLocked = true;
this.decryptedKeys = null;
}
/**
* Unlock the vault with password
*/
public async unlock(password: string): Promise<void> {
if (!this.encryptedVault) {
throw new SecurityError('No vault loaded');
}
if (this.failedAttempts >= this.maxFailedAttempts) {
throw new AuthenticationError('Vault is locked due to too many failed attempts');
}
this.validatePassword(password);
try {
// Decrypt the vault
const decryptedData = await this.cryptoEngine.decrypt(this.encryptedVault, password);
// Validate decrypted data
this.validateApiKeys(decryptedData);
// Store decrypted keys (normalize to strings)
this.decryptedKeys = this.normalizeApiKeys(decryptedData);
this.isLocked = false;
this.failedAttempts = 0;
this.updateAccessTime();
// Set up auto-lock
this.setupAutoLock();
} catch (error) {
this.failedAttempts++;
if (this.failedAttempts >= this.maxFailedAttempts) {
this.lockVault();
setTimeout(() => {
this.failedAttempts = 0;
}, this.lockoutDuration);
}
throw new AuthenticationError('Failed to unlock vault - invalid password');
}
}
/**
* Lock the vault
*/
public lock(): void {
this.lockVault();
}
/**
* Get API key for a specific provider
*/
public getApiKey(provider: string): string {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
this.updateAccessTime();
const key = this.decryptedKeys[provider];
if (!key) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
return key;
}
/**
* Add or update an API key
*/
public async setApiKey(provider: string, apiKey: string, password: string): Promise<void> {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!provider || !apiKey) {
throw new SecurityError('Provider and API key cannot be empty');
}
// Update the keys
this.decryptedKeys[provider] = apiKey;
// Re-encrypt the vault with updated keys
const newVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = newVault;
this.updateAccessTime();
}
/**
* Remove an API key
*/
public async removeApiKey(provider: string, password: string): Promise<void> {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!this.decryptedKeys[provider]) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
// Remove the key
delete this.decryptedKeys[provider];
// Re-encrypt the vault
const newVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = newVault;
this.updateAccessTime();
}
/**
* Rotate an API key with optional grace period
*/
public async rotateKey(provider: string, newKey: string, options: {
gracePeriod?: number; // Time in ms to keep old key active
password?: string; // Required for re-encryption
notifyRotation?: boolean;
} = {}): Promise<void> {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!this.decryptedKeys[provider]) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
const { gracePeriod = 0, password, notifyRotation = true } = options;
const oldKey = this.decryptedKeys[provider];
try {
// Update the key immediately
this.decryptedKeys[provider] = newKey;
this.updateAccessTime();
// If password provided, re-encrypt the vault with new key
if (password && this.encryptedVault) {
const updatedVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = updatedVault;
}
// Schedule old key cleanup if grace period specified
if (gracePeriod > 0) {
setTimeout(() => {
// Securely erase old key from memory
if (typeof oldKey === 'string') {
// Create array of characters and overwrite
const keyArray = oldKey.split('');
for (let i = 0; i < keyArray.length; i++) {
keyArray[i] = Math.random().toString(36).charAt(0);
}
}
}, gracePeriod);
}
if (notifyRotation) {
console.info(`🔄 API key rotated for provider: ${provider}`);
}
this.auditLog('info', `API key rotated for provider: ${provider}`);
} catch (error) {
// Rollback on error
if (this.decryptedKeys && oldKey) {
this.decryptedKeys[provider] = oldKey;
}
throw new SecurityError(`Key rotation failed for ${provider}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Batch rotate multiple API keys
*/
public async rotateMultipleKeys(keyUpdates: Record<string, string>, options: {
password?: string;
gracePeriod?: number;
continueOnError?: boolean;
} = {}): Promise<{ success: string[]; failed: Record<string, string> }> {
const results = { success: [] as string[], failed: {} as Record<string, string> };
const { continueOnError = true } = options;
for (const [provider, newKey] of Object.entries(keyUpdates)) {
try {
await this.rotateKey(provider, newKey, {
...options,
notifyRotation: false // We'll notify at the end
});
results.success.push(provider);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.failed[provider] = errorMessage;
console.error(`❌ Failed to rotate key for ${provider}: ${errorMessage}`);
if (!continueOnError) {
throw error;
}
}
}
console.info(`🔄 Batch key rotation completed: ${results.success.length} successful, ${Object.keys(results.failed).length} failed`);
return results;
}
/**
* Setup automatic key rotation schedule
*/
public setupKeyRotation(config: {
providers: string[];
rotationInterval: number; // in milliseconds
keyGenerator: (provider: string) => Promise<string>;
password: string;
}): NodeJS.Timeout {
const { providers, rotationInterval, keyGenerator, password } = config;
return setInterval(async () => {
console.info('🔄 Starting scheduled key rotation...');
const keyUpdates: Record<string, string> = {};
// Generate new keys for all providers
for (const provider of providers) {
try {
keyUpdates[provider] = await keyGenerator(provider);
} catch (error) {
console.error(`Failed to generate new key for ${provider}:`, error);
}
}
// Perform batch rotation
try {
const results = await this.rotateMultipleKeys(keyUpdates, {
password,
gracePeriod: 5 * 60 * 1000, // 5 minute grace period
continueOnError: true
});
console.info(`✅ Scheduled rotation completed: ${results.success.length} keys rotated`);
} catch (error) {
console.error('❌ Scheduled key rotation failed:', error);
}
}, rotationInterval);
}
/**
* Get vault status
*/
public getStatus(): {
isLocked: boolean;
hasVault: boolean;
keyCount: number;
lastAccess: Date | null;
autoLockEnabled: boolean;
failedAttempts: number;
} {
return {
isLocked: this.isLocked,
hasVault: !!this.encryptedVault,
keyCount: this.decryptedKeys ? Object.keys(this.decryptedKeys).length : 0,
lastAccess: this.lastAccessTime,
autoLockEnabled: this.options.autoLock || false,
failedAttempts: this.failedAttempts
};
}
/**
* Get list of configured providers
*/
public getProviders(): string[] {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
this.updateAccessTime();
return Object.keys(this.decryptedKeys);
}
/**
* Test API key validity
*/
public async testApiKey(provider: string): Promise<boolean> {
const key = this.getApiKey(provider);
// Basic validation
if (!key || key.length < 8) {
return false;
}
// Provider-specific validation could be added here
return true;
}
/**
* Export encrypted vault
*/
public exportVault(): RealEncryptionResult {
if (!this.encryptedVault) {
throw new SecurityError('No vault to export');
}
return { ...this.encryptedVault };
}
// ===== PRIVATE METHODS =====
private lockVault(): void {
this.isLocked = true;
this.decryptedKeys = null;
this.clearAutoLockTimer();
// Secure memory cleanup
if (this.decryptedKeys) {
this.cryptoEngine.secureErase(this.decryptedKeys);
}
}
private updateAccessTime(): void {
this.lastAccessTime = new Date();
if (this.options.autoLock) {
this.setupAutoLock();
}
}
private setupAutoLock(): void {
this.clearAutoLockTimer();
if (this.options.autoLock && this.options.lockTimeout) {
this.autoLockTimer = setTimeout(() => {
this.lockVault();
}, this.options.lockTimeout);
}
}
private clearAutoLockTimer(): void {
if (this.autoLockTimer) {
clearTimeout(this.autoLockTimer);
this.autoLockTimer = null;
}
}
private validatePassword(password: string): void {
if (!password || typeof password !== 'string') {
throw new SecurityError('Password is required');
}
if (password.length < 12) {
throw new SecurityError('Password must be at least 12 characters long');
}
// Calculate entropy score (bits of entropy)
const entropy = this.calculatePasswordEntropy(password);
if (entropy < 50) {
throw new SecurityError(`Password is too weak (entropy: ${entropy.toFixed(1)} bits). Minimum required: 50 bits`);
}
// Check for common weak patterns
if (this.hasWeakPatterns(password)) {
throw new SecurityError('Password contains weak patterns');
}
}
private calculatePasswordEntropy(password: string): number {
// Character set sizes
const charSets = {
lowercase: /[a-z]/.test(password) ? 26 : 0,
uppercase: /[A-Z]/.test(password) ? 26 : 0,
digits: /[0-9]/.test(password) ? 10 : 0,
symbols: /[^a-zA-Z0-9]/.test(password) ? 32 : 0 // Approximate symbol count
};
const charsetSize = Object.values(charSets).reduce((sum, size) => sum + size, 0);
if (charsetSize === 0) {
return 0;
}
// Entropy = log2(charset_size^length)
const entropy = password.length * Math.log2(charsetSize);
// Apply penalty for repeated characters
const uniqueChars = new Set(password).size;
const repetitionPenalty = uniqueChars / password.length;
return entropy * repetitionPenalty;
}
private hasWeakPatterns(password: string): boolean {
const weakPatterns = [
/(.)\1{2,}/, // 3+ repeated characters
/012|123|234|345|456|567|678|789|890/, // Sequential numbers
/abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz/i, // Sequential letters
/^(password|admin|user|login|qwerty|123456|letmein)$/i, // Common passwords
/^.{1,3}$/ // Too short patterns
];
return weakPatterns.some(pattern => pattern.test(password));
}
private validateApiKeys(apiKeys: any): void {
if (!apiKeys || typeof apiKeys !== 'object') {
throw new SecurityError('API keys must be an object');
}
const keys = Object.keys(apiKeys);
if (keys.length === 0) {
throw new SecurityError('At least one API key is required');
}
// Validate each key
keys.forEach(provider => {
const key = apiKeys[provider];
if (!key || typeof key !== 'string' || key.length < 8) {
throw new SecurityError(`Invalid API key for provider: ${provider}`);
}
});
}
// @ts-ignore - Keep for future use
private sanitizeProvider(provider: string): string {
// Remove any potentially dangerous characters
return provider.replace(/[^a-zA-Z0-9_-]/g, '');
}
/**
* Simple audit logging for security events
*/
private auditLog(level: 'info' | 'warn' | 'error', message: string, details?: any): void {
const timestamp = new Date().toISOString();
// const _logEntry = {
// timestamp,
// level,
// service: 'KeyVault',
// message,
// ...(details && { details })
// };
// For now, just log to console. In production, this would go to a proper audit system
console[level](`[KeyVault Audit] ${timestamp} - ${message}`, details || '');
}
}