twenty-mcp-server
Version:
Easy-to-install Model Context Protocol server for Twenty CRM. Try instantly with 'npx twenty-mcp-server setup' or install globally for permanent use.
101 lines • 3.44 kB
JavaScript
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
/**
* Encryption service for securing API keys
* Uses AES-256-GCM for authenticated encryption
*/
export class EncryptionService {
key;
algorithm = 'aes-256-gcm';
saltLength = 32;
tagLength = 16;
ivLength = 16;
constructor(secret) {
if (!secret || secret.length < 32) {
throw new Error('Encryption secret must be at least 32 characters');
}
// Derive a key from the secret using scrypt
const salt = Buffer.from('twenty-mcp-salt-v1', 'utf8');
this.key = scryptSync(secret, salt, 32);
}
/**
* Encrypts a string value
* @returns Base64 encoded string containing: salt + iv + authTag + encrypted data
*/
encrypt(plaintext) {
if (!plaintext) {
throw new Error('Cannot encrypt empty string');
}
// Generate random IV
const iv = randomBytes(this.ivLength);
// Create cipher
const cipher = createCipheriv(this.algorithm, this.key, iv);
// Encrypt the data
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);
// Get the authentication tag
const authTag = cipher.getAuthTag();
// Combine all parts: iv + authTag + encrypted
const combined = Buffer.concat([iv, authTag, encrypted]);
// Return base64 encoded
return combined.toString('base64');
}
/**
* Decrypts a base64 encoded encrypted string
*/
decrypt(encryptedData) {
if (!encryptedData) {
throw new Error('Cannot decrypt empty string');
}
try {
// Decode from base64
const combined = Buffer.from(encryptedData, 'base64');
// Extract components
const iv = combined.slice(0, this.ivLength);
const authTag = combined.slice(this.ivLength, this.ivLength + this.tagLength);
const encrypted = combined.slice(this.ivLength + this.tagLength);
// Create decipher
const decipher = createDecipheriv(this.algorithm, this.key, iv);
decipher.setAuthTag(authTag);
// Decrypt
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.toString('utf8');
}
catch (error) {
throw new Error('Failed to decrypt data: invalid key or corrupted data');
}
}
/**
* Safely compares two encrypted values without decrypting
*/
compareEncrypted(encrypted1, encrypted2) {
try {
const plain1 = this.decrypt(encrypted1);
const plain2 = this.decrypt(encrypted2);
return plain1 === plain2;
}
catch {
return false;
}
}
}
// Singleton instance
let encryptionService = null;
/**
* Get or create the encryption service instance
*/
export function getEncryptionService() {
if (!encryptionService) {
const secret = process.env.API_KEY_ENCRYPTION_SECRET;
if (!secret) {
throw new Error('API_KEY_ENCRYPTION_SECRET environment variable is required');
}
encryptionService = new EncryptionService(secret);
}
return encryptionService;
}
//# sourceMappingURL=encryption.js.map