diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ced318d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "arrowParens": "always", + "trailingComma": "all", + "singleQuote": true, + "htmlWhitespaceSensitivity": "ignore" +} diff --git a/.travis.yml b/.travis.yml index d65523e..60cb3c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ dist: bionic sudo: false language: node_js +cache: npm node_js: - "12" addons: + postgresql: "9.6" hosts: - db diff --git a/index.js b/index.js index ca6d43e..c728fd4 100644 --- a/index.js +++ b/index.js @@ -1,105 +1,111 @@ var crypto = require('crypto'); function EncryptedField(Sequelize, key, opt) { - if (!(this instanceof EncryptedField)) { - return new EncryptedField(Sequelize, key, opt); - } - - var self = this; - - opt = opt || {}; - self._algorithm = opt.algorithm || 'aes-256-cbc'; - self._iv_length = opt.iv_length || 16; - self.encrypted_field_name = undefined; - - var extraDecryptionKeys = []; - if (opt.extraDecryptionKeys) { - extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) ? - opt.extraDecryptionKeys : - Array(opt.extraDecryptionKeys); - } - self.decryptionKeys = ([key].concat(extraDecryptionKeys)) - .map(function (key) { - return new Buffer(key, 'hex'); - }); - self.encryptionKey = self.decryptionKeys[0]; - self.Sequelize = Sequelize; -}; + if (!(this instanceof EncryptedField)) { + return new EncryptedField(Sequelize, key, opt); + } + + var self = this; + + opt = opt || {}; + self._algorithm = opt.algorithm || 'aes-256-cbc'; + self._iv_length = opt.iv_length || 16; + self.encrypted_field_name = undefined; + + var extraDecryptionKeys = []; + if (opt.extraDecryptionKeys) { + extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) + ? opt.extraDecryptionKeys + : Array(opt.extraDecryptionKeys); + } + self.decryptionKeys = [key].concat(extraDecryptionKeys).map(function(key) { + return new Buffer(key, 'hex'); + }); + self.encryptionKey = self.decryptionKeys[0]; + self.Sequelize = Sequelize; +} EncryptedField.prototype.vault = function(name) { - var self = this; - - if (self.encrypted_field_name) { - throw new Error('vault already initialized'); - } - - self.encrypted_field_name = name; - - return { - type: self.Sequelize.BLOB, - get: function() { - var previous = this.getDataValue(name); - if (!previous) { - return {}; - } - - previous = new Buffer(previous); - - function decrypt(key) { - var iv = previous.slice(0, self._iv_length); - var content = previous.slice(self._iv_length, previous.length); - var decipher = crypto.createDecipheriv(self._algorithm, key, iv); - - var json = decipher.update(content, undefined, 'utf8') + decipher.final('utf8'); - return JSON.parse(json); - } - - var keyCount = self.decryptionKeys.length; - for (var i = 0; i < keyCount; i++) { - try { - return decrypt(self.decryptionKeys[i]); - } catch (error) { - if (i >= keyCount - 1) { - throw error; - } - } - } - }, - set: function(value) { - // if new data is set, we will use a new IV - var new_iv = crypto.randomBytes(self._iv_length); - - var cipher = crypto.createCipheriv(self._algorithm, self.encryptionKey, new_iv); - - cipher.end(JSON.stringify(value), 'utf-8'); - var enc_final = Buffer.concat([new_iv, cipher.read()]); - var previous = this.setDataValue(name, enc_final); + var self = this; + + if (self.encrypted_field_name) { + throw new Error('vault already initialized'); + } + + self.encrypted_field_name = name; + + return { + type: self.Sequelize.BLOB, + get: function() { + var previous = this.getDataValue(name); + if (!previous) { + return {}; + } + + previous = new Buffer(previous); + + function decrypt(key) { + var iv = previous.slice(0, self._iv_length); + var content = previous.slice(self._iv_length, previous.length); + var decipher = crypto.createDecipheriv(self._algorithm, key, iv); + + var json = + decipher.update(content, undefined, 'utf8') + decipher.final('utf8'); + return JSON.parse(json); + } + + var keyCount = self.decryptionKeys.length; + for (var i = 0; i < keyCount; i++) { + try { + return decrypt(self.decryptionKeys[i]); + } catch (error) { + if (i >= keyCount - 1) { + throw error; + } } - } + } + }, + set: function(value) { + // if new data is set, we will use a new IV + var new_iv = crypto.randomBytes(self._iv_length); + + var cipher = crypto.createCipheriv( + self._algorithm, + self.encryptionKey, + new_iv, + ); + + cipher.end(JSON.stringify(value), 'utf-8'); + var enc_final = Buffer.concat([new_iv, cipher.read()]); + var previous = this.setDataValue(name, enc_final); + }, + }; }; EncryptedField.prototype.field = function(name) { - var self = this; - - if (!self.encrypted_field_name) { - throw new Error('you must initialize the vault field before using encrypted fields'); - } - var encrypted_field_name = self.encrypted_field_name; - - return { - type: self.Sequelize.VIRTUAL, - set: function set_encrypted(val) { - // use `this` not self because we need to reference the sequelize instance - // not our EncryptedField instance - var encrypted = this[encrypted_field_name]; - encrypted[name] = val; - this[encrypted_field_name] = encrypted; - }, - get: function get_encrypted() { - var encrypted = this[encrypted_field_name]; - return encrypted[name]; - } - } + var self = this; + + if (!self.encrypted_field_name) { + throw new Error( + 'you must initialize the vault field before using encrypted fields', + ); + } + var encrypted_field_name = self.encrypted_field_name; + + return { + type: self.Sequelize.VIRTUAL, + set: function set_encrypted(val) { + // use `this` not self because we need to reference the sequelize instance + // not our EncryptedField instance + var encrypted = this[encrypted_field_name]; + encrypted[name] = val; + this[encrypted_field_name] = encrypted; + }, + get: function get_encrypted() { + var encrypted = this[encrypted_field_name]; + return encrypted[name]; + }, + }; }; module.exports = EncryptedField; diff --git a/package.json b/package.json index d72e51d..a37e7af 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "encrypted sequelize fields", "main": "index.js", "scripts": { + "format": "prettier --write '*.?s' 'test/*.?s'", "test": "mocha" }, "repository": { @@ -21,13 +22,14 @@ }, "homepage": "https://github.com/defunctzombie/sequelize-encrypted", "devDependencies": { - "babel-plugin-transform-async-to-generator": "6.8.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.10.3", - "babel-polyfill": "6.9.1", - "babel-preset-es2015": "6.9.0", - "babel-register": "6.9.0", - "mocha": "2.0.1", - "pg": "6.0.1", - "sequelize": "3.23.4" + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-register": "^6.26.0", + "mocha": "^6.2.1", + "pg": "^6.4.1", + "prettier": "^1.19.1", + "sequelize": "^3.34.0" } } diff --git a/test/index.js b/test/index.js index 7a36feb..6597ce4 100644 --- a/test/index.js +++ b/test/index.js @@ -2,7 +2,8 @@ import assert from 'assert'; import Sequelize from 'sequelize'; import EncryptedField from '../'; -const sequelize = new Sequelize('postgres://postgres@db:5432/postgres'); +const dbHost = process.env.DB_HOST || 'db'; +const sequelize = new Sequelize(`postgres://postgres@${dbHost}:5432/postgres`); const key1 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230e'; const key2 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230f'; @@ -11,110 +12,116 @@ const v1 = EncryptedField(Sequelize, key1); const v2 = EncryptedField(Sequelize, key2); describe('sequelize-encrypted', () => { - - const User = sequelize.define('user', { - name: Sequelize.STRING, - encrypted: v1.vault('encrypted'), - another_encrypted: v2.vault('another_encrypted'), - - // encrypted virtual fields - private_1: v1.field('private_1'), - private_2: v2.field('private_2'), + const User = sequelize.define('user', { + name: Sequelize.STRING, + encrypted: v1.vault('encrypted'), + another_encrypted: v2.vault('another_encrypted'), + + // encrypted virtual fields + private_1: v1.field('private_1'), + private_2: v2.field('private_2'), + }); + + before('create models', async () => { + await User.sync({ force: true }); + }); + + after(async () => { + await sequelize.close(); + }); + + it('should save an encrypted field', async () => { + const user = User.build(); + user.private_1 = 'test'; + + await user.save(); + const found = await User.findById(user.id); + assert.equal(found.private_1, user.private_1); + }); + + it('should support multiple encrypted fields', async () => { + const user = User.build(); + user.private_1 = 'baz'; + user.private_2 = 'foobar'; + await user.save(); + + const vault = EncryptedField(Sequelize, key2); + + const AnotherUser = sequelize.define('user', { + name: Sequelize.STRING, + another_encrypted: vault.vault('another_encrypted'), + private_2: vault.field('private_2'), + private_1: vault.field('private_1'), }); - before('create models', async () => { - await User.sync({force: true}); + const found = await AnotherUser.findById(user.id); + assert.equal(found.private_2, user.private_2); + + // encrypted with key1 and different field originally + // and thus can't be recovered with key2 + assert.equal(found.private_1, undefined); + }); + + it('should throw error on decryption using invalid key', async () => { + // attempt to use key2 for vault encrypted with key1 + const badEncryptedField = EncryptedField(Sequelize, key2); + const BadEncryptionUser = sequelize.define('user', { + name: Sequelize.STRING, + encrypted: badEncryptedField.vault('encrypted'), + private_1: badEncryptedField.field('private_1'), }); - it('should save an encrypted field', async () => { - const user = User.build(); - user.private_1 = 'test'; - - await user.save(); - const found = await User.findById(user.id); - assert.equal(found.private_1, user.private_1); + const model = User.build(); + model.private_1 = 'secret!'; + await model.save(); + + let threw; + try { + const found = await BadEncryptionUser.findById(model.id); + found.private_1; // trigger decryption + } catch (error) { + threw = error; + } + + assert.ok( + threw && /bad decrypt$/.test(threw.message), + 'should have thrown decryption error', + ); + }); + + it('should support extra decryption keys (to facilitate key rotation)', async () => { + const keyOneEncryptedField = EncryptedField(Sequelize, key1); + const keyTwoAndOneEncryptedField = EncryptedField(Sequelize, key2, { + extraDecryptionKeys: [key1], }); - it('should support multiple encrypted fields', async() => { - const user = User.build(); - user.private_1 = 'baz'; - user.private_2 = 'foobar'; - await user.save(); - - const vault = EncryptedField(Sequelize, key2); - - const AnotherUser = sequelize.define('user', { - name: Sequelize.STRING, - another_encrypted: vault.vault('another_encrypted'), - private_2: vault.field('private_2'), - private_1: vault.field('private_1'), - }); - - const found = await AnotherUser.findById(user.id); - assert.equal(found.private_2, user.private_2); - - // encrypted with key1 and different field originally - // and thus can't be recovered with key2 - assert.equal(found.private_1, undefined); + // models both access the same table, with different encryption keys + const KeyOneModel = sequelize.define('rotateMe', { + encrypted: keyOneEncryptedField.vault('encrypted'), + private: keyOneEncryptedField.field('private'), }); - - it('should throw error on decryption using invalid key', async() => { - // attempt to use key2 for vault encrypted with key1 - const badEncryptedField = EncryptedField(Sequelize, key2); - const BadEncryptionUser = sequelize.define('user', { - name: Sequelize.STRING, - encrypted: badEncryptedField.vault('encrypted'), - private_1: badEncryptedField.field('private_1'), - }); - - const model = User.build(); - model.private_1 = 'secret!'; - await model.save(); - - let threw; - try { - const found = await BadEncryptionUser.findById(model.id) - found.private_1; // trigger decryption - } catch (error) { - threw = error; - } - - assert.ok(threw && /bad decrypt$/.test(threw.message), - 'should have thrown decryption error'); + const KeyTwoAndOneModel = sequelize.define('rotateMe', { + encrypted: keyTwoAndOneEncryptedField.vault('encrypted'), + private: keyTwoAndOneEncryptedField.field('private'), }); - it('should support extra decryption keys (to facilitate key rotation)', async() => { - const keyOneEncryptedField = EncryptedField(Sequelize, key1); - const keyTwoAndOneEncryptedField = EncryptedField(Sequelize, key2, { - extraDecryptionKeys: [key1], - }); - - // models both access the same table, with different encryption keys - const KeyOneModel = sequelize.define('rotateMe', { - encrypted: keyOneEncryptedField.vault('encrypted'), - private: keyOneEncryptedField.field('private'), - }); - const KeyTwoAndOneModel = sequelize.define('rotateMe', { - encrypted: keyTwoAndOneEncryptedField.vault('encrypted'), - private: keyTwoAndOneEncryptedField.field('private'), - }); - - await KeyOneModel.sync({force: true}); - - const modelUsingKeyOne = KeyOneModel.build(); - const modelUsingKeyTwo = KeyTwoAndOneModel.build(); - modelUsingKeyOne.private = 'secret!'; - modelUsingKeyTwo.private = 'also secret!'; - await Promise.all([ - modelUsingKeyOne.save(), - modelUsingKeyTwo.save(), - ]); - - // note: both sets of data accessed via KeyTwoAndOneModel - const foundFromKeyOne = await KeyTwoAndOneModel.findById(modelUsingKeyOne.id); - const foundFromKeyTwo = await KeyTwoAndOneModel.findById(modelUsingKeyTwo.id); - - assert.equal(foundFromKeyOne.private, modelUsingKeyOne.private); - assert.equal(foundFromKeyTwo.private, modelUsingKeyTwo.private); - }); + await KeyOneModel.sync({ force: true }); + + const modelUsingKeyOne = KeyOneModel.build(); + const modelUsingKeyTwo = KeyTwoAndOneModel.build(); + modelUsingKeyOne.private = 'secret!'; + modelUsingKeyTwo.private = 'also secret!'; + await Promise.all([modelUsingKeyOne.save(), modelUsingKeyTwo.save()]); + + // note: both sets of data accessed via KeyTwoAndOneModel + const foundFromKeyOne = await KeyTwoAndOneModel.findById( + modelUsingKeyOne.id, + ); + const foundFromKeyTwo = await KeyTwoAndOneModel.findById( + modelUsingKeyTwo.id, + ); + + assert.equal(foundFromKeyOne.private, modelUsingKeyOne.private); + assert.equal(foundFromKeyTwo.private, modelUsingKeyTwo.private); + }); }); diff --git a/test/mocha.opts b/test/mocha.opts index 8df2fe0..5d491df 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,5 +1,5 @@ --reporter spec --check-leaks --bail ---compilers js:babel-register +--require babel-core/register --require babel-polyfill