/** * Secure API Key Storage * * Provides secure storage for API keys and sensitive credentials. * Uses OS keychain when available, with encrypted localStorage fallback. * * Security features: * - Keys stored in OS keychain (Windows DPAPI, macOS Keychain, Linux Secret Service) * - Encrypted backup in localStorage for migration support * - Key validation and format checking * - Audit logging for key access * - Support for multiple API key types */ import { secureStorage, isSecureStorageAvailable } from './secure-storage'; import { hashSha256, generateRandomString } from './crypto-utils'; // Storage key prefixes const API_KEY_PREFIX = 'zclaw_api_key_'; const API_KEY_META_PREFIX = 'zclaw_api_key_meta_'; /** * Supported API key types */ export type ApiKeyType = | 'openai' | 'anthropic' | 'google' | 'deepseek' | 'zhipu' | 'moonshot' | 'custom'; /** * API key metadata */ export interface ApiKeyMetadata { type: ApiKeyType; name: string; description?: string; createdAt: number; updatedAt: number; lastUsedAt?: number; keyHash: string; // Partial hash for validation prefix: string; // First 8 characters for display isValid?: boolean; } /** * API key entry with metadata */ export interface ApiKeyEntry { key: string; metadata: ApiKeyMetadata; } /** * Validation rules for different API key types */ const KEY_VALIDATION_RULES: Record = { openai: { pattern: /^sk-[A-Za-z0-9_-]{20,}$/, minLength: 20, maxLength: 200, prefix: ['sk-'], }, anthropic: { pattern: /^sk-ant-[A-Za-z0-9_-]{20,}$/, minLength: 20, maxLength: 200, prefix: ['sk-ant-'], }, google: { pattern: /^AIza[A-Za-z0-9_-]{35}$/, minLength: 35, maxLength: 50, prefix: ['AIza'], }, deepseek: { pattern: /^sk-[A-Za-z0-9]{20,}$/, minLength: 20, maxLength: 100, prefix: ['sk-'], }, zhipu: { pattern: /^[A-Za-z0-9_.-]{20,}$/, minLength: 20, maxLength: 100, }, moonshot: { pattern: /^sk-[A-Za-z0-9]{20,}$/, minLength: 20, maxLength: 100, prefix: ['sk-'], }, custom: { pattern: /^.{8,}$/, minLength: 8, maxLength: 500, }, }; /** * Validate an API key format * * @param type - The API key type * @param key - The API key to validate * @returns True if the key format is valid */ export function validateApiKeyFormat(type: ApiKeyType, key: string): { valid: boolean; error?: string; } { const rules = KEY_VALIDATION_RULES[type]; if (!key || typeof key !== 'string') { return { valid: false, error: 'API key is required' }; } // Trim whitespace const trimmedKey = key.trim(); if (trimmedKey.length < rules.minLength) { return { valid: false, error: `API key too short (minimum ${rules.minLength} characters)`, }; } if (trimmedKey.length > rules.maxLength) { return { valid: false, error: `API key too long (maximum ${rules.maxLength} characters)`, }; } if (!rules.pattern.test(trimmedKey)) { return { valid: false, error: `Invalid API key format for type: ${type}`, }; } if (rules.prefix && !rules.prefix.some(p => trimmedKey.startsWith(p))) { return { valid: false, error: `API key must start with: ${rules.prefix.join(' or ')}`, }; } return { valid: true }; } /** * Create a partial hash for key validation * Uses first 8 characters for identification without exposing full key */ async function createKeyHash(key: string): Promise { // Use partial hash for validation const partialKey = key.slice(0, 8) + key.slice(-4); return hashSha256(partialKey); } /** * Store an API key securely * * @param type - The API key type * @param key - The API key value * @param options - Optional metadata */ export async function storeApiKey( type: ApiKeyType, key: string, options?: { name?: string; description?: string; } ): Promise { // Validate key format const validation = validateApiKeyFormat(type, key); if (!validation.valid) { throw new Error(validation.error); } const trimmedKey = key.trim(); const now = Date.now(); const keyHash = await createKeyHash(trimmedKey); const metadata: ApiKeyMetadata = { type, name: options?.name || `${type}_api_key`, description: options?.description, createdAt: now, updatedAt: now, keyHash, prefix: trimmedKey.slice(0, 8) + '...', isValid: true, }; // Store key in secure storage const storageKey = API_KEY_PREFIX + type; await secureStorage.set(storageKey, trimmedKey); // Store metadata in localStorage (non-sensitive) localStorage.setItem( API_KEY_META_PREFIX + type, JSON.stringify(metadata) ); // Log security event logSecurityEvent('api_key_stored', { type, prefix: metadata.prefix }); return metadata; } /** * Retrieve an API key from secure storage * * @param type - The API key type * @returns The API key or null if not found */ export async function getApiKey(type: ApiKeyType): Promise { const storageKey = API_KEY_PREFIX + type; const key = await secureStorage.get(storageKey); if (!key) { return null; } // Validate key still matches stored hash const metaJson = localStorage.getItem(API_KEY_META_PREFIX + type); if (metaJson) { try { const metadata: ApiKeyMetadata = JSON.parse(metaJson); const currentHash = await createKeyHash(key); if (currentHash !== metadata.keyHash) { console.error('[ApiKeyStorage] Key hash mismatch - possible tampering'); logSecurityEvent('api_key_hash_mismatch', { type }); return null; } // Update last used timestamp metadata.lastUsedAt = Date.now(); localStorage.setItem(API_KEY_META_PREFIX + type, JSON.stringify(metadata)); } catch { // Ignore metadata parsing errors } } logSecurityEvent('api_key_accessed', { type }); return key; } /** * Get API key metadata (without the actual key) * * @param type - The API key type * @returns The metadata or null if not found */ export function getApiKeyMetadata(type: ApiKeyType): ApiKeyMetadata | null { const metaJson = localStorage.getItem(API_KEY_META_PREFIX + type); if (!metaJson) { return null; } try { return JSON.parse(metaJson) as ApiKeyMetadata; } catch { return null; } } /** * List all stored API key metadata * * @returns Array of API key metadata */ export function listApiKeyMetadata(): ApiKeyMetadata[] { const metadata: ApiKeyMetadata[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(API_KEY_META_PREFIX)) { try { const meta = JSON.parse(localStorage.getItem(key) || ''); metadata.push(meta); } catch { // Ignore parsing errors } } } return metadata; } /** * Delete an API key * * @param type - The API key type */ export async function deleteApiKey(type: ApiKeyType): Promise { const storageKey = API_KEY_PREFIX + type; await secureStorage.delete(storageKey); localStorage.removeItem(API_KEY_META_PREFIX + type); logSecurityEvent('api_key_deleted', { type }); } /** * Update API key metadata * * @param type - The API key type * @param updates - Metadata updates */ export function updateApiKeyMetadata( type: ApiKeyType, updates: Partial> ): void { const metaJson = localStorage.getItem(API_KEY_META_PREFIX + type); if (!metaJson) { throw new Error(`API key metadata not found for type: ${type}`); } const metadata: ApiKeyMetadata = JSON.parse(metaJson); Object.assign(metadata, updates, { updatedAt: Date.now() }); localStorage.setItem(API_KEY_META_PREFIX + type, JSON.stringify(metadata)); } /** * Check if an API key exists for a type * * @param type - The API key type * @returns True if key exists */ export async function hasApiKey(type: ApiKeyType): Promise { const key = await getApiKey(type); return key !== null; } /** * Validate a stored API key * * @param type - The API key type * @returns Validation result */ export async function validateStoredApiKey(type: ApiKeyType): Promise<{ valid: boolean; error?: string; }> { const key = await getApiKey(type); if (!key) { return { valid: false, error: 'API key not found' }; } return validateApiKeyFormat(type, key); } /** * Rotate an API key * * @param type - The API key type * @param newKey - The new API key value */ export async function rotateApiKey(type: ApiKeyType, newKey: string): Promise { // Delete old key first await deleteApiKey(type); // Store new key return storeApiKey(type, newKey, { name: `${type}_api_key_rotated`, description: `Rotated at ${new Date().toISOString()}`, }); } /** * Export API key configuration (without actual keys) * Useful for backup or migration */ export function exportApiKeyConfig(): Array> { return listApiKeyMetadata().map(({ keyHash: _, ...meta }) => meta); } /** * Check if using OS keychain for storage */ export async function isUsingKeychain(): Promise { return isSecureStorageAvailable(); } // ============================================================================ // Security Audit Logging // ============================================================================ interface SecurityEvent { type: string; timestamp: number; details: Record; } const SECURITY_LOG_KEY = 'zclaw_security_events'; const MAX_LOG_ENTRIES = 1000; /** * Log a security event */ function logSecurityEvent( type: string, details: Record ): void { try { const events: SecurityEvent[] = JSON.parse( localStorage.getItem(SECURITY_LOG_KEY) || '[]' ); events.push({ type, timestamp: Date.now(), details, }); // Trim old entries if (events.length > MAX_LOG_ENTRIES) { events.splice(0, events.length - MAX_LOG_ENTRIES); } localStorage.setItem(SECURITY_LOG_KEY, JSON.stringify(events)); } catch { // Ignore logging failures } } /** * Get security event log */ export function getSecurityLog(): SecurityEvent[] { try { return JSON.parse(localStorage.getItem(SECURITY_LOG_KEY) || '[]'); } catch { return []; } } /** * Clear security event log */ export function clearSecurityLog(): void { localStorage.removeItem(SECURITY_LOG_KEY); } /** * Generate a random API key for testing * @internal Only use for testing purposes */ export function generateTestApiKey(type: ApiKeyType): string { const rules = KEY_VALIDATION_RULES[type]; const length = rules.minLength + 10; let key = ''; if (rules.prefix && rules.prefix.length > 0) { key = rules.prefix[0]; } const remainingLength = length - key.length; key += generateRandomString(remainingLength); return key; }