Skip to main content
Glama
cameronsjo

MCP Server Template

by cameronsjo
crypto.ts6.21 kB
/** * Encryption utilities using AES-256-GCM * * Provides secure encryption at rest for sensitive data like * session tokens, API keys, and credentials. */ import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'node:crypto'; import { createLogger } from './logger.js'; const logger = createLogger('crypto'); /** * Encryption configuration */ const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 12; // 96 bits for GCM const AUTH_TAG_LENGTH = 16; // 128 bits const SALT_LENGTH = 32; // 256 bits const KEY_LENGTH = 32; // 256 bits for AES-256 const PBKDF2_ITERATIONS = 100000; /** * Encrypted data envelope */ export interface EncryptedData { /** Base64-encoded ciphertext */ ciphertext: string; /** Base64-encoded initialization vector */ iv: string; /** Base64-encoded authentication tag */ authTag: string; /** Base64-encoded salt (if key derived from password) */ salt?: string; /** Algorithm identifier */ algorithm: string; /** Version for future compatibility */ version: number; } /** * Derive a key from a password using PBKDF2 */ export function deriveKey(password: string, salt?: Buffer): { key: Buffer; salt: Buffer } { const derivedSalt = salt ?? randomBytes(SALT_LENGTH); const key = pbkdf2Sync(password, derivedSalt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256'); return { key, salt: derivedSalt }; } /** * Encrypt data using AES-256-GCM with a raw key */ export function encryptWithKey(plaintext: string, key: Buffer): EncryptedData { if (key.length !== KEY_LENGTH) { throw new Error(`Invalid key length: expected ${KEY_LENGTH}, got ${key.length}`); } const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const authTag = cipher.getAuthTag(); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), authTag: authTag.toString('base64'), algorithm: ALGORITHM, version: 1, }; } /** * Decrypt data using AES-256-GCM with a raw key */ export function decryptWithKey(encrypted: EncryptedData, key: Buffer): string { if (key.length !== KEY_LENGTH) { throw new Error(`Invalid key length: expected ${KEY_LENGTH}, got ${key.length}`); } if (encrypted.version !== 1) { throw new Error(`Unsupported encryption version: ${encrypted.version}`); } const iv = Buffer.from(encrypted.iv, 'base64'); const authTag = Buffer.from(encrypted.authTag, 'base64'); const ciphertext = Buffer.from(encrypted.ciphertext, 'base64'); const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return decrypted.toString('utf8'); } /** * Encrypt data using a password (derives key with PBKDF2) */ export function encrypt(plaintext: string, password: string): EncryptedData { const { key, salt } = deriveKey(password); const encrypted = encryptWithKey(plaintext, key); return { ...encrypted, salt: salt.toString('base64'), }; } /** * Decrypt data using a password */ export function decrypt(encrypted: EncryptedData, password: string): string { if (!encrypted.salt) { throw new Error('Salt required for password-based decryption'); } const salt = Buffer.from(encrypted.salt, 'base64'); const { key } = deriveKey(password, salt); return decryptWithKey(encrypted, key); } /** * Generate a secure random key */ export function generateKey(): Buffer { return randomBytes(KEY_LENGTH); } /** * Generate a secure random string (for tokens, IDs, etc.) */ export function generateSecureToken(length: number = 32): string { return randomBytes(length).toString('base64url'); } /** * Constant-time string comparison to prevent timing attacks */ export function secureCompare(a: string, b: string): boolean { if (a.length !== b.length) { return false; } const bufA = Buffer.from(a); const bufB = Buffer.from(b); let result = 0; for (let i = 0; i < bufA.length; i++) { result |= (bufA[i] ?? 0) ^ (bufB[i] ?? 0); } return result === 0; } /** * Hash a value for non-reversible storage (e.g., tokens for lookup) */ export function hashForLookup(value: string, salt?: string): string { const hashSalt = salt ?? ''; const hash = pbkdf2Sync(value, hashSalt, 10000, 32, 'sha256'); return hash.toString('base64url'); } /** * Secrets manager for encrypted storage */ export class SecretsManager { private readonly masterKey: Buffer; constructor(masterPassword: string) { // Derive master key from password // In production, consider using a dedicated secret from env const { key } = deriveKey(masterPassword, Buffer.from('mcp-server-master-salt')); this.masterKey = key; logger.debug('Secrets manager initialized'); } /** * Encrypt a secret value */ encrypt(value: string): string { const encrypted = encryptWithKey(value, this.masterKey); return JSON.stringify(encrypted); } /** * Decrypt a secret value */ decrypt(encryptedJson: string): string { const encrypted = JSON.parse(encryptedJson) as EncryptedData; return decryptWithKey(encrypted, this.masterKey); } /** * Encrypt an object (serializes to JSON first) */ encryptObject<T>(obj: T): string { return this.encrypt(JSON.stringify(obj)); } /** * Decrypt an object */ decryptObject<T>(encryptedJson: string): T { const decrypted = this.decrypt(encryptedJson); return JSON.parse(decrypted) as T; } } // Singleton secrets manager (initialized lazily) let secretsManager: SecretsManager | null = null; /** * Get or create the secrets manager */ export function getSecretsManager(): SecretsManager { if (!secretsManager) { const masterPassword = process.env['MCP_SERVER_MASTER_KEY'] ?? 'default-dev-key-change-in-production'; secretsManager = new SecretsManager(masterPassword); } return secretsManager; } /** * Reset secrets manager (for testing) */ export function resetSecretsManager(): void { secretsManager = 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/cameronsjo/mcp-server-template'

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