UNPKG

@kya-os/mcp-i

Version:

COMING SOON:Production-ready MCP Identity with automatic registration, key rotation, and optimized performance

230 lines 8.27 kB
/** * Optimized cryptographic utilities for MCP-I with lazy loading * Implements Ed25519 signing and verification with caching */ // Lazy-loaded modules let ed25519 = null; let cryptoModule = null; // Cache for computed values const signatureCache = new Map(); const MAX_CACHE_SIZE = 100; /** * Lazy load crypto dependencies */ async function loadEd25519() { if (!ed25519) { ed25519 = await import('@noble/ed25519'); } return ed25519; } async function loadCrypto() { if (!cryptoModule) { cryptoModule = await import('crypto'); } return cryptoModule; } /** * Generate a new Ed25519 key pair with precomputed values */ export async function generateKeyPair() { const ed = await loadEd25519(); const privateKeyBytes = ed.utils.randomPrivateKey(); const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes); // Precompute base64 strings const publicKey = Buffer.from(publicKeyBytes).toString('base64'); const privateKey = Buffer.from(privateKeyBytes).toString('base64'); return { publicKey, privateKey, publicKeyBytes, privateKeyBytes }; } /** * Sign a message with Ed25519 (with caching) */ export async function sign(message, privateKeyBase64) { // Create cache key const messageStr = typeof message === 'string' ? message : message.toString('base64'); const cacheKey = `${privateKeyBase64}:${messageStr}`; // Check cache const cached = signatureCache.get(cacheKey); if (cached) { return cached; } // Perform signing const ed = await loadEd25519(); const messageBuffer = typeof message === 'string' ? Buffer.from(message, 'utf-8') : message; const privateKey = Buffer.from(privateKeyBase64, 'base64'); const signature = await ed.signAsync(messageBuffer, privateKey); const signatureBase64 = Buffer.from(signature).toString('base64'); // Cache result (with size limit) if (signatureCache.size >= MAX_CACHE_SIZE) { // Remove oldest entry const firstKey = signatureCache.keys().next().value; if (firstKey) { signatureCache.delete(firstKey); } } signatureCache.set(cacheKey, signatureBase64); return signatureBase64; } /** * Verify an Ed25519 signature */ export async function verify(message, signatureBase64, publicKeyBase64) { try { const ed = await loadEd25519(); const messageBuffer = typeof message === 'string' ? Buffer.from(message, 'utf-8') : message; const signature = Buffer.from(signatureBase64, 'base64'); const publicKey = Buffer.from(publicKeyBase64, 'base64'); return await ed.verifyAsync(signature, messageBuffer, publicKey); } catch { return false; } } /** * Generate a cryptographically secure nonce */ export async function generateNonce(length = 32) { const crypto = await loadCrypto(); return crypto.randomBytes(length).toString('hex'); } /** * Generate nonce synchronously (for performance-critical paths) * Uses cached crypto module if available */ let cachedCrypto = null; export function generateNonceSync(length = 32) { if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) { // Use Web Crypto API if available const bytes = new Uint8Array(length); globalThis.crypto.getRandomValues(bytes); return Buffer.from(bytes).toString('hex'); } else { // Try to use cached crypto module if (!cachedCrypto) { // In Node.js environment, we should have already loaded crypto module // This is a fallback for edge cases const hex = '0123456789abcdef'; let output = ''; for (let i = 0; i < length * 2; i++) { output += hex[Math.floor(Math.random() * 16)]; } console.warn('Using Math.random for nonce generation - not cryptographically secure!'); return output; } return cachedCrypto.randomBytes(length).toString('hex'); } } // Initialize crypto module cache on load (for Node.js environments) if (typeof process !== 'undefined' && process.versions && process.versions.node) { loadCrypto().then(crypto => { cachedCrypto = crypto; }).catch(() => { // Ignore errors - will fallback to other methods }); } /** * Constant-time string comparison to prevent timing attacks */ export function constantTimeEqual(a, b) { if (a.length !== b.length) { return false; } let result = 0; for (let i = 0; i < a.length; i++) { result |= a.charCodeAt(i) ^ b.charCodeAt(i); } return result === 0; } /** * Convert Ed25519 public key to did:key format */ export function publicKeyToDid(publicKeyBase64) { const publicKey = Buffer.from(publicKeyBase64, 'base64'); // Multicodec ed25519-pub header (0xed 0x01) const multicodec = Buffer.from([0xed, 0x01]); const multikey = Buffer.concat([multicodec, publicKey]); // Base58 encode (simplified - in production use a proper base58 library) return `did:key:z${multikey.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')}`; } /** * Encrypt data using AES-256-GCM (for key storage) */ export async function encrypt(data, password) { const encoder = new TextEncoder(); const salt = new Uint8Array(16); globalThis.crypto.getRandomValues(salt); // Derive key from password const keyMaterial = await globalThis.crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']); const key = await globalThis.crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']); const iv = new Uint8Array(12); globalThis.crypto.getRandomValues(iv); const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoder.encode(data)); // Combine salt + iv + encrypted data const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength); combined.set(salt); combined.set(iv, salt.length); combined.set(new Uint8Array(encrypted), salt.length + iv.length); return 'enc:' + Buffer.from(combined).toString('base64'); } /** * Decrypt data using AES-256-GCM */ export async function decrypt(encryptedData, password) { // Handle enc: prefix let dataToDecrypt = encryptedData; if (encryptedData.startsWith('enc:')) { dataToDecrypt = encryptedData.slice(4); } // If data doesn't look like base64 encrypted data, return as-is if (!dataToDecrypt || dataToDecrypt.length < 44) { return encryptedData; } const encoder = new TextEncoder(); const decoder = new TextDecoder(); try { const combined = Buffer.from(dataToDecrypt, 'base64'); // Check minimum size for encrypted data (salt + iv + auth tag + at least 1 byte) if (combined.length < 29) { return encryptedData; } // Extract salt, iv, and encrypted data const salt = combined.slice(0, 16); const iv = combined.slice(16, 28); const encrypted = combined.slice(28); // Derive key from password const keyMaterial = await globalThis.crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']); const key = await globalThis.crypto.subtle.deriveKey({ name: 'PBKDF2', salt: new Uint8Array(salt), iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt']); const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: new Uint8Array(iv) }, key, new Uint8Array(encrypted)); return decoder.decode(decrypted); } catch (error) { // If decryption fails, throw error throw new Error('Failed to decrypt data: invalid password or corrupted data'); } } /** * Clear signature cache (useful for testing or memory management) */ export function clearCache() { signatureCache.clear(); } //# sourceMappingURL=crypto.js.map