Merge pull request #185 from indexzero/crypto
[wip] Allow for "secure" option to be passed to `nconf.stores.File`
This commit is contained in:
commit
d4ebf49908
7 changed files with 232 additions and 67 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
26
.travis.yml
26
.travis.yml
|
@ -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
|
||||
|
|
53
README.md
53
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
18
test/fixtures/secure.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue