@kya-os/mcp-i
Version:
COMING SOON:Production-ready MCP Identity with automatic registration, key rotation, and optimized performance
230 lines • 8.27 kB
JavaScript
/**
* 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