69 lines
2.7 KiB
JavaScript
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
|
|
};
|