@aminya/dotenv-vault
Version:
A secrets manager for .env files – from the same people that pioneered dotenv.
171 lines (170 loc) • 6.44 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalBuildService = void 0;
const tslib_1 = require("tslib");
const crypto = tslib_1.__importStar(require("crypto"));
const dotenv = tslib_1.__importStar(require("dotenv"));
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const vars_1 = require("../../vars");
const fs_1 = require("fs");
const core_1 = require("@oclif/core");
const append_to_ignore_service_1 = require("../../services/append-to-ignore-service");
const log_service_1 = require("../../services/log-service");
class LocalBuildService {
constructor(attrs = {}) {
this.cmd = attrs.cmd;
this.log = new log_service_1.LogService({ cmd: attrs.cmd });
}
async run() {
this.log.deprecated();
new append_to_ignore_service_1.AppendToIgnoreService().run();
const buildMsg = 'Building .env.vault from files on your machine';
core_1.CliUx.ux.action.start(`${chalk_1.default.dim(this.log.pretextLocal)}${buildMsg}`);
await this.build();
}
async build() {
(0, fs_1.writeFileSync)(this.keysName, this.keysData);
(0, fs_1.writeFileSync)(this.vaultName, this.vaultData);
core_1.CliUx.ux.action.stop();
this.log.local(`Built ${this.vaultName}`);
this.log.plain('');
this.log.plain('Next:');
this.log.plain(`1. Commit ${this.vaultName} to code`);
this.log.plain('2. Set DOTENV_KEY on server');
this.log.plain('3. Deploy your code');
this.log.plain('');
this.log.plain(`(Find your DOTENV_KEY in the ${chalk_1.default.bold(this.keysName)} file)`);
core_1.CliUx.ux.action.stop();
}
get vaultData() {
let s = `${vars_1.vars.vaultFileHeaderComment}\n\n`;
for (const file in this.envLookups) {
if (Object.prototype.hasOwnProperty.call(this.envLookups, file)) {
const environment = this.envLookups[file];
const dotenvKey = this.keys[`DOTENV_KEY_${environment.toUpperCase()}`];
const message = (0, fs_1.readFileSync)(file, 'utf8');
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey);
const ciphertext = this._encrypt(key, message);
s += `# ${environment}\n`;
s += `DOTENV_VAULT_${environment.toUpperCase()}="${ciphertext}"\n\n`;
}
}
return s;
}
get keys() {
const keys = {};
// grab current .env.keys
const parsed = (dotenv.configDotenv({ path: this.keysName }).parsed || {});
for (const file in this.envLookups) {
if (Object.prototype.hasOwnProperty.call(this.envLookups, file)) {
const environment = this.envLookups[file];
const key = `DOTENV_KEY_${environment.toUpperCase()}`;
let value = parsed[key];
// prevent overwriting current .env.keys data
if (!value || value.length === 0) {
value = this._generateDotenvKey(environment);
}
keys[key] = value;
}
}
return keys;
}
get keysData() {
let keysData = `${vars_1.vars.keysFileHeaderComment}\n`;
for (const key in this.keys) {
if (Object.prototype.hasOwnProperty.call(this.keys, key)) {
const value = this.keys[key];
keysData += `${key}="${value}"\n`;
}
}
return keysData;
}
get vaultName() {
return '.env.vault';
}
get keysName() {
return '.env.keys';
}
get envLookups() {
const dir = './';
const lookups = {};
const files = (0, fs_1.readdirSync)(dir);
for (const file of files) {
// must be a .env* file
if (!file.startsWith('.env')) {
continue;
}
// must not be .env.vault.something, or .env.me.something, etc.
if (this._reservedEnvFilePath(file)) {
continue;
}
// must not end with .previous
if (file.endsWith('.previous')) {
continue;
}
const environment = this._determineLikelyEnvironment(file);
lookups[file] = environment;
}
return lookups;
}
_reservedEnvFilePath(file) {
const reservedEnvFiles = ['.env.vault', '.env.keys', '.env.me'];
let result = false;
for (const reservedFile of reservedEnvFiles) {
if (file.startsWith(reservedFile)) {
result = true;
}
}
return result;
}
_determineLikelyEnvironment(file) {
const splitFile = file.split('.');
const possibleEnvironment = splitFile[2]; // ['', 'env', environment']
if (!possibleEnvironment || possibleEnvironment.length === 0) {
return 'development';
}
return possibleEnvironment;
}
_generateDotenvKey(environment) {
const rand = crypto.randomBytes(32).toString('hex');
return `dotenv://:key_${rand}@dotenv.local/vault/.env.vault?environment=${environment}`;
}
_parseEncryptionKeyFromDotenvKey(dotenvKey) {
// Parse DOTENV_KEY. Format is a URI
const uri = new URL(dotenvKey);
// Get decrypt key
const key = uri.password;
if (!key) {
throw new Error('INVALID_DOTENV_KEY: Missing key part');
}
return Buffer.from(key.slice(-64), 'hex');
}
_encrypt(key, message) {
// set up nonce
const nonce = this._generateNonce();
// set up cipher
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
// generate ciphertext
let ciphertext = '';
ciphertext += cipher.update(message, 'utf8', 'hex');
ciphertext += cipher.final('hex');
ciphertext += cipher.getAuthTag().toString('hex');
// prepend nonce
ciphertext = nonce.toString('hex') + ciphertext;
// base64 encode output
return Buffer.from(ciphertext, 'hex').toString('base64');
}
_generateNonce() {
return crypto.randomBytes(this._nonceBytes());
}
_keyBytes() {
return 32;
}
_authTagBytes() {
return 16;
}
_nonceBytes() {
return 12;
}
}
exports.LocalBuildService = LocalBuildService;