// Keys derived from the installation's session secret. // // The root of every at-rest key is the session secret the installation is // CONFIGURED with: the SALTCORN_SESSION_SECRET env var (what Saltcorn checks // first) or session_secret in the Saltcorn config file (~/.config/.saltcorn). // Saltcorn's own fallback (db.connectObj.session_secret when neither is set) is // refused on purpose: it is a fresh random value per process (sealed data would // not open after a restart or in another cluster worker) or the public // "deadbeef" default (sealed data readable by anyone). // // A key is HKDF-SHA256(ikm = session secret, salt, info). Each plugin passes its // own salt/info strings, so plugins on one instance derive independent keys; // those strings must never change or existing ciphertexts stop opening. // Rotating the session secret invalidates every derived key (documented). const crypto = require("crypto"); const HKDF_HASH = "sha256"; const KEY_BYTES = 32; // Derived keys by (salt, info, bytes), valid only for cachedSecret: the session // secret is global, so one cache serves every tenant, and a changed secret // clears it so a rotated secret never keeps serving stale keys. const keyCache = new Map(); let cachedSecret = null; // owner names the plugin in the error raised when no acceptable secret is set. const getSessionSecret = (owner) => { const fromEnv = process.env.SALTCORN_SESSION_SECRET; if (fromEnv) { return fromEnv; } let cfg = null; try { const { getConfigFile } = require("@saltcorn/data/db/connect"); cfg = getConfigFile(); } catch (e) { // not running inside Saltcorn (connect module unavailable): no config file } if (cfg && typeof cfg.session_secret === "string" && cfg.session_secret.length > 0) { return cfg.session_secret; } throw new Error(`${owner}: no session_secret configured (set SALTCORN_SESSION_SECRET or session_secret in the Saltcorn config file); refusing to derive a key from Saltcorn's generated fallback secret`); }; // keySpec = { owner, salt, info, bytes? } -> Buffer of `bytes` (default 32). const deriveKey = ({ owner, salt, info, bytes = KEY_BYTES }) => { const secret = getSessionSecret(owner); if (secret !== cachedSecret) { keyCache.clear(); cachedSecret = secret; } const cacheKey = JSON.stringify([salt, info, bytes]); let key = keyCache.get(cacheKey); if (!key) { const ikm = Buffer.from(secret, "utf8"); key = Buffer.from(crypto.hkdfSync(HKDF_HASH, ikm, Buffer.from(salt, "utf8"), Buffer.from(info, "utf8"), bytes)); keyCache.set(cacheKey, key); } return key; }; module.exports = { getSessionSecret, deriveKey };