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
JavaScript
"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