Merge pull request #185 from indexzero/crypto

[wip] Allow for "secure" option to be passed to `nconf.stores.File`
master
Charlie Robbins 2015-09-20 01:13:26 -07:00
commit d4ebf49908
7 changed files with 232 additions and 67 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ test/fixtures/*.json
!test/fixtures/malformed.json
!test/fixtures/bom.json
!test/fixtures/no-bom.json
!test/fixtures/secure.json
node_modules/
node_modules/*
npm-debug.log

View File

@ -1,14 +1,28 @@
language: node_js
sudo: false
before_install:
- '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || npm install -g npm@1.4.28'
- npm install -g npm@latest
language: node_js
node_js:
- "0.8"
- "0.10"
- "0.11"
- "0.12"
- "iojs"
- "4.1"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || travis_retry npm install -g npm@1.4.28'
- '[ "${TRAVIS_NODE_VERSION}" == "0.8" ] || travis_retry npm install -g npm@2.14.2'
- travis_retry npm install
before_install:
- travis_retry npm install -g npm@2.5.1
- travis_retry npm install
script:
- npm test
matrix:
allow_failures:
- node_js: "0.8"
- node_js: "0.10"
notifications:
email:
- travis@nodejitsu.com

View File

@ -20,8 +20,8 @@ Using nconf is easy; it is designed to be a simple key-value store with support
// 3. A file located at 'path/to/config.json'
//
nconf.argv()
.env()
.file({ file: 'path/to/config.json' });
.env()
.file({ file: 'path/to/config.json' });
//
// Set a few variables on `nconf`.
@ -216,7 +216,9 @@ Loads a given object literal into the configuration hierarchy. Both `nconf.defau
```
### File
Based on the Memory store, but provides additional methods `.save()` and `.load()` which allow you to read your configuration to and from file. As with the Memory store, all method calls are synchronous with the exception of `.save()` and `.load()` which take callback functions. It is important to note that setting keys in the File engine will not be persisted to disk until a call to `.save()` is made. Note a custom key must be supplied as the first parameter for hierarchy to work if multiple files are used.
Based on the Memory store, but provides additional methods `.save()` and `.load()` which allow you to read your configuration to and from file. As with the Memory store, all method calls are synchronous with the exception of `.save()` and `.load()` which take callback functions.
It is important to note that setting keys in the File engine will not be persisted to disk until a call to `.save()` is made. Note a custom key must be supplied as the first parameter for hierarchy to work if multiple files are used.
``` js
nconf.file('path/to/your/config.json');
@ -229,6 +231,35 @@ The file store is also extensible for multiple file formats, defaulting to `JSON
If the file does not exist at the provided path, the store will simply be empty.
#### Encrypting file contents
As of `nconf@0.8.0` it is now possible to encrypt and decrypt file contents using the `secure` option:
``` js
nconf.file('secure-file', {
file: 'path/to/secure-file.json',
secure: {
secret: 'super-secretzzz-keyzz',
alg: 'aes-256-ctr'
}
})
```
This will encrypt each key using [`crypto.createCipher`](https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password), defaulting to `aes-256-ctr`. The encrypted file contents will look like this:
```
{
"config-key-name": {
"alg": "aes-256-ctr", // cipher used
"value": "af07fbcf" // encrypted contents
},
"another-config-key": {
"alg": "aes-256-ctr", // cipher used
"value": "e310f6d94f13" // encrypted contents
},
}
```
### Redis
There is a separate Redis-based store available through [nconf-redis][0]. To install and use this store simply:
@ -252,22 +283,8 @@ Once installing both `nconf` and `nconf-redis`, you must require both modules to
```
## Installation
### Installing npm (node package manager)
```
curl http://npmjs.org/install.sh | sh
```
### Installing nconf
```
[sudo] npm install nconf
```
## More Documentation
There is more documentation available through docco. I haven't gotten around to making a gh-pages branch so in the meantime if you clone the repository you can view the docs:
```
open docs/nconf.html
npm install nconf --save
```
## Run Tests

View File

@ -8,8 +8,12 @@
var fs = require('fs'),
async = require('async'),
common = require('./nconf/common'),
Provider = require('./nconf/provider').Provider,
nconf = module.exports = new Provider();
Provider = require('./nconf/provider').Provider;
//
// `nconf` is by default an instance of `nconf.Provider`.
//
var nconf = module.exports = new Provider();
//
// Expose the version from the package.json

View File

@ -5,7 +5,8 @@
*
*/
var fs = require('fs'),
var crypto = require('crypto'),
fs = require('fs'),
path = require('path'),
util = require('util'),
formats = require('../formats'),
@ -26,11 +27,29 @@ 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 = typeof this.secure === 'string'
? { secret: this.secure }
: 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 +72,7 @@ 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();
});
fs.writeFile(this.file, this.stringify(), callback);
};
//
@ -66,12 +83,7 @@ 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);
}
fs.writeFileSync(this.file, this.stringify());
return this.store;
};
@ -97,12 +109,13 @@ 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);
self.store = self.parse(stringData);
}
catch (ex) {
return callback(new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message));
@ -119,30 +132,81 @@ File.prototype.load = function (callback) {
// and responds appropriately.
//
File.prototype.loadSync = function () {
var data, self = this;
if (!existsSync(self.file)) {
self.store = {};
data = {};
}
else {
//
// Else, the path exists, read it from disk
//
try {
//deals with file that include BOM
var fileData = fs.readFileSync(this.file, 'utf8');
if (fileData.charAt(0) === '\uFEFF') fileData = fileData.substr(1);
data = this.format.parse(fileData);
this.store = data;
}
catch (ex) {
throw new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message);
}
if (!existsSync(this.file)) {
this.store = {};
return this.store;
}
return data;
//
// Else, the path exists, read it from disk
//
try {
// Deals with file that include BOM
var fileData = fs.readFileSync(this.file, 'utf8');
if (fileData.charAt(0) === '\uFEFF') {
fileData = fileData.substr(1);
}
this.store = this.parse(fileData);
}
catch (ex) {
throw new Error("Error parsing your configuration file: [" + this.file + ']: ' + ex.message);
}
return this.store;
};
//
// ### function stringify ()
// Returns an encrypted version of the contents IIF
// `this.secure` is enabled
//
File.prototype.stringify = function () {
var data = this.store,
self = this;
if (this.secure) {
data = Object.keys(data).reduce(function (acc, key) {
var value = self.format.stringify(data[key]);
acc[key] = {
alg: self.secure.alg,
value: cipherConvert(value, {
alg: self.secure.alg,
secret: self.secure.secret,
encs: { input: 'utf8', output: 'hex' }
})
}
return acc;
}, {});
}
return this.format.stringify(data, null, this.spacing);
};
//
// ### function parse (contents)
// Returns a decrypted version of the contents IFF
// `this.secure` is enabled.
//
File.prototype.parse = function (contents) {
var parsed = this.format.parse(contents),
self = this;
if (!this.secure) {
return parsed;
}
return Object.keys(parsed).reduce(function (acc, key) {
var decrypted = cipherConvert(parsed[key].value, {
alg: parsed[key].alg || self.secure.alg,
secret: self.secure.secret,
encs: { input: 'hex', output: 'utf8' }
});
acc[key] = self.format.parse(decrypted);
return acc;
}, {});
};
//
@ -235,3 +299,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);
}

18
test/fixtures/secure.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"isNull": {
"alg": "aes-256-ctr",
"value": "af07fbcf"
},
"literal": {
"alg": "aes-256-ctr",
"value": "e310f6d94f13"
},
"arr": {
"alg": "aes-256-ctr",
"value": "9a78b783175e69bb8f3458042b1c098d8ed9613410fac185b3735099224f8fe4ece0f0da8decfddbbf0eab3b7c391c47772b5441"
},
"obj": {
"alg": "aes-256-ctr",
"value": "ba78b783175968add93a680429424ae4cf957d2916ebcfa399730bb17200ddb0ecacb183c1b1ebcd950ced76726964062e74643c995c47372bfb1311bee8f65bbeb5a1d9426537a6d83635220ec7934e1d7cc187f7218cd4afadfa2f107fb42c232d80d95c160ee704fa8e922998b0b3e47ec579dd0baef7cae6d7dbaa203d732adb5cff22b80d810d7191237999cd8dc528d8f2201ae128a9f9e2df96d1a816aa73e3e6b8e6246cd98b454e453b36f43f9117cb4af8fa85429a92"
}
}

View File

@ -222,5 +222,40 @@ vows.describe('nconf/stores/file').addBatch({
}
}
}
}).addBatch({
"When using the nconf file store": {
topic: function () {
var secureStore = new nconf.File({
file: path.join(__dirname, '..', 'fixtures', 'secure.json'),
secure: 'super-secretzzz'
});
secureStore.store = data;
return secureStore;
},
"the stringify() method should encrypt properly": function (store) {
var contents = JSON.parse(store.stringify());
Object.keys(data).forEach(function (key) {
assert.isObject(contents[key]);
assert.isString(contents[key].value);
assert.equal(contents[key].alg, 'aes-256-ctr');
});
},
"the parse() method should decrypt properly": function (store) {
var contents = store.stringify();
var parsed = store.parse(contents);
assert.deepEqual(parsed, data);
},
"the load() method should decrypt properly": function (store) {
store.load(function (err, loaded) {
assert.isNull(err);
assert.deepEqual(loaded, data);
});
},
"the loadSync() method should decrypt properly": function (store) {
var loaded = store.loadSync()
assert.deepEqual(loaded, data);
}
}
}).export(module);