admin / Synapse-Sonar
publicAttack Surface Simulation
Synapse-Sonar / synapse-sonar / lib / crypto.ts
2012 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import crypto from "crypto"; /** * AES-256-GCM encryption for integration secrets at rest. * * INTEGRATION_ENCRYPTION_KEY must be 64 hex chars (32 bytes). * openssl rand -hex 32 * * The encrypted value is stored as JSON: { iv, authTag, ciphertext } (all base64), * which maps cleanly into the IntegrationSetting.config JSONB column. */ const ALGO = "aes-256-gcm"; function getKey(): Buffer { const hex = process.env.INTEGRATION_ENCRYPTION_KEY; if (!hex || hex.length !== 64) { throw new Error( "INTEGRATION_ENCRYPTION_KEY must be set to 64 hex characters (32 bytes)." ); } return Buffer.from(hex, "hex"); } export interface EncryptedBlob { iv: string; authTag: string; ciphertext: string; } export function encryptJson(payload: Record<string, unknown>): EncryptedBlob { const iv = crypto.randomBytes(12); // 96-bit nonce, recommended for GCM const cipher = crypto.createCipheriv(ALGO, getKey(), iv); const plaintext = Buffer.from(JSON.stringify(payload), "utf8"); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); const authTag = cipher.getAuthTag(); return { iv: iv.toString("base64"), authTag: authTag.toString("base64"), ciphertext: ciphertext.toString("base64"), }; } export function decryptJson<T = Record<string, unknown>>(blob: EncryptedBlob): T { const decipher = crypto.createDecipheriv( ALGO, getKey(), Buffer.from(blob.iv, "base64") ); decipher.setAuthTag(Buffer.from(blob.authTag, "base64")); const plaintext = Buffer.concat([ decipher.update(Buffer.from(blob.ciphertext, "base64")), decipher.final(), ]); return JSON.parse(plaintext.toString("utf8")) as T; } /** * Mask a secret for display in the UI ("****abcd"). The plaintext value * never leaves the server once stored — the UI only ever sees masks. */ export function maskSecret(value: string): string { if (!value) return ""; const tail = value.slice(-4); return `••••••••${tail}`; } |