Skip to main content
Glama
crypto.ts7.5 kB
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID as nodeRandomUUID, timingSafeEqual, pbkdf2Sync, scryptSync, hkdfSync, } from 'node:crypto' const IV_LENGTH = 12 // AES-GCM recommended 12 bytes const AUTH_TAG_LENGTH = 16 function deriveKey(key: string | Buffer): Buffer { return Buffer.isBuffer(key) ? createHash('sha256').update(key).digest() : createHash('sha256').update(Buffer.from(key)).digest() } function b64(input: ArrayBuffer | Uint8Array): string { return Buffer.from(input as any).toString('base64') } function fromB64(input: string): Buffer { return Buffer.from(input, 'base64') } /** * Node-focused crypto utilities used by the Master MCP Server runtime. * Worker builds exclude this file via tsconfig.worker.json. */ export class CryptoUtils { /** Encrypts UTF-8 text using AES-256-GCM. Returns base64(iv||tag||ciphertext). */ static encrypt(data: string, key: string | Buffer): string { const iv = randomBytes(IV_LENGTH) const cipher = createCipheriv('aes-256-gcm', deriveKey(key), iv) const ciphertext = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]) const authTag = cipher.getAuthTag() return Buffer.concat([iv, authTag, ciphertext]).toString('base64') } /** Decrypts base64(iv||tag||ciphertext) produced by encrypt(). */ static decrypt(encryptedData: string, key: string | Buffer): string { const raw = Buffer.from(encryptedData, 'base64') const iv = raw.subarray(0, IV_LENGTH) const authTag = raw.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH) const ciphertext = raw.subarray(IV_LENGTH + AUTH_TAG_LENGTH) const decipher = createDecipheriv('aes-256-gcm', deriveKey(key), iv) decipher.setAuthTag(authTag) const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]) return plaintext.toString('utf8') } /** Secure random bytes as hex string. */ static generateSecureRandom(length: number): string { return randomBytes(length).toString('hex') } /** Returns RFC4122 v4 UUID using crypto RNG. */ static uuid(): string { return nodeRandomUUID() } /** SHA-256 digest as hex string. */ static hash(input: string | Buffer): string { return createHash('sha256').update(input).digest('hex') } /** Constant-time equality check for hex strings produced by hash(). */ static verify(input: string | Buffer, hash: string): boolean { const calculated = Buffer.from(this.hash(input), 'utf8') const provided = Buffer.from(hash, 'utf8') if (calculated.length !== provided.length) return false return timingSafeEqual(calculated, provided) } /** Derives a key using PBKDF2-HMAC-SHA256. Returns base64 key bytes. */ static pbkdf2( password: string | Buffer, salt: string | Buffer, iterations = 100_000, keyLen = 32, ): string { const dk = pbkdf2Sync(password, salt, iterations, keyLen, 'sha256') return b64(dk) } /** * Hashes password using PBKDF2. Format: pbkdf2$sha256$iter$saltB64$hashB64 */ static pbkdf2Hash(password: string, iterations = 100_000, saltLen = 16): string { const salt = randomBytes(saltLen) const hash = pbkdf2Sync(password, salt, iterations, 32, 'sha256') return `pbkdf2$sha256$${iterations}$${b64(salt)}$${b64(hash)}` } static pbkdf2Verify(password: string, encoded: string): boolean { try { const [algo, hashName, iterStr, saltB64, hashB64] = encoded.split('$') if (algo !== 'pbkdf2' || hashName !== 'sha256') return false const iterations = Number(iterStr) const salt = fromB64(saltB64) const expected = fromB64(hashB64) const actual = pbkdf2Sync(password, salt, iterations, expected.length, 'sha256') return timingSafeEqual(actual, expected) } catch { return false } } /** * Hashes password using scrypt with defaults N=16384, r=8, p=1. * Format: scrypt$N$r$p$saltB64$hashB64 */ static scryptHash(password: string, opts?: { N?: number; r?: number; p?: number; saltLen?: number; keyLen?: number }): string { const N = opts?.N ?? 16384 const r = opts?.r ?? 8 const p = opts?.p ?? 1 const saltLen = opts?.saltLen ?? 16 const keyLen = opts?.keyLen ?? 32 const salt = randomBytes(saltLen) const hash = scryptSync(password, salt, keyLen, { N, r, p }) return `scrypt$${N}$${r}$${p}$${b64(salt)}$${b64(hash)}` } static scryptVerify(password: string, encoded: string): boolean { try { const [algo, nStr, rStr, pStr, saltB64, hashB64] = encoded.split('$') if (algo !== 'scrypt') return false const N = Number(nStr) const r = Number(rStr) const p = Number(pStr) const salt = fromB64(saltB64) const expected = fromB64(hashB64) const actual = scryptSync(password, salt, expected.length, { N, r, p }) return timingSafeEqual(actual, expected) } catch { return false } } /** * Attempts bcrypt via optional dependency. If unavailable, falls back to scrypt * and encodes using the scrypt$... scheme. This ensures secure hashing without * adding runtime deps. */ static async bcryptHash(password: string, rounds = 12): Promise<string> { try { // Attempt to use optional bcrypt packages if present via dynamic import const mod = await dynamicImportAny(['bcrypt', 'bcryptjs']) if (mod?.hash) return await mod.hash(password, rounds) } catch { // ignore and fallback } // Fallback to scrypt return this.scryptHash(password) } static async bcryptVerify(password: string, encoded: string): Promise<boolean> { // If it looks like a bcrypt hash, try optional bcrypt packages if (encoded.startsWith('$2a$') || encoded.startsWith('$2b$') || encoded.startsWith('$2y$')) { try { const mod = await dynamicImportAny(['bcrypt', 'bcryptjs']) if (mod?.compare) return await mod.compare(password, encoded) } catch { // ignore and fallback } return false } // Otherwise, support scrypt fallback if (encoded.startsWith('scrypt$')) return this.scryptVerify(password, encoded) if (encoded.startsWith('pbkdf2$')) return this.pbkdf2Verify(password, encoded) return false } /** HKDF with SHA-256. Returns base64 key bytes. */ static hkdf(ikm: string | Buffer, salt: string | Buffer, info: string | Buffer, length = 32): string { try { const okm = hkdfSync('sha256', ikm, salt, info, length) return b64(okm) } catch { // Fallback manual HKDF implementation (RFC 5869) const prk = createHmac('sha256', salt as any).update(ikm as any).digest() const n = Math.ceil(length / 32) const t: any[] = [] let prev: any = Buffer.alloc(0) for (let i = 0; i < n; i++) { prev = createHmac('sha256', prk as any) .update(Buffer.concat([prev, Buffer.from(info as any), Buffer.from([i + 1])]) as any) .digest() as Buffer t.push(prev) } return b64(Buffer.concat(t).subarray(0, length)) } } } async function dynamicImportAny(modules: string[]): Promise<any | null> { for (const m of modules) { try { // Avoid triggering TS module resolution by computing the specifier const importer = new Function('m', 'return import(m)') as (m: string) => Promise<any> const mod = await importer(m) if (mod) return mod.default ?? mod } catch { // continue } } return null }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/master-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server