diff --git a/global-vars.js b/global-vars.js index a52e832c..f60ae431 100644 --- a/global-vars.js +++ b/global-vars.js @@ -14,6 +14,7 @@ module.exports = { envIndexPath: path.join(boxes, 'data', 'index.json'), bakerForMacPath, bakerSSHConfig, - privateKey + privateKey, // spinnerDot : 'dots' + version: require('./package.json').version }; diff --git a/lib/bakelets/config/vault.js b/lib/bakelets/config/vault.js new file mode 100644 index 00000000..1ab8d90b --- /dev/null +++ b/lib/bakelets/config/vault.js @@ -0,0 +1,96 @@ +const Bakelet = require('../bakelet'); +const path = require('path'); + +const { privateKey } = require('../../../global-vars'); +const conf = require('../../modules/configstore') + +const Ansible = require('../../modules/configuration/ansible'); +const VaultLib = require('../../modules/vault'); +const Ssh = require('../../modules/ssh'); + +class Vault extends Bakelet { + + constructor(name,ansibleSSHConfig, version) { + super(ansibleSSHConfig); + + this.name = name; + this.version = version; + } + + + async promptPass() + { + return new Promise(function(resolve,reject) + { + var properties = [ + { + name: 'password', + hidden: true + } + ]; + + prompt.start(); + + prompt.get(properties, function (err, result) { + if (err) { reject(err); } + else + { + resolve(result.password) + } + }); + }); + } + + + async load(obj, variables) + { + + let passphraseKey = `vault:${process.cwd()}`; + let passphrase = ''; + if (conf.has(passphraseKey)) + { + passphrase = conf.get(passphraseKey); + } + else + { + passphrase = promptPass(); + } + let vault = new VaultLib(); + + if( Array.isArray(obj.vault) ) + { + this.vault = obj.vault; + this.variables = variables || {}; + + for (let entry of obj.vault) + { + let file = path.join(this.bakePath, entry.file); + let content = vault.retrieve(file, passphrase); + + await Ssh.writeContentToDest(content, + `/home/vagrant/baker/${this.name}/templates/${entry.file}`, + this.ansibleSSHConfig, + false + ); + } + } + } + + async install() + { + if( this.vault ) + { + for (let entry of this.vault) + { + await Ansible.runAnsibleTemplateCmd( + {name: this.name}, `/home/vagrant/baker/${this.name}/templates/${entry.file}`, + entry.dest, this.variables, this.ansibleSSHConfig, this.verbose); + } + } + } + + +} + +module.exports = Vault; + diff --git a/lib/commands/vault.js b/lib/commands/vault.js new file mode 100644 index 00000000..072423fb --- /dev/null +++ b/lib/commands/vault.js @@ -0,0 +1,115 @@ + +const Baker = require('../modules/baker'); +const conf = require('../../lib/modules/configstore') +const Print = require('../modules/print'); +const Spinner = require('../modules/spinner'); + +const fs = require('fs'); +const prompt = require('prompt'); + +const VaultLib = require('../modules/vault'); + +const spinnerDot = conf.get('spinnerDot'); + +exports.command = 'vault '; +exports.desc = `encrypt a file`; + +exports.builder = (yargs) => { + yargs + .example(`$0 vault secret.json`, `Encrypt the secret.json file with a passphrase`) + .example(`$0 vault -v secret.json`, `View unencrypted content with passphrase`) + .example(`$0 vault -u secret.json`, `Unencrypt content with passphrase`) + + yargs + .positional('file', { + describe: 'file to encrypt', + type: 'string' + }); + + yargs.options({ + view: { + alias: 'v', + describe: `view unencrypted content with passphrase`, + demand: false, + type: 'boolean' + }, + decrypt: { + alias: 'u', + describe: `Unencrypt content with passphrase`, + demand: false, + type: 'boolean' + }, + }); + + +} + + +async function promptPass() +{ + return new Promise(function(resolve,reject) + { + var properties = [ + { + name: 'password', + hidden: true + } + ]; + + prompt.start(); + + prompt.get(properties, function (err, result) { + if (err) { reject(err); } + else + { + resolve(result.password) + } + }); + }); +} + + +exports.handler = async function(argv) { + let { envName, verbose } = argv; + + try{ + // await Spinner.spinPromise(BakerObj.start(envName, verbose), `Starting VM: ${envName}`, spinnerDot); + if( !fs.existsSync(argv.file) ) + { + throw new Error(`The provide file does not exist: ${argv.file}`) + } + + let passphraseKey = `vault:${process.cwd()}`; + if (!conf.has(passphraseKey)) + { + let typedPassphrase = await promptPass(); + conf.set(passphraseKey, typedPassphrase); + } + + let passphrase = conf.get(passphraseKey); + let vault = new VaultLib(); + + if( argv.decrypt) + { + let content = vault.retrieve(argv.file, passphrase); + console.log("Decrypting contents and writing to file.") + fs.writeFileSync(argv.file, content); + } + else + { + if ( argv.view ) + { + let content = vault.retrieve(argv.file, passphrase); + console.log("Viewing decrypted contents:") + console.log(content); + } + else + { + vault.vault(argv.file, passphrase); + } + } + + } catch (err){ + Print.error(err); + } +} diff --git a/lib/modules/ssh.js b/lib/modules/ssh.js index 466b8943..69390b55 100644 --- a/lib/modules/ssh.js +++ b/lib/modules/ssh.js @@ -9,6 +9,7 @@ const path = require('path'); const print = require('./print') const scp2 = require('scp2'); const util = require('./utils/utils'); +const SCPClient = scp2.Client; const shell = require('node-powershell'); @@ -437,6 +438,37 @@ class Ssh { }); }; + + static async writeContentToDest(content, dest, destSSHConfig) { + let Ssh = this; + + return new Promise((resolve, reject) => { + + var client = new SCPClient({ + host: '127.0.0.1', + port: destSSHConfig.port, + username: destSSHConfig.user, + privateKey: fs.readFileSync(destSSHConfig.private_key, 'utf8'), + }); + + client.write({ + destination: dest, + content: Buffer.from(content) + }, function(err) + { + client.close(); + + if( err ) { + reject(); + } else { + resolve(); + } + }); + + }); + } + + static async copyFromHostToVM(src, dest, destSSHConfig, chmod_ = true) { let Ssh = this; diff --git a/lib/modules/vault.js b/lib/modules/vault.js new file mode 100644 index 00000000..17a92735 --- /dev/null +++ b/lib/modules/vault.js @@ -0,0 +1,80 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const {version} = require('../../global-vars') + +class VaultLib { + constructor() + { + this.iv = crypto.randomBytes(16); + this.algorithm = 'aes-256-ctr'; + this.salt = Buffer.from('5ebe2294ecd0e0f', 'hex'); + this.headerRegex = /baker-vault:\d+[.]\d+[.]\d+:[0-9A-Fa-f]{15}/g; + } + + isEncrypted(filePath) + { + let lines = fs.readFileSync(filePath).toString().split(/\r?\n/); + if( lines.length > 0 ) + { + let matches = lines[0].match( this.headerRegex ); + return matches != null; + } + return false; + } + + vault(filePath, passphrase) + { + if( this.isEncrypted(filePath) ) + { + throw new Error(`File is already encrypted: ${filePath}`); + } + + let key = this.generateKeyFromPhrase(passphrase); + let content = fs.readFileSync(filePath); + + let buffer = `baker-vault:${version}:${this.iv.toString('hex')}\n`; + buffer += this.encryptWithKey(content, key, this.iv); + + fs.writeFileSync(filePath, buffer); + + return key.toString('hex'); + } + + retrieve(filePath, passphrase) + { + if( this.isEncrypted(filePath) ) + { + let lines = fs.readFileSync(filePath).toString().split(/\r?\n/); + let key = this.generateKeyFromPhrase(passphrase); + let iv = Buffer.from(lines[0].split(':')[2],'hex'); + + return this.decryptWithKey(lines[1],key,iv); + } + throw new Error(`File is not encrypted: ${filePath}`); + } + + encryptWithKey(text, key, iv) { + let cipher = crypto.createCipheriv(this.algorithm, key, iv ) + let crypted = cipher.update(text,'utf8','hex') + crypted += cipher.final('hex'); + return crypted; + } + + generateKeyFromPhrase(passphrase) + { + return crypto.pbkdf2Sync(passphrase, this.salt, 100000, 32, 'sha512'); + } + + decryptWithKey(text, key, iv) + { + let decipher = crypto.createDecipheriv(this.algorithm, key, iv) + let dec = decipher.update(text,'hex','utf8') + dec += decipher.final('utf8'); + return dec; + } +} + +module.exports = VaultLib; +