From 2de2bc0b662878c0b79255a1c9910e859860e161 Mon Sep 17 00:00:00 2001 From: indexzero Date: Fri, 18 Sep 2015 20:40:53 -0700 Subject: [PATCH] [api] Allow for "secure" option to be passed to `nconf.stores.File` to perform content encryption / decryption with `crypto.createCipher`. --- lib/nconf/stores/file.js | 103 +++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/lib/nconf/stores/file.js b/lib/nconf/stores/file.js index f3caaac..0e7bc4e 100644 --- a/lib/nconf/stores/file.js +++ b/lib/nconf/stores/file.js @@ -26,11 +26,25 @@ var File = exports.File = function (options) { Memory.call(this, options); - this.type = 'file'; - this.file = options.file; - this.dir = options.dir || process.cwd(); - this.format = options.format || formats.json; - this.json_spacing = options.json_spacing || 2; + this.type = 'file'; + this.file = options.file; + this.dir = options.dir || process.cwd(); + this.format = options.format || formats.json; + this.secure = options.secure; + this.spacing = options.json_spacing + || options.spacing + || 2; + + if (this.secure) { + this.secure.alg = this.secure.alg || 'aes-256-ctr'; + if (this.secure.secretPath) { + this.secret = fs.readFileSync(this.secure.secretPath, 'utf8'); + } + + if (!this.secure.secret) { + throw new Error('secure.secret option is required'); + } + } if (options.search) { this.search(this.dir); @@ -53,9 +67,10 @@ File.prototype.save = function (value, callback) { value = null; } - fs.writeFile(this.file, this.format.stringify(this.store, null, this.json_spacing), function (err) { - return err ? callback(err) : callback(); - }); + var contents = this.format.stringify(this.store, null, this.spacing), + bytes = this.tryEncrypt(contents); + + fs.writeFile(this.file, bytes, callback); }; // @@ -66,12 +81,10 @@ File.prototype.save = function (value, callback) { // using the format specified by `this.format` synchronously. // File.prototype.saveSync = function (value) { - try { - fs.writeFileSync(this.file, this.format.stringify(this.store, null, this.json_spacing)); - } - catch (ex) { - throw(ex); - } + var contents = this.format.stringify(this.store, null, this.spacing), + bytes = this.tryEncrypt(contents); + + fs.writeFileSync(this.file, bytes); return this.store; }; @@ -97,12 +110,14 @@ File.prototype.load = function (callback) { } try { - //deals with string that include BOM + // Deals with string that include BOM var stringData = data.toString(); + if (stringData.charAt(0) === '\uFEFF') { + stringData = stringData.substr(1); + } - if (stringData.charAt(0) === '\uFEFF') stringData = stringData.substr(1); - self.store = self.format.parse(stringData); - + var contents = self.tryDecrypt(stringData); + self.store = self.format.parse(contents); } catch (ex) { return callback(new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message)); @@ -130,9 +145,11 @@ File.prototype.loadSync = function () { // Else, the path exists, read it from disk // try { - //deals with file that include BOM + // Deals with file that include BOM var fileData = fs.readFileSync(this.file, 'utf8'); - if (fileData.charAt(0) === '\uFEFF') fileData = fileData.substr(1); + if (fileData.charAt(0) === '\uFEFF') { + fileData = fileData.substr(1); + } data = this.format.parse(fileData); this.store = data; @@ -145,6 +162,40 @@ File.prototype.loadSync = function () { return data; }; +// +// ### function tryEncrypt () +// Returns an encrypted version of the contents IIF +// `this.secure` is enabled +// +File.prototype.tryEncrypt = function (contents) { + if (!this.secure) { return contents; } + + // + // Contents have already been stringified by the format + // so no need to re-stringify here. + // + return cipherConvert(contents, { + alg: this.secure.alg, + secret: this.secure.secret, + encs: { input: 'utf8', output: 'hex' } + }); +}; + +// +// ### function tryDecrypt (contents) +// Returns a decrypted version of the contents IFF +// `this.secure` is enabled. +// +File.prototype.tryDecrypt = function (contents) { + if (!this.secure) { return contents; } + + return cipherConvert(contents, { + alg: this.secure.alg, + secret: this.secure.secret, + encs: { input: 'hex', output: 'utf8' } + }); +}; + // // ### function search (base) // #### @base {string} Base directory (or file) to begin searching for the target file. @@ -235,3 +286,15 @@ File.prototype.search = function (base) { return fullpath; }; + +// +// ### function cipherConvert (contents, opts) +// Returns the result of the cipher operation +// on the contents contents. +// +function cipherConvert(contents, opts) { + var encs = opts.encs; + var cipher = crypto.createCipher(opts.alg, opts.secret); + return cipher.update(contents, encs.input, encs.output) + + cipher.final(encs.output); +}