gitops-secrets
Version:
SecretOps workflow for bundling encrypted secrets into your deployments to safely decrypt at runtime.
110 lines (97 loc) • 3.96 kB
JavaScript
const fs = require("fs");
const path = require("path");
const secrets = require("./secrets.js");
const SECRETS_FOLDER = path.join(__dirname, "../.secrets");
const DEFAULT_JS_PATH = path.join(SECRETS_FOLDER, ".secrets.enc.js");
const DEFAULT_FILE_PATH = path.join(SECRETS_FOLDER, ".secrets.enc.json");
// eslint-disable-next-line security/detect-non-literal-fs-filename
if (!fs.existsSync(SECRETS_FOLDER)) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.mkdirSync(SECRETS_FOLDER);
}
/**
* Encapsulate encrypted secrets in a JS module for easy runtime access.
* Use {options.path} to output module locally for when package level storage or non-literal imports are disallowed.
* Use {options.cipherTextOnly} to limit the JS file to only exporting `CIPHER_TEXT`
* @param {<Record<string, any>} payload
* @param {{path: string, cipherTextOnly: boolean}} [options={path: null, cipherTextOnly: false}]
*/
async function build(payload, options = { path: null, cipherTextOnly: false }) {
const cipherText = secrets.encrypt(payload);
const filePath = options.path ? path.resolve(options.path) : DEFAULT_JS_PATH;
const packageType = process.env.npm_package_type === "module" ? "esm" : "cjs";
const format = filePath === DEFAULT_JS_PATH ? "cjs" : packageType;
let lines = ["/* eslint-disable */", "// This file was auto-generated by gitops-secrets"];
if (format === "esm") {
if (!options.cipherTextOnly) {
lines = [
...lines,
`import secrets from "gitops-secrets/no-fs";`,
`const CIPHER_TEXT = "${cipherText}";`,
"const loadSecrets = () => secrets.loadSecretsFromCipher(CIPHER_TEXT);",
"export { CIPHER_TEXT, loadSecrets };",
];
} else {
lines = [...lines, `const CIPHER_TEXT = "${cipherText}";`, "export { CIPHER_TEXT };"];
}
}
if (format === "cjs") {
if (!options.cipherTextOnly) {
lines = [
...lines,
`const secrets = require("gitops-secrets/no-fs");`,
`const CIPHER_TEXT = "${cipherText}";`,
"const loadSecrets = () => secrets.loadSecretsFromCipher(CIPHER_TEXT);",
"module.exports = { CIPHER_TEXT, loadSecrets };",
];
} else {
lines = [...lines, `const CIPHER_TEXT = "${cipherText}";`, "module.exports = { CIPHER_TEXT };"];
}
}
writeFile(filePath, lines.join("\n"));
}
/**
* Encrypt JSON-serializable payload to a static file
* @param {Record<string,any>} payload
* @param {{path: string}} [options={ path: null }]
*/
function encryptToFile(payload, options = { path: null }) {
const cipherText = secrets.encrypt(payload);
const filePath = options.path ? path.resolve(options.path) : DEFAULT_FILE_PATH;
writeFile(filePath, cipherText);
}
/**
* Decrypt JSON payload to object with option to merge with process.env
* @param {string} filePath
* @returns
*/
function decryptFromFile(filePath) {
filePath = filePath ? path.resolve(filePath) : DEFAULT_FILE_PATH;
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const payload = secrets.decrypt(fs.readFileSync(filePath, { encoding: "utf-8" }));
return { ...payload, populateEnv: () => secrets.populateEnv(payload) };
} catch (error) {
throw new Error(`Unable to read secrets from ${filePath}: ${error}`);
}
}
function writeFile(filePath, fileContents) {
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.writeFileSync(filePath, fileContents, { encoding: "utf-8" });
} catch (error) {
throw new Error(`Unable to write secrets to ${filePath}: ${error}`);
}
}
function loadSecrets() {
// eslint-disable-next-line security/detect-non-literal-require
return require(DEFAULT_JS_PATH).loadSecrets();
}
module.exports = {
build: build,
encryptToFile: encryptToFile,
decryptFromFile: decryptFromFile,
loadSecrets: loadSecrets,
DEFAULT_JS_PATH: DEFAULT_JS_PATH,
DEFAULT_FILE_PATH: DEFAULT_FILE_PATH,
};