Upgraded encryption using CipherIV (#322)

* Remove package-lock.json from gitignore

* Update dependencies and fix repo url

* Fix test

* Update to cipheriv

* Bump version

* Sync package-lock

* Revert extraneous package changes

* Revert minor doc change
master
Matt Hamann 2019-05-15 23:27:38 -04:00 committed by GitHub
parent 10318c0098
commit d582066743
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 33 deletions

4
.gitignore vendored
View File

@ -6,8 +6,8 @@ test/fixtures/*.json
!test/fixtures/bom.json !test/fixtures/bom.json
!test/fixtures/no-bom.json !test/fixtures/no-bom.json
!test/fixtures/secure.json !test/fixtures/secure.json
!test/fixtures/secure-iv.json
node_modules/ node_modules/
node_modules/* node_modules/*
npm-debug.log npm-debug.log
coverage coverage
package-lock.json

View File

@ -230,7 +230,7 @@ The input `obj` contains two properties passed in the following format:
The transformation function may alter both the key and the value. The transformation function may alter both the key and the value.
The function may return either an object in the asme format as the input or a value that evaluates to false. The function may return either an object in the same format as the input or a value that evaluates to false.
If the return value is falsey, the entry will be dropped from the store, otherwise it will replace the original key/value. If the return value is falsey, the entry will be dropped from the store, otherwise it will replace the original key/value.
*Note: If the return value doesn't adhere to the above rules, an exception will be thrown.* *Note: If the return value doesn't adhere to the above rules, an exception will be thrown.*
@ -397,17 +397,19 @@ nconf.file('secure-file', {
}) })
``` ```
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: This will encrypt each key using [`crypto.createCipheriv`](https://nodejs.org/api/crypto.html#crypto_crypto_createcipheriv_algorithm_key_iv_options), defaulting to `aes-256-ctr`. The encrypted file contents will look like this:
``` ```
{ {
"config-key-name": { "config-key-name": {
"alg": "aes-256-ctr", // cipher used "alg": "aes-256-ctr", // cipher used
"value": "af07fbcf" // encrypted contents "value": "af07fbcf", // encrypted contents
"iv": "49e7803a2a5ef98c7a51a8902b76dd10" // initialization vector
}, },
"another-config-key": { "another-config-key": {
"alg": "aes-256-ctr", // cipher used "alg": "aes-256-ctr", // cipher used
"value": "e310f6d94f13" // encrypted contents "value": "e310f6d94f13", // encrypted contents
"iv": "b654e01aed262f37d0acf200be193985" // initialization vector
}, },
} }
``` ```

View File

@ -8,7 +8,7 @@
var fs = require('fs'), var fs = require('fs'),
path = require('path'), path = require('path'),
util = require('util'), util = require('util'),
Secure = require('secure-keys'), crypto = require('crypto'),
formats = require('../formats'), formats = require('../formats'),
Memory = require('./memory').Memory; Memory = require('./memory').Memory;
@ -50,12 +50,6 @@ var File = exports.File = function (options) {
if (!this.secure.secret) { if (!this.secure.secret) {
throw new Error('secure.secret option is required'); throw new Error('secure.secret option is required');
} }
this.keys = new Secure({
secret: this.secure.secret,
alg: this.secure.alg,
format: this.format
});
} }
if (options.search) { if (options.search) {
@ -185,7 +179,16 @@ File.prototype.stringify = function (format) {
} }
if (this.secure) { if (this.secure) {
data = this.keys.encrypt(data); var self = this;
data = Object.keys(data).reduce(function (acc, key) {
var value = format.stringify(data[key]);
var iv = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(self.secure.alg, self.secure.secret, iv);
var ciphertext = cipher.update(value, 'utf8', 'hex');
ciphertext += cipher.final('hex');
acc[key] = { alg: self.secure.alg, value: ciphertext, iv: iv.toString('hex') };
return acc;
}, {});
} }
return format.stringify(data, null, this.spacing); return format.stringify(data, null, this.spacing);
@ -199,11 +202,31 @@ File.prototype.stringify = function (format) {
File.prototype.parse = function (contents) { File.prototype.parse = function (contents) {
var parsed = this.format.parse(contents); var parsed = this.format.parse(contents);
if (!this.secure) { if (this.secure) {
return parsed; var self = this;
var outdated = false;
parsed = Object.keys(parsed).reduce(function (acc, key) {
var value = parsed[key];
var decipher = crypto.createDecipher(value.alg, self.secure.secret);
if (value.iv) {
// For backward compatibility, use createDecipheriv only if there is iv stored in file
decipher = crypto.createDecipheriv(value.alg, self.secure.secret, Buffer.from(value.iv, 'hex'));
} else {
outdated = true;
}
var plaintext = decipher.update(value.value, 'hex', 'utf8');
plaintext += decipher.final('utf8');
acc[key] = self.format.parse(plaintext);
return acc;
}, {});
if (outdated) {
// warn user if the file is encrypted without iv
console.warn('Your encrypted file is outdated (encrypted without iv). Please re-encrypt your file.');
}
} }
return this.keys.decrypt(parsed); return parsed;
}; };

11
package-lock.json generated
View File

@ -3606,9 +3606,9 @@
}, },
"dependencies": { "dependencies": {
"resolve": { "resolve": {
"version": "1.10.1", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
"integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
"requires": { "requires": {
"path-parse": "^1.0.6" "path-parse": "^1.0.6"
} }
@ -4506,11 +4506,6 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
}, },
"secure-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
"integrity": "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o="
},
"semver": { "semver": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",

View File

@ -21,7 +21,6 @@
"dependencies": { "dependencies": {
"ini": "^1.3.0", "ini": "^1.3.0",
"jest": "^21.2.1", "jest": "^21.2.1",
"secure-keys": "^1.0.0",
"yargs": "^10.0.3" "yargs": "^10.0.3"
}, },
"devDependencies": { "devDependencies": {

22
test/fixtures/secure-iv.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"isNull": {
"alg": "aes-256-ctr",
"value": "16f39325",
"iv": "49e7803a2a5ef98c7a51a8902b76dd10"
},
"literal": {
"alg": "aes-256-ctr",
"value": "647965c22605",
"iv": "b654e01aed262f37d0acf200be193985"
},
"arr": {
"alg": "aes-256-ctr",
"value": "70c6d3b2ec3820e4d4fa3d7834fcf7fd51190a9e580944583ca7b30f42d230b7f271e72287f6c2ab4624685c779936aa2ed19b90",
"iv": "b5263665331158c2165fe623a895870f"
},
"obj": {
"alg": "aes-256-ctr",
"value": "521f1995698896d2fe7d76d16bec9e86b9b5b25bcfae3a4f3c52ad6f0425c3fd059793b9dec7927c616f4c4c57f2b9ee833f8c865d9aa42bbb37203763703755a0c79afd13f8c0e96e9ac47f490a12105583e31ff628104e6b216265b849cdf6642eb8a4b5bd6f532339a5f5737bd6a7ae6e7bb22148cbcfa549802336cb1322fb701151b25b5863c5c6d5047875aed9806350ae0e292c259e82d5a561eb672402d20230a0c3107ff2b1e6259ccb1bbbe7a25ce965ce56cb32f679",
"iv": "355f307363d69ed58345ae395744f5f0"
}
}

View File

@ -106,14 +106,14 @@ describe('nconf/hierarchy, When using nconf', () => {
var argv = ['--candy--something', 'foo', '--candy--something5--second', 'bar']; var argv = ['--candy--something', 'foo', '--candy--something5--second', 'bar'];
var data = ''; var data = '';
process.env.candy__bonbon = 'sweet'; process.env.candy__bonbon = 'sweet';
var child = spawn('node', [script].concat(argv)); var child = spawn('node', [script].concat(argv), {env: process.env});
delete process.env.candy__bonbon; delete process.env.candy__bonbon;
child.stdout.on('data', function (d) { child.stdout.on('data', function (d) {
data += d; data += d;
}); });
child.on('close', function () { child.on('close', function () {
console.log(data) console.log(data);
expect(JSON.parse(data)).toEqual({ expect(JSON.parse(data)).toEqual({
apples: true, apples: true,
candy: { candy: {

View File

@ -183,7 +183,7 @@ describe('nconf/stores/file', () => {
describe("When using the nconf file store", () => { describe("When using the nconf file store", () => {
it("the search() method when the target file exists higher in the directory tree should update the file appropriately", () => { it("the search() method when the target file exists higher in the directory tree should update the file appropriately", () => {
var searchBase = process.env.HOME; var searchBase = require('os').homedir();
var filePath = path.join(searchBase, '.nconf'); var filePath = path.join(searchBase, '.nconf');
fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
var store = new nconf.File({ var store = new nconf.File({
@ -206,8 +206,8 @@ describe('nconf/stores/file', () => {
}) })
describe("When using the nconf file store", () => { describe("When using the nconf file store", () => {
var secureStore = new nconf.File({ var secureStore = new nconf.File({
file: path.join(__dirname, '..', 'fixtures', 'secure.json'), file: path.join(__dirname, '..', 'fixtures', 'secure-iv.json'),
secure: 'super-secretzzz' secure: 'super-secret-key-32-characterszz'
}); });
secureStore.store = data; secureStore.store = data;
@ -218,6 +218,7 @@ describe('nconf/stores/file', () => {
expect(typeof contents[key]).toBe('object'); expect(typeof contents[key]).toBe('object');
expect(typeof contents[key].value).toBe('string'); expect(typeof contents[key].value).toBe('string');
expect(contents[key].alg).toEqual('aes-256-ctr'); expect(contents[key].alg).toEqual('aes-256-ctr');
expect(typeof contents[key].iv).toBe('string');
}); });
}); });
it("the parse() method should decrypt properly", () => { it("the parse() method should decrypt properly", () => {
@ -232,8 +233,27 @@ describe('nconf/stores/file', () => {
}); });
}); });
it("the loadSync() method should decrypt properly", () => { it("the loadSync() method should decrypt properly", () => {
var loaded = secureStore.loadSync() var loaded = secureStore.loadSync();
expect(loaded).toEqual(data); expect(loaded).toEqual(data);
}); });
}) })
});
describe("When using the nconf file store", () => {
var secureStore = new nconf.File({
file: path.join(__dirname, '..', 'fixtures', 'secure.json'),
secure: 'super-secretzzz'
});
it("the load() method should decrypt legacy file properly", () => {
secureStore.load(function (err, loaded) {
expect(err).toBe(null);
expect(loaded).toEqual(data);
});
});
it("the loadSync() method should decrypt legacy file properly", () => {
var loaded = secureStore.loadSync();
expect(loaded).toEqual(data);
});
})
});