sc-site-sync/lib/common/sessionKey.js
2026-10-19 01:25:16 -05:00

69 lines
2.7 KiB
JavaScript

// 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
};