UNPKG

dotencr

Version:

Encrypt and decrypt individual lines inside a .env file. Supports multiple encryption keys. Keys and dotenv files can be read text file or read from the environment.

352 lines 17.3 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const reservedWords = __importStar(require("./reserved_words")); const lodash_1 = __importDefault(require("lodash")); const dotenv_1 = __importDefault(require("dotenv")); const fs_1 = __importDefault(require("fs")); const feedback_1 = require("./feedback"); const dotenv_variable_1 = require("./dotenv_variable"); const crypto_wrapper_1 = require("./crypto_wrapper"); const dotenv_parser_1 = require("./dotenv_parser"); const util_1 = require("./util"); const assert = __importStar(require("assert")); /** * This class read/writes/encrypts/decrypts the a dotenv file. * It can also test a dotenv file for errors. */ class DotenvProcessor { constructor(ckm, pathOrBuffer, forceIvToFixedValue = false) { this.matchingDotenvVariables = []; this.nonMatchingDotenvVariables = []; /** * Used exclusively for testing to force the IV to be the same value, so encrypted * always match the expected value. If TRUE, this weakens the strength of the encryption. */ this.forceIvToFixedValue = false; this.forceIvToFixedValue = forceIvToFixedValue; this.cryptoKeyMgr = ckm; this.cryptoWrapper = new crypto_wrapper_1.CryptoWrapperAes256Gcm(ckm, this.forceIvToFixedValue); this.plainTextParser = new dotenv_parser_1.DotenvParserPlainTextVariable(); this.cipheredTextParser = new dotenv_parser_1.DotenvParserCipheredVariable(); if (pathOrBuffer) { if (typeof pathOrBuffer === "string") { this.dotenvPath = pathOrBuffer; this.dotenvAsBuffer = fs_1.default.readFileSync(pathOrBuffer); } else { this.dotenvAsBuffer = pathOrBuffer; } const env = dotenv_1.default.parse(this.dotenvAsBuffer); this.plainTextParser.parseObject(env); this.cipheredTextParser.parseObject(env); } else { this.cipheredTextParser.parseObject(process.env); } } defaultBackupPath() { if (lodash_1.default.isEmpty(this.dotenvPath)) { return "no_file_name_specified.bak"; } else { return `${this.dotenvPath}.bak`; } } createFileNameForBackupFile(backupFolder) { let fileName; if (backupFolder) { if (this.dotenvPath && !lodash_1.default.isEmpty(this.dotenvPath)) { fileName = util_1.createBackupFilePath(this.dotenvPath, backupFolder); } else { fileName = util_1.createBackupFilePath("", backupFolder); } } else { fileName = this.defaultBackupPath(); } return fileName; } createBackupFile(backupFolder) { const fileName = this.createFileNameForBackupFile(backupFolder); assert.ok(this.dotenvAsBuffer); fs_1.default.writeFileSync(fileName, this.dotenvAsBuffer); return fileName; } determineMatchingCipheredAndPlainTextVariables(decryptResults, encryptResults) { decryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((y) => { const ciphered = this.cipheredTextParser.variables.find((z) => z.leftSide === y.oldDotenvVariableName); // find the plain text variable const plainText = this.plainTextParser.variables.find((z) => z.leftSide === y.newDotenvVariableName); if (ciphered && plainText) { // Both the ciphered text and the plain text exist let plainTextEncryptionResult = encryptResults === null || encryptResults === void 0 ? void 0 : encryptResults.find((z) => z.status === crypto_wrapper_1.StatusCryptoWrapperResult.success && z.oldDotenvVariableName === plainText.leftSide); if (y.result === plainText.rightSide) { this.matchingDotenvVariables.push({ plainText, ciphered, plainTextEncryptionResult: plainTextEncryptionResult, cipheredDecryptionResult: y, }); } else { this.nonMatchingDotenvVariables.push({ plainText, ciphered, plainTextEncryptionResult: plainTextEncryptionResult, cipheredDecryptionResult: y, }); } } }); this.matchingDotenvVariables = lodash_1.default.uniqWith(this.matchingDotenvVariables, dotenv_variable_1.isEqualMatchingDotenvVariable); this.nonMatchingDotenvVariables = lodash_1.default.uniqWith(this.nonMatchingDotenvVariables, dotenv_variable_1.isEqualMatchingDotenvVariable); } feedbackPlainTextVariables(encryptResults) { if (encryptResults) { return lodash_1.default.uniq(lodash_1.default.difference(this.plainTextParser.variables.map((x) => x.leftSide), encryptResults.map((x) => x.oldDotenvVariableName))).sort(); } else { return []; } } feedbackErrorDecryptingVariables(decryptResults) { const failureExplanation = []; decryptResults .filter((x) => x.status !== crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((y) => failureExplanation.push(...y.failureExplanation)); return lodash_1.default.uniq(failureExplanation).sort(); } feedbackErrorEncryptingVariables(encryptResults) { const failureExplanation = []; encryptResults .filter((x) => x.status !== crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((y) => failureExplanation.push(...y.failureExplanation)); return lodash_1.default.uniq(failureExplanation).sort(); } feedbackErrorValuesDoNotMatch() { return this.nonMatchingDotenvVariables .map((x) => `Mismatch between [${x.plainText.leftSide}] and [${x.ciphered.leftSide}]. [${x.plainText.rightSide}]!=[${x.cipheredDecryptionResult.result}]`) .sort(); } feedbackInfoMatchingValues() { return this.matchingDotenvVariables .map((x) => `Match between [${x.plainText.leftSide}] and [${x.ciphered.leftSide}]`) .sort(); } feedbackSuccessfullyDecrypted(decryptResults) { return lodash_1.default.uniq(decryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .map((y) => y.oldDotenvVariableName)).sort(); } feedbackSuccessfullyEncrypted(encryptResults) { return lodash_1.default.uniq(encryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .map((y) => y.oldDotenvVariableName)).sort(); } feedbackEncryptionKeyErrors() { return this.cryptoKeyMgr.errors(); } feedbackCipheredVariableErrors() { return lodash_1.default.flatten(this.cipheredTextParser.variableInvalid.map((x) => x.errors)); } feedbackPlainTextVariableErrors() { return lodash_1.default.flatten(this.plainTextParser.variableInvalid.map((x) => x.errors)); } feedbackMissingEncryptionKeys() { const missing = lodash_1.default.uniq(lodash_1.default.difference(lodash_1.default.concat(this.plainTextParser.variables.map((x) => x.leftCryptoKeyName()), this.cipheredTextParser.variables.map((y) => y.leftCryptoKeyName())), lodash_1.default.concat(this.cryptoKeyMgr.all().map((z) => z.kid), reservedWords.keyNames))).sort(); return missing.map((x) => this.cryptoKeyMgr.buildDotenvNameFromKeyName(x)); } feedbackCipheredVariables() { return this.cipheredTextParser.variables.map((y) => y.leftSide).sort(); } feedbackEncryptionKeys() { return this.cryptoKeyMgr.parser.variables.map((x) => x.leftSide).sort(); } feedbackInvalidVariableFormats() { // Rewrite the code so test coverage is correctly reported. <groan> // const p = this.plainTextParser.variableInvalid.map(x => x.leftSide); // const c = this.cipheredTextParser.variableInvalid.map(y => y.leftSide); // return _.uniq(p.concat(c)).sort(); return lodash_1.default.uniq(lodash_1.default.concat(this.plainTextParser.variableInvalid.map((x) => x.leftSide), this.cipheredTextParser.variableInvalid.map((y) => y.leftSide))).sort(); } testImplementation(decryptResults, encryptResults) { assert.ok(!lodash_1.default.isEmpty(this.dotenvPath)); const feedback = Object.assign({}, feedback_1.createDefaultFeedback()); // this.dotenvPath cannot be null in this code path. Force to a string. feedback.dotenvSourceFileName = this.dotenvPath; this.determineMatchingCipheredAndPlainTextVariables(decryptResults, encryptResults); //----------------------------------------------- feedback.errors.errorDecryptingVariables = this.feedbackErrorDecryptingVariables(decryptResults); feedback.errors.cipheredVariableErrors = this.feedbackCipheredVariableErrors(); feedback.errors.encryptionKeyErrors = this.feedbackEncryptionKeyErrors(); feedback.errors.errorValuesDoNotMatch = this.feedbackErrorValuesDoNotMatch(); feedback.errors.invalidVariableFormat = this.feedbackInvalidVariableFormats(); feedback.errors.plainTextVariableErrors = this.feedbackPlainTextVariableErrors(); feedback.errors.plainTextVariables = this.feedbackPlainTextVariables(encryptResults); feedback.info.encryptedVariables = this.feedbackCipheredVariables(); feedback.info.encryptionKeys = this.feedbackEncryptionKeys(); feedback.info.matchingPlainTextAndCipherValues = this.feedbackInfoMatchingValues(); feedback.warnings.missingEncryptionKeys = this.feedbackMissingEncryptionKeys(); if (encryptResults) { feedback.errors.errorEncryptingVariables = this.feedbackErrorEncryptingVariables(encryptResults); feedback.info.successfullyDecrypted = this.feedbackSuccessfullyDecrypted(decryptResults); feedback.info.successfullyEncrypted = this.feedbackSuccessfullyEncrypted(encryptResults); } // Make the JSON object easier to read by sorting the properties by key name. const sortedFeedback = util_1.sortObjectByKeys(feedback); return sortedFeedback; } test() { const decryptResults = this.decrypt(); const encryptResults = this.encrypt(); return feedback_1.coerceFeedbackPathsToRelativePaths(this.testImplementation(decryptResults, encryptResults)); } decrypt() { const result = this.cryptoWrapper.decryptWithKeyManager(this.cipheredTextParser.variables); return result; } encrypt() { const result = this.cryptoWrapper.encryptWithKeyManager(this.plainTextParser.variables); return result; } decryptImplementation() { assert.ok(this.dotenvAsBuffer); const decryptResults = this.decrypt(); const feedback = this.testImplementation(decryptResults, undefined); let dotenvAsString = this.dotenvAsBuffer.toString(); // Key: exists // PlainText: exists // Cipher: exists // Cipher decryption: success // compare values. // If values match, remove cipher text // If values DO NOT match, update the plain text and remove cipher text { this.matchingDotenvVariables.forEach((x) => { const pattern = `^\\s*?${x.ciphered.leftSide}\\s*?=.*?$`; const rex = new RegExp(pattern, "gm"); dotenvAsString = dotenvAsString.replace(rex, ""); }); this.nonMatchingDotenvVariables.forEach((x) => { let pattern = `^\\s*?${x.plainText.leftSide}\\s*?=.*?$`; let rex = new RegExp(pattern, "gm"); const replacement = `${x.plainText.leftSide}=${x.cipheredDecryptionResult.result}`; dotenvAsString = dotenvAsString.replace(rex, replacement); pattern = `^\\s*?${x.ciphered.leftSide}\\s*?=.*?$`; rex = new RegExp(pattern, "gm"); dotenvAsString = dotenvAsString.replace(rex, ""); }); } // Key: exists // PlainText: missing // Cipher: exists // Cipher decryption: success // replace cipher with plain text { decryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((x) => { const pattern = `^\\s*?${x.oldDotenvVariableName}\\s*?=.*?$`; const rex = new RegExp(pattern, "gm"); const replacement = `${x.newDotenvVariableName}=${x.result}`; dotenvAsString = dotenvAsString.replace(rex, replacement); }); } return { feedback, dotenvAsString, }; } decryptToProcessEnvironment() { const decryptResult = this.decryptImplementation(); const parsed = dotenv_1.default.parse(decryptResult.dotenvAsString); Object.keys(parsed).forEach(function (key) { // Overwrite env value. This behavior differs from dotenv. process.env[key] = parsed[key]; }); return feedback_1.coerceFeedbackPathsToRelativePaths(decryptResult.feedback); } decryptToFile(destinationFile) { const decryptResult = this.decryptImplementation(); fs_1.default.writeFileSync(destinationFile, decryptResult.dotenvAsString); decryptResult.feedback.dotenvOutputFileName = destinationFile; return feedback_1.coerceFeedbackPathsToRelativePaths(decryptResult.feedback); } encryptToFile(destinationFile) { assert.ok(this.dotenvAsBuffer); const encryptResults = this.encrypt(); const decryptResults = this.decrypt(); const feedback = this.testImplementation(decryptResults, encryptResults); feedback.dotenvOutputFileName = destinationFile; let dotenvAsString = this.dotenvAsBuffer.toString(); // Key: exists // PlainText: exists // Plain text encryption: success // Cipher: exists // Cipher decryption: success // compare values. If values match, remove plain text { this.matchingDotenvVariables.forEach((x) => { const pattern = `^\\s*?${x.plainText.leftSide}\\s*?=.*?$`; const rex = new RegExp(pattern, "gm"); dotenvAsString = dotenvAsString.replace(rex, ""); }); } // Key: exists // PlainText: exists // Plain text encryption: success // Cipher: exists // Cipher decryption: success // compare values. If values DO NOT match, remove plain text // update cipher value { this.nonMatchingDotenvVariables.forEach((x) => { let pattern = `^\\s*?${x.plainText.leftSide}\\s*?=.*?$`; let rex = new RegExp(pattern, "gm"); dotenvAsString = dotenvAsString.replace(rex, ""); }); encryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((y) => { let pattern = `^\\s*?${y.newDotenvVariableName}\\s*?=.*?$`; let rex = new RegExp(pattern, "gm"); const replacement = `${y.newDotenvVariableName}=${y.result}`; dotenvAsString = dotenvAsString.replace(rex, replacement); }); } // Key: exists // PlainText: exists // Plain text encryption: success // Cipher: missing // Cipher decryption: n/a // replace plain text with cipher { encryptResults .filter((x) => x.status === crypto_wrapper_1.StatusCryptoWrapperResult.success) .forEach((y) => { const pattern = `^\\s*?${y.oldDotenvVariableName}\\s*?=.*?$`; const rex = new RegExp(pattern, "gm"); const replacementString = `${y.newDotenvVariableName}=${y.result}`; dotenvAsString = dotenvAsString.replace(rex, replacementString); }); } fs_1.default.writeFileSync(destinationFile, dotenvAsString); return feedback_1.coerceFeedbackPathsToRelativePaths(feedback); } } exports.DotenvProcessor = DotenvProcessor; //# sourceMappingURL=dotenv_processor.js.map