Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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}`;
}