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/malformed.json
|
||||||
!test/fixtures/bom.json
|
!test/fixtures/bom.json
|
||||||
!test/fixtures/no-bom.json
|
!test/fixtures/no-bom.json
|
||||||
|
!test/fixtures/secure.json
|
||||||
node_modules/
|
node_modules/
|
||||||
node_modules/*
|
node_modules/*
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
26
.travis.yml
26
.travis.yml
|
@ -1,14 +1,28 @@
|
||||||
language: node_js
|
|
||||||
sudo: false
|
sudo: false
|
||||||
before_install:
|
language: node_js
|
||||||
- '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || npm install -g npm@1.4.28'
|
|
||||||
- npm install -g npm@latest
|
|
||||||
node_js:
|
node_js:
|
||||||
- "0.8"
|
- "0.8"
|
||||||
- "0.10"
|
- "0.10"
|
||||||
- "0.11"
|
|
||||||
- "0.12"
|
- "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:
|
notifications:
|
||||||
email:
|
email:
|
||||||
- travis@nodejitsu.com
|
- travis@nodejitsu.com
|
||||||
|
|
49
README.md
49
README.md
|
@ -216,7 +216,9 @@ Loads a given object literal into the configuration hierarchy. Both `nconf.defau
|
||||||
```
|
```
|
||||||
|
|
||||||
### File
|
### 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
|
``` js
|
||||||
nconf.file('path/to/your/config.json');
|
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.
|
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
|
### Redis
|
||||||
There is a separate Redis-based store available through [nconf-redis][0]. To install and use this store simply:
|
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
|
## Installation
|
||||||
|
|
||||||
### Installing npm (node package manager)
|
|
||||||
```
|
```
|
||||||
curl http://npmjs.org/install.sh | sh
|
npm install nconf --save
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run Tests
|
## Run Tests
|
||||||
|
|
|
@ -8,8 +8,12 @@
|
||||||
var fs = require('fs'),
|
var fs = require('fs'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
common = require('./nconf/common'),
|
common = require('./nconf/common'),
|
||||||
Provider = require('./nconf/provider').Provider,
|
Provider = require('./nconf/provider').Provider;
|
||||||
nconf = module.exports = new Provider();
|
|
||||||
|
//
|
||||||
|
// `nconf` is by default an instance of `nconf.Provider`.
|
||||||
|
//
|
||||||
|
var nconf = module.exports = new Provider();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Expose the version from the package.json
|
// 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'),
|
path = require('path'),
|
||||||
util = require('util'),
|
util = require('util'),
|
||||||
formats = require('../formats'),
|
formats = require('../formats'),
|
||||||
|
@ -30,7 +31,25 @@ var File = exports.File = function (options) {
|
||||||
this.file = options.file;
|
this.file = options.file;
|
||||||
this.dir = options.dir || process.cwd();
|
this.dir = options.dir || process.cwd();
|
||||||
this.format = options.format || formats.json;
|
this.format = options.format || formats.json;
|
||||||
this.json_spacing = options.json_spacing || 2;
|
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) {
|
if (options.search) {
|
||||||
this.search(this.dir);
|
this.search(this.dir);
|
||||||
|
@ -53,9 +72,7 @@ File.prototype.save = function (value, callback) {
|
||||||
value = null;
|
value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFile(this.file, this.format.stringify(this.store, null, this.json_spacing), function (err) {
|
fs.writeFile(this.file, this.stringify(), callback);
|
||||||
return err ? callback(err) : callback();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -66,12 +83,7 @@ File.prototype.save = function (value, callback) {
|
||||||
// using the format specified by `this.format` synchronously.
|
// using the format specified by `this.format` synchronously.
|
||||||
//
|
//
|
||||||
File.prototype.saveSync = function (value) {
|
File.prototype.saveSync = function (value) {
|
||||||
try {
|
fs.writeFileSync(this.file, this.stringify());
|
||||||
fs.writeFileSync(this.file, this.format.stringify(this.store, null, this.json_spacing));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
throw(ex);
|
|
||||||
}
|
|
||||||
return this.store;
|
return this.store;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,12 +109,13 @@ File.prototype.load = function (callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//deals with string that include BOM
|
// Deals with string that include BOM
|
||||||
var stringData = data.toString();
|
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.parse(stringData);
|
||||||
self.store = self.format.parse(stringData);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return callback(new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message));
|
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.
|
// and responds appropriately.
|
||||||
//
|
//
|
||||||
File.prototype.loadSync = function () {
|
File.prototype.loadSync = function () {
|
||||||
var data, self = this;
|
if (!existsSync(this.file)) {
|
||||||
|
this.store = {};
|
||||||
if (!existsSync(self.file)) {
|
return this.store;
|
||||||
self.store = {};
|
|
||||||
data = {};
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
//
|
//
|
||||||
// Else, the path exists, read it from disk
|
// Else, the path exists, read it from disk
|
||||||
//
|
//
|
||||||
try {
|
try {
|
||||||
//deals with file that include BOM
|
// Deals with file that include BOM
|
||||||
var fileData = fs.readFileSync(this.file, 'utf8');
|
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 = this.parse(fileData);
|
||||||
this.store = data;
|
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
throw new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message);
|
throw new Error("Error parsing your configuration file: [" + this.file + ']: ' + ex.message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
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;
|
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);
|
}).export(module);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue