122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
// The Postiz API key at rest: sealed in _sc_plugins.configuration (Saltcorn
|
|
// stores plugin configuration plaintext, so a DB dump, a backup or a dev-deploy
|
|
// sync would otherwise expose it), opened only at the point of use. The key is
|
|
// derived from the session secret (env / config file, NOT the database), see
|
|
// lib/common/aesGcm.js + sessionKey.js. The configuration workflow seals a newly
|
|
// typed key before Saltcorn saves it (lib/config.js); onLoad seals a plaintext
|
|
// key left by plugin versions before 0.0.3. Rotating the session secret means
|
|
// re-entering the key.
|
|
|
|
const { createSealer, isSealedString, STRING_PREFIX } = require("./common/aesGcm");
|
|
const { getSessionSecret } = require("./common/sessionKey");
|
|
const { PLUGIN_NAME } = require("./constants");
|
|
|
|
// Domain label that ties the sealed apiKey to this plugin + field (key
|
|
// derivation). Changing it would orphan already-sealed keys.
|
|
const API_KEY_INFO = `${PLUGIN_NAME}:apiKey`;
|
|
|
|
// What the stored apiKey is: not set, sealed (and it opens), sealed but it does
|
|
// not open (session secret missing or rotated), or plaintext (legacy, or no
|
|
// session secret to seal under).
|
|
const API_KEY_STATES = {
|
|
UNSET: "unset",
|
|
SEALED: "sealed",
|
|
SEALED_UNOPENABLE: "sealed_unopenable",
|
|
PLAINTEXT: "plaintext"
|
|
};
|
|
|
|
|
|
// The sealer of one configuration field, keyed by its domain label: HKDF salt
|
|
// "scs1:<label>", info "<label>" -- byte-identical to the derivation every
|
|
// already-sealed key was made with.
|
|
const fieldSealer = (info) => {
|
|
return createSealer({ owner: PLUGIN_NAME, salt: STRING_PREFIX + info, info: info });
|
|
};
|
|
|
|
|
|
const apiKeySealer = fieldSealer(API_KEY_INFO);
|
|
|
|
|
|
// The saved key's state (API_KEY_STATES) and its usable plaintext ("" when it
|
|
// is unset or does not open).
|
|
const readApiKey = (pluginCfg) => {
|
|
const raw = pluginCfg && pluginCfg.apiKey;
|
|
if (!raw) {
|
|
return { state: API_KEY_STATES.UNSET, key: "" };
|
|
}
|
|
if (!isSealedString(raw)) {
|
|
return { state: API_KEY_STATES.PLAINTEXT, key: String(raw) };
|
|
}
|
|
const opened = apiKeySealer.openString(raw);
|
|
if (opened === null) {
|
|
return { state: API_KEY_STATES.SEALED_UNOPENABLE, key: "" };
|
|
}
|
|
return { state: API_KEY_STATES.SEALED, key: opened };
|
|
};
|
|
|
|
|
|
// The usable plaintext key from saved config, sealed or (legacy / no session
|
|
// secret) plaintext. Throws an actionable error if a sealed key cannot be opened.
|
|
const resolveApiKey = (pluginCfg) => {
|
|
const stored = readApiKey(pluginCfg);
|
|
if (stored.state === API_KEY_STATES.SEALED_UNOPENABLE) {
|
|
throw new Error(`${PLUGIN_NAME}: stored Postiz API key could not be decrypted (SALTCORN_SESSION_SECRET / the config-file session_secret is missing or was rotated). Re-enter it in the plugin config.`);
|
|
}
|
|
return stored.key;
|
|
};
|
|
|
|
|
|
// Whether an acceptable session secret is configured, so a key is sealed on
|
|
// save and a sealed key can be opened.
|
|
const sessionSecretConfigured = () => {
|
|
try {
|
|
getSessionSecret(PLUGIN_NAME);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
// A plaintext key sealed; a sealed blob or an empty value unchanged. Without an
|
|
// acceptable session secret the key stays plaintext (with a warning) rather than
|
|
// being sealed under a secret Saltcorn generated for this process only.
|
|
const sealApiKey = (value) => {
|
|
if (!value || isSealedString(value)) {
|
|
return value;
|
|
}
|
|
try {
|
|
return apiKeySealer.sealString(value);
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`[${PLUGIN_NAME}] API key left unsealed: ${e.message}`);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
|
|
const sealStoredApiKey = async () => {
|
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
const plugin = await Plugin.findOne({ name: PLUGIN_NAME });
|
|
if (!plugin || !plugin.configuration) {
|
|
return;
|
|
}
|
|
const current = plugin.configuration.apiKey;
|
|
const sealed = sealApiKey(current);
|
|
if (sealed === current) {
|
|
return;
|
|
}
|
|
plugin.configuration = { ...plugin.configuration, apiKey: sealed };
|
|
await plugin.upsert();
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
API_KEY_INFO,
|
|
fieldSealer,
|
|
readApiKey,
|
|
resolveApiKey,
|
|
sealApiKey,
|
|
sealStoredApiKey,
|
|
sessionSecretConfigured
|
|
};
|