From 5dd7f75356d5fbaee8bb97fab3d0753e1f18d2f2 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:03:22 +0100 Subject: [PATCH 01/14] chore: prettier --- .prettierrc.json | 6 ++ index.js | 192 ++++++++++++++++++++++++---------------------- package.json | 1 + test/index.js | 196 ++++++++++++++++++++++++----------------------- 4 files changed, 205 insertions(+), 190 deletions(-) create mode 100644 .prettierrc.json 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/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..5fc93e9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "encrypted sequelize fields", "main": "index.js", "scripts": { + "format": "npx prettier --write '*.js' 'test/*.js'", "test": "mocha" }, "repository": { diff --git a/test/index.js b/test/index.js index 7a36feb..4678994 100644 --- a/test/index.js +++ b/test/index.js @@ -11,110 +11,112 @@ 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 }); + }); + + 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); + }); }); From 57ccdc3d3caa3c4d14edca668c02c7aafa210ffe Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:07:19 +0100 Subject: [PATCH 02/14] chore: allow overriding the db host --- test/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index 4678994..7db70e3 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'; From 1c077fc983de6f48e46e5ec8f8d36882cd26cc04 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:21:18 +0100 Subject: [PATCH 03/14] chore: move tests to ts-jest --- .babelrc | 6 --- package.json | 16 +++--- test/index.js | 123 --------------------------------------------- test/mocha.opts | 5 -- test/smoke.test.ts | 113 +++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 17 +++++++ 6 files changed, 137 insertions(+), 143 deletions(-) delete mode 100644 .babelrc delete mode 100644 test/index.js delete mode 100644 test/mocha.opts create mode 100644 test/smoke.test.ts create mode 100644 tsconfig.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 1113850..0000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "plugins": [ - "transform-async-to-generator", - "transform-es2015-modules-commonjs", - ] -} diff --git a/package.json b/package.json index 5fc93e9..8a7b6a3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "encrypted sequelize fields", "main": "index.js", "scripts": { - "format": "npx prettier --write '*.js' 'test/*.js'", - "test": "mocha" + "format": "npx prettier --write '*.js' 'test/*.?s'", + "test": "jest --detectOpenHandles --forceExit" }, "repository": { "type": "git", @@ -22,13 +22,11 @@ }, "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", + "@types/jest": "^24.0.18", + "@types/sequelize": "^4.28.5", + "jest": "^24.9.0", "pg": "6.0.1", - "sequelize": "3.23.4" + "sequelize": "3.23.4", + "ts-jest": "^24.1.0" } } diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 7db70e3..0000000 --- a/test/index.js +++ /dev/null @@ -1,123 +0,0 @@ -import assert from 'assert'; -import Sequelize from 'sequelize'; -import EncryptedField from '../'; - -const dbHost = process.env.DB_HOST || 'db'; -const sequelize = new Sequelize(`postgres://postgres@${dbHost}:5432/postgres`); - -const key1 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230e'; -const key2 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230f'; - -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'), - }); - - before('create models', async () => { - await User.sync({ force: true }); - }); - - 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'), - }); - - 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'), - }); - - 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], - }); - - // 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); - }); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 8df2fe0..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,5 +0,0 @@ ---reporter spec ---check-leaks ---bail ---compilers js:babel-register ---require babel-polyfill diff --git a/test/smoke.test.ts b/test/smoke.test.ts new file mode 100644 index 0000000..9dedbf9 --- /dev/null +++ b/test/smoke.test.ts @@ -0,0 +1,113 @@ +const Sequelize = require('sequelize'); +const EncryptedField = require('../'); + +const dbHost = process.env.DB_HOST || 'db'; +const sequelize = new Sequelize(`postgres://postgres@${dbHost}:5432/postgres`); + +const key1 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230e'; +const key2 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230f'; + +const v1 = EncryptedField(Sequelize, key1); +const v2 = EncryptedField(Sequelize, key2); + +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'), +}); + +beforeAll(async () => { + await User.sync({ force: true }); +}); + +test('should save an encrypted field', async () => { + const user = User.build(); + user.private_1 = 'test'; + + await user.save(); + const found = await User.findById(user.id); + expect(found.private_1).toEqual(user.private_1); +}); + +test('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); + expect(found.private_2).toEqual(user.private_2); + + // encrypted with key1 and different field originally + // and thus can't be recovered with key2 + expect(found.private_1).toBeUndefined(); +}); + +test('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; + } + + expect(threw && /bad decrypt$/.test(threw.message)).toBeTruthy(); +}); + +test('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); + + expect(foundFromKeyOne.private).toEqual(modelUsingKeyOne.private); + expect(foundFromKeyTwo.private).toEqual(modelUsingKeyTwo.private); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a6c214f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "target": "es2017", + "module": "commonjs", + "sourceMap": true, + "pretty": true, + "importHelpers": true, + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noImplicitReturns": true + }, + "include": [ + "./test/*" + ] +} From ffcf7d1e99cd46c1d527d6e010d7f6cd11608dde Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:26:37 +0100 Subject: [PATCH 04/14] chore: upgrade sequelize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a7b6a3..cc70a29 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@types/sequelize": "^4.28.5", "jest": "^24.9.0", "pg": "6.0.1", - "sequelize": "3.23.4", + "sequelize": "^3.34.0", "ts-jest": "^24.1.0" } } From 0c6cb229c200cbee2835e66fa37a1bd26f0910e1 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:27:06 +0100 Subject: [PATCH 05/14] chore: upgrade pg --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc70a29..7006026 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@types/jest": "^24.0.18", "@types/sequelize": "^4.28.5", "jest": "^24.9.0", - "pg": "6.0.1", + "pg": "^6.4.1", "sequelize": "^3.34.0", "ts-jest": "^24.1.0" } From 3588e844b0d5ae582f1da29a015d8e52ffc049a5 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:36:40 +0100 Subject: [PATCH 06/14] chore: eliminate new Buffer --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index c728fd4..f2fa6f3 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,7 @@ function EncryptedField(Sequelize, key, opt) { : Array(opt.extraDecryptionKeys); } self.decryptionKeys = [key].concat(extraDecryptionKeys).map(function(key) { - return new Buffer(key, 'hex'); + return Buffer.from(key, 'hex'); }); self.encryptionKey = self.decryptionKeys[0]; self.Sequelize = Sequelize; @@ -42,7 +42,7 @@ EncryptedField.prototype.vault = function(name) { return {}; } - previous = new Buffer(previous); + previous = Buffer.from(previous); function decrypt(key) { var iv = previous.slice(0, self._iv_length); From 97760bc58d926acf32ab5e005a9a7fc7e0905951 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:38:43 +0100 Subject: [PATCH 07/14] chore: var -> const --- index.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index f2fa6f3..9f25885 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,18 @@ -var crypto = require('crypto'); +const crypto = require('crypto'); function EncryptedField(Sequelize, key, opt) { if (!(this instanceof EncryptedField)) { return new EncryptedField(Sequelize, key, opt); } - var self = this; + const 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 = []; + let extraDecryptionKeys = []; if (opt.extraDecryptionKeys) { extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) ? opt.extraDecryptionKeys @@ -26,7 +26,7 @@ function EncryptedField(Sequelize, key, opt) { } EncryptedField.prototype.vault = function(name) { - var self = this; + const self = this; if (self.encrypted_field_name) { throw new Error('vault already initialized'); @@ -37,7 +37,7 @@ EncryptedField.prototype.vault = function(name) { return { type: self.Sequelize.BLOB, get: function() { - var previous = this.getDataValue(name); + let previous = this.getDataValue(name); if (!previous) { return {}; } @@ -45,17 +45,17 @@ EncryptedField.prototype.vault = function(name) { previous = Buffer.from(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); + const iv = previous.slice(0, self._iv_length); + const content = previous.slice(self._iv_length, previous.length); + const decipher = crypto.createDecipheriv(self._algorithm, key, iv); - var json = + const 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++) { + const keyCount = self.decryptionKeys.length; + for (let i = 0; i < keyCount; i++) { try { return decrypt(self.decryptionKeys[i]); } catch (error) { @@ -67,42 +67,42 @@ EncryptedField.prototype.vault = function(name) { }, set: function(value) { // if new data is set, we will use a new IV - var new_iv = crypto.randomBytes(self._iv_length); + const new_iv = crypto.randomBytes(self._iv_length); - var cipher = crypto.createCipheriv( + const 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); + const enc_final = Buffer.concat([new_iv, cipher.read()]); + this.setDataValue(name, enc_final); }, }; }; EncryptedField.prototype.field = function(name) { - var self = this; + const 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; + const 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]; + const encrypted = this[encrypted_field_name]; encrypted[name] = val; this[encrypted_field_name] = encrypted; }, get: function get_encrypted() { - var encrypted = this[encrypted_field_name]; + const encrypted = this[encrypted_field_name]; return encrypted[name]; }, }; From f7ff49cc9fa51e4a53e25bef953b18bd980ce3d8 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 13:46:48 +0100 Subject: [PATCH 08/14] chore: add eslint --- .eslintrc.json | 19 +++++++++++++++++++ package.json | 11 +++++++++-- tsconfig.json | 2 ++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..eaddb35 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "env": { + "node": true, + "es6": true, + "jest/globals": true + }, + "plugins": ["jest", "@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + "prettier/@typescript-eslint" + ] +} diff --git a/package.json b/package.json index 7006026..beb5dc2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "encrypted sequelize fields", "main": "index.js", "scripts": { - "format": "npx prettier --write '*.js' 'test/*.?s'", + "format": "npx prettier --write '*.?s' 'test/*.?s'", + "lint": "eslint '*.?s' 'test/*.?s'", "test": "jest --detectOpenHandles --forceExit" }, "repository": { @@ -24,9 +25,15 @@ "devDependencies": { "@types/jest": "^24.0.18", "@types/sequelize": "^4.28.5", + "@typescript-eslint/eslint-plugin": "^2.3.3", + "@typescript-eslint/parser": "^2.2.0", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.4.0", + "eslint-plugin-jest": "^22.17.0", "jest": "^24.9.0", "pg": "^6.4.1", "sequelize": "^3.34.0", - "ts-jest": "^24.1.0" + "ts-jest": "^24.1.0", + "typescript": "^3.6.3" } } diff --git a/tsconfig.json b/tsconfig.json index a6c214f..19d64f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,11 @@ "strict": true, "noImplicitAny": true, "noUnusedLocals": true, + "allowJs": true, "noImplicitReturns": true }, "include": [ + "./*", "./test/*" ] } From 087b2841ca3fb334f6e05674784bd0f771b9b2f4 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 14:30:52 +0100 Subject: [PATCH 09/14] chore: typescript --- index.js => index.ts | 30 ++++++++++++++++++++---------- jest.config.js | 10 ++++++++++ package.json | 6 ++++-- test/smoke.test.ts | 26 +++++++++++++++----------- tsconfig.json | 12 ++++-------- 5 files changed, 53 insertions(+), 31 deletions(-) rename index.js => index.ts (79%) create mode 100644 jest.config.js diff --git a/index.js b/index.ts similarity index 79% rename from index.js rename to index.ts index 9f25885..1a1518c 100644 --- a/index.js +++ b/index.ts @@ -1,7 +1,17 @@ -const crypto = require('crypto'); +import * as crypto from 'crypto'; +import { Instance, Sequelize } from 'sequelize'; -function EncryptedField(Sequelize, key, opt) { +type Key = string; + +export interface FieldOptions { + algorithm?: string; + iv_length?: number; + extraDecryptionKeys?: Key | Key[]; +} + +function EncryptedField(Sequelize: Sequelize, key: Key, opt?: FieldOptions) { if (!(this instanceof EncryptedField)) { + // @ts-ignore return new EncryptedField(Sequelize, key, opt); } @@ -12,7 +22,7 @@ function EncryptedField(Sequelize, key, opt) { self._iv_length = opt.iv_length || 16; self.encrypted_field_name = undefined; - let extraDecryptionKeys = []; + let extraDecryptionKeys: Key[] = []; if (opt.extraDecryptionKeys) { extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) ? opt.extraDecryptionKeys @@ -25,7 +35,7 @@ function EncryptedField(Sequelize, key, opt) { self.Sequelize = Sequelize; } -EncryptedField.prototype.vault = function(name) { +EncryptedField.prototype.vault = function(name: string) { const self = this; if (self.encrypted_field_name) { @@ -37,14 +47,14 @@ EncryptedField.prototype.vault = function(name) { return { type: self.Sequelize.BLOB, get: function() { - let previous = this.getDataValue(name); - if (!previous) { + let stored: string | null = this.getDataValue(name); + if (!stored) { return {}; } - previous = Buffer.from(previous); + const previous: Buffer = Buffer.from(stored); - function decrypt(key) { + function decrypt(key: Key) { const iv = previous.slice(0, self._iv_length); const content = previous.slice(self._iv_length, previous.length); const decipher = crypto.createDecipheriv(self._algorithm, key, iv); @@ -65,7 +75,7 @@ EncryptedField.prototype.vault = function(name) { } } }, - set: function(value) { + set: function(this: Instance, value: any) { // if new data is set, we will use a new IV const new_iv = crypto.randomBytes(self._iv_length); @@ -82,7 +92,7 @@ EncryptedField.prototype.vault = function(name) { }; }; -EncryptedField.prototype.field = function(name) { +EncryptedField.prototype.field = function(name: string) { const self = this; if (!self.encrypted_field_name) { diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..234bcc4 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + testEnvironment: 'node', + transform: { + '^.+\\.(ts)?$': 'ts-jest', + }, + testRegex: '(/test/.*|(\\.|/)(spec.jest))(? { }); test('should save an encrypted field', async () => { - const user = User.build(); + const user: any = User.build(); user.private_1 = 'test'; await user.save(); - const found = await User.findById(user.id); + const found: any = await User.findById(user.id); expect(found.private_1).toEqual(user.private_1); }); test('should support multiple encrypted fields', async () => { - const user = User.build(); + const user: any = User.build(); user.private_1 = 'baz'; user.private_2 = 'foobar'; await user.save(); @@ -48,7 +48,7 @@ test('should support multiple encrypted fields', async () => { private_1: vault.field('private_1'), }); - const found = await AnotherUser.findById(user.id); + const found: any = await AnotherUser.findById(user.id); expect(found.private_2).toEqual(user.private_2); // encrypted with key1 and different field originally @@ -65,13 +65,13 @@ test('should throw error on decryption using invalid key', async () => { private_1: badEncryptedField.field('private_1'), }); - const model = User.build(); + const model: any = User.build(); model.private_1 = 'secret!'; await model.save(); let threw; try { - const found = await BadEncryptionUser.findById(model.id); + const found: any = await BadEncryptionUser.findById(model.id); found.private_1; // trigger decryption } catch (error) { threw = error; @@ -98,15 +98,19 @@ test('should support extra decryption keys (to facilitate key rotation)', async await KeyOneModel.sync({ force: true }); - const modelUsingKeyOne = KeyOneModel.build(); - const modelUsingKeyTwo = KeyTwoAndOneModel.build(); + const modelUsingKeyOne: any = KeyOneModel.build(); + const modelUsingKeyTwo: any = 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); + const foundFromKeyOne: any = await KeyTwoAndOneModel.findById( + modelUsingKeyOne.id, + ); + const foundFromKeyTwo: any = await KeyTwoAndOneModel.findById( + modelUsingKeyTwo.id, + ); expect(foundFromKeyOne.private).toEqual(modelUsingKeyOne.private); expect(foundFromKeyTwo.private).toEqual(modelUsingKeyTwo.private); diff --git a/tsconfig.json b/tsconfig.json index 19d64f1..725acab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,17 +3,13 @@ "outDir": "./dist", "target": "es2017", "module": "commonjs", + "declaration": true, "sourceMap": true, "pretty": true, - "importHelpers": true, - "strict": true, - "noImplicitAny": true, - "noUnusedLocals": true, - "allowJs": true, - "noImplicitReturns": true + "importHelpers": true + }, "include": [ - "./*", - "./test/*" + "./*" ] } From b01bb2a8c70e9ed246a4428c680274257e0125f0 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 14:38:19 +0100 Subject: [PATCH 10/14] chore: takeown --- .travis.yml | 11 +++++++++++ README.md | 2 ++ package.json | 19 ++++++++++++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index d65523e..a6a28b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,21 @@ dist: bionic sudo: false language: node_js +cache: npm node_js: - "12" +install: + - npm ci + +script: + - npm run build + - npm run lint + +after_success: + - npm run semantic-release + addons: hosts: - db diff --git a/README.md b/README.md index a41f34d..0b5a042 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,5 @@ To achieve a zero-downtime rotation from `oldKey` to `newKey`: ## License MIT + +Originally based on work by [defunctzombie](https://github.com/defunctzombie). diff --git a/package.json b/package.json index c24e139..c8346fb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sequelize-encrypted", + "name": "@snyk/sequelize-encrypted", "version": "0.1.0", "description": "encrypted sequelize fields", "main": "dist/index.js", @@ -7,35 +7,40 @@ "build": "tsc", "format": "npx prettier --write '*.?s' 'test/*.?s'", "lint": "eslint '*.?s' 'test/*.?s'", - "test": "jest --detectOpenHandles --forceExit" + "test": "jest --detectOpenHandles --forceExit", + "semantic-release": "semantic-release" }, "repository": { "type": "git", - "url": "https://github.com/defunctzombie/sequelize-encrypted.git" + "url": "https://github.com/snyk/sequelize-encrypted.git" }, "keywords": [ "sequelize", "encrypted" ], - "author": "Roman Shtylman ", + "author": "snyk.io", "license": "MIT", "bugs": { - "url": "https://github.com/defunctzombie/sequelize-encrypted/issues" + "url": "https://github.com/snyk/sequelize-encrypted/issues" }, - "homepage": "https://github.com/defunctzombie/sequelize-encrypted", + "homepage": "https://github.com/snyk/sequelize-encrypted", "devDependencies": { "@types/jest": "^24.0.18", + "@types/node": "^10.14.20", "@types/sequelize": "^3.30.10", "@typescript-eslint/eslint-plugin": "^2.3.3", "@typescript-eslint/parser": "^2.2.0", - "@types/node": "^10.14.20", "eslint": "^6.5.1", "eslint-config-prettier": "^6.4.0", "eslint-plugin-jest": "^22.17.0", "jest": "^24.9.0", "pg": "^6.4.1", + "semantic-release": "^15.13.24", "sequelize": "^3.34.0", "ts-jest": "^24.1.0", "typescript": "^3.6.3" + }, + "publishConfig": { + "access": "restricted" } } From 9e673242230d6fa0af1c927263263174834b579b Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Tue, 8 Oct 2019 15:44:41 +0100 Subject: [PATCH 11/14] chore: it's a class --- index.ts | 206 +++++++++++++++++++++++---------------------- package.json | 3 +- test/smoke.test.ts | 14 +-- tsconfig.json | 7 +- 4 files changed, 120 insertions(+), 110 deletions(-) diff --git a/index.ts b/index.ts index 1a1518c..c431cb9 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,9 @@ import * as crypto from 'crypto'; -import { Instance, Sequelize } from 'sequelize'; +import { + DataTypeAbstract, + DefineAttributeColumnOptions, + Instance, +} from 'sequelize'; type Key = string; @@ -9,113 +13,115 @@ export interface FieldOptions { extraDecryptionKeys?: Key | Key[]; } -function EncryptedField(Sequelize: Sequelize, key: Key, opt?: FieldOptions) { - if (!(this instanceof EncryptedField)) { - // @ts-ignore - return new EncryptedField(Sequelize, key, opt); +export interface SequelizeConstants { + BLOB: DataTypeAbstract; + VIRTUAL: DataTypeAbstract; +} + +export class EncryptedField { + _algorithm: string; + _iv_length: number; + encrypted_field_name: string | undefined = undefined; + encryptionKey: Buffer; + decryptionKeys: Buffer[]; + Sequelize: SequelizeConstants; + + constructor(Sequelize: SequelizeConstants, key: Key, opt?: FieldOptions) { + opt = opt || {}; + this._algorithm = opt.algorithm || 'aes-256-cbc'; + this._iv_length = opt.iv_length || 16; + + let extraDecryptionKeys: Key[] = []; + if (opt.extraDecryptionKeys) { + extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) + ? opt.extraDecryptionKeys + : Array(opt.extraDecryptionKeys); + } + this.decryptionKeys = [key].concat(extraDecryptionKeys).map(function(key) { + return Buffer.from(key, 'hex'); + }); + this.encryptionKey = this.decryptionKeys[0]; + this.Sequelize = Sequelize; } - const self = this; + vault(name: string): DefineAttributeColumnOptions { + if (this.encrypted_field_name) { + throw new Error('vault already initialized'); + } - opt = opt || {}; - self._algorithm = opt.algorithm || 'aes-256-cbc'; - self._iv_length = opt.iv_length || 16; - self.encrypted_field_name = undefined; + this.encrypted_field_name = name; - let extraDecryptionKeys: Key[] = []; - if (opt.extraDecryptionKeys) { - extraDecryptionKeys = Array.isArray(opt.extraDecryptionKeys) - ? opt.extraDecryptionKeys - : Array(opt.extraDecryptionKeys); - } - self.decryptionKeys = [key].concat(extraDecryptionKeys).map(function(key) { - return Buffer.from(key, 'hex'); - }); - self.encryptionKey = self.decryptionKeys[0]; - self.Sequelize = Sequelize; -} + const self = this; -EncryptedField.prototype.vault = function(name: string) { - const self = this; + return { + type: self.Sequelize.BLOB, + get: function(this: Instance) { + const stored: string | null = this.getDataValue(name); + if (!stored) { + return {}; + } - if (self.encrypted_field_name) { - throw new Error('vault already initialized'); - } + const previous: Buffer = Buffer.from(stored); - self.encrypted_field_name = name; - - return { - type: self.Sequelize.BLOB, - get: function() { - let stored: string | null = this.getDataValue(name); - if (!stored) { - return {}; - } - - const previous: Buffer = Buffer.from(stored); - - function decrypt(key: Key) { - const iv = previous.slice(0, self._iv_length); - const content = previous.slice(self._iv_length, previous.length); - const decipher = crypto.createDecipheriv(self._algorithm, key, iv); - - const json = - decipher.update(content, undefined, 'utf8') + decipher.final('utf8'); - return JSON.parse(json); - } - - const keyCount = self.decryptionKeys.length; - for (let i = 0; i < keyCount; i++) { - try { - return decrypt(self.decryptionKeys[i]); - } catch (error) { - if (i >= keyCount - 1) { - throw error; - } - } - } - }, - set: function(this: Instance, value: any) { - // if new data is set, we will use a new IV - const new_iv = crypto.randomBytes(self._iv_length); - - const cipher = crypto.createCipheriv( - self._algorithm, - self.encryptionKey, - new_iv, - ); + function decrypt(key: Buffer) { + const iv = previous.slice(0, self._iv_length); + const content = previous.slice(self._iv_length, previous.length); + const decipher = crypto.createDecipheriv(self._algorithm, key, iv); - cipher.end(JSON.stringify(value), 'utf-8'); - const enc_final = Buffer.concat([new_iv, cipher.read()]); - this.setDataValue(name, enc_final); - }, - }; -}; + const json = + decipher.update(content, undefined, 'utf8') + + decipher.final('utf8'); + return JSON.parse(json); + } -EncryptedField.prototype.field = function(name: string) { - const self = this; + const keyCount = self.decryptionKeys.length; + for (let i = 0; i < keyCount; i++) { + try { + return decrypt(self.decryptionKeys[i]); + } catch (error) { + if (i >= keyCount - 1) { + throw error; + } + } + } + }, + set: function(this: Instance, value: any) { + // if new data is set, we will use a new IV + const new_iv = crypto.randomBytes(self._iv_length); + + const cipher = crypto.createCipheriv( + self._algorithm, + self.encryptionKey, + new_iv, + ); + + cipher.end(JSON.stringify(value), 'utf-8'); + const enc_final = Buffer.concat([new_iv, cipher.read()]); + this.setDataValue(name, enc_final); + }, + }; + } - if (!self.encrypted_field_name) { - throw new Error( - 'you must initialize the vault field before using encrypted fields', - ); + field(name: string): DefineAttributeColumnOptions { + if (!this.encrypted_field_name) { + throw new Error( + 'you must initialize the vault field before using encrypted fields', + ); + } + + const encrypted_field_name = this.encrypted_field_name; + + return { + type: this.Sequelize.VIRTUAL, + set: function(this: any, val) { + // the proxying breaks if you don't use this local + const encrypted = this[encrypted_field_name]; + encrypted[name] = val; + this[encrypted_field_name] = encrypted; + }, + get: function(this: any) { + return this[encrypted_field_name][name]; + }, + }; } - const 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 - const encrypted = this[encrypted_field_name]; - encrypted[name] = val; - this[encrypted_field_name] = encrypted; - }, - get: function get_encrypted() { - const encrypted = this[encrypted_field_name]; - return encrypted[name]; - }, - }; -}; - -module.exports = EncryptedField; +} diff --git a/package.json b/package.json index c8346fb..8298b9b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "format": "npx prettier --write '*.?s' 'test/*.?s'", "lint": "eslint '*.?s' 'test/*.?s'", "test": "jest --detectOpenHandles --forceExit", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "prepare": "npm run build" }, "repository": { "type": "git", diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 9e38eac..e9c162e 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,5 +1,5 @@ import * as Sequelize from 'sequelize'; -const EncryptedField = require('../'); +import { EncryptedField } from '../index'; const dbHost = process.env.DB_HOST || 'db'; const sequelize = new Sequelize(`postgres://postgres@${dbHost}:5432/postgres`); @@ -7,8 +7,8 @@ const sequelize = new Sequelize(`postgres://postgres@${dbHost}:5432/postgres`); const key1 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230e'; const key2 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230f'; -const v1 = EncryptedField(Sequelize, key1); -const v2 = EncryptedField(Sequelize, key2); +const v1 = new EncryptedField(Sequelize, key1); +const v2 = new EncryptedField(Sequelize, key2); const User = sequelize.define('user', { name: Sequelize.STRING, @@ -39,7 +39,7 @@ test('should support multiple encrypted fields', async () => { user.private_2 = 'foobar'; await user.save(); - const vault = EncryptedField(Sequelize, key2); + const vault = new EncryptedField(Sequelize, key2); const AnotherUser = sequelize.define('user', { name: Sequelize.STRING, @@ -58,7 +58,7 @@ test('should support multiple encrypted fields', async () => { test('should throw error on decryption using invalid key', async () => { // attempt to use key2 for vault encrypted with key1 - const badEncryptedField = EncryptedField(Sequelize, key2); + const badEncryptedField = new EncryptedField(Sequelize, key2); const BadEncryptionUser = sequelize.define('user', { name: Sequelize.STRING, encrypted: badEncryptedField.vault('encrypted'), @@ -81,8 +81,8 @@ test('should throw error on decryption using invalid key', async () => { }); test('should support extra decryption keys (to facilitate key rotation)', async () => { - const keyOneEncryptedField = EncryptedField(Sequelize, key1); - const keyTwoAndOneEncryptedField = EncryptedField(Sequelize, key2, { + const keyOneEncryptedField = new EncryptedField(Sequelize, key1); + const keyTwoAndOneEncryptedField = new EncryptedField(Sequelize, key2, { extraDecryptionKeys: [key1], }); diff --git a/tsconfig.json b/tsconfig.json index 725acab..5920d6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,11 @@ "declaration": true, "sourceMap": true, "pretty": true, - "importHelpers": true - + "importHelpers": true, + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noImplicitReturns": true }, "include": [ "./*" From a923e517ab802893eaa9cb490e00116563d7df27 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Wed, 9 Oct 2019 19:28:37 +0100 Subject: [PATCH 12/14] chore: the exit bug, quashed --- package.json | 2 +- test/smoke.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8298b9b..61ab82b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "tsc", "format": "npx prettier --write '*.?s' 'test/*.?s'", "lint": "eslint '*.?s' 'test/*.?s'", - "test": "jest --detectOpenHandles --forceExit", + "test": "jest --detectOpenHandles", "semantic-release": "semantic-release", "prepare": "npm run build" }, diff --git a/test/smoke.test.ts b/test/smoke.test.ts index e9c162e..81bdb93 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -24,6 +24,10 @@ beforeAll(async () => { await User.sync({ force: true }); }); +afterAll(async () => { + sequelize.close(); +}); + test('should save an encrypted field', async () => { const user: any = User.build(); user.private_1 = 'test'; From a8dca75de9d44a12171340e667c058f7f65211c1 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Sun, 19 Apr 2020 17:06:54 +0100 Subject: [PATCH 13/14] chore: bumps --- package.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 61ab82b..544f9ef 100644 --- a/package.json +++ b/package.json @@ -26,20 +26,20 @@ }, "homepage": "https://github.com/snyk/sequelize-encrypted", "devDependencies": { - "@types/jest": "^24.0.18", - "@types/node": "^10.14.20", - "@types/sequelize": "^3.30.10", - "@typescript-eslint/eslint-plugin": "^2.3.3", - "@typescript-eslint/parser": "^2.2.0", - "eslint": "^6.5.1", - "eslint-config-prettier": "^6.4.0", - "eslint-plugin-jest": "^22.17.0", - "jest": "^24.9.0", - "pg": "^6.4.1", - "semantic-release": "^15.13.24", - "sequelize": "^3.34.0", - "ts-jest": "^24.1.0", - "typescript": "^3.6.3" + "@types/jest": "^25.2.1", + "@types/node": "^12.12.36", + "@types/sequelize": "^3.30.13", + "@typescript-eslint/eslint-plugin": "^2.28.0", + "@typescript-eslint/parser": "^2.28.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.1", + "eslint-plugin-jest": "^23.8.2", + "jest": "^25.3.0", + "pg": "^6.4.2", + "semantic-release": "^17.0.6", + "sequelize": "^3.35.1", + "ts-jest": "^25.4.0", + "typescript": "^3.8.3" }, "publishConfig": { "access": "restricted" From c2b2813b92ba174a0c01ae594b0d9e5f53469d9d Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Sun, 19 Apr 2020 17:07:58 +0100 Subject: [PATCH 14/14] chore: new prettier --- index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index c431cb9..1c5de73 100644 --- a/index.ts +++ b/index.ts @@ -37,7 +37,7 @@ export class EncryptedField { ? opt.extraDecryptionKeys : Array(opt.extraDecryptionKeys); } - this.decryptionKeys = [key].concat(extraDecryptionKeys).map(function(key) { + this.decryptionKeys = [key].concat(extraDecryptionKeys).map(function (key) { return Buffer.from(key, 'hex'); }); this.encryptionKey = this.decryptionKeys[0]; @@ -55,7 +55,7 @@ export class EncryptedField { return { type: self.Sequelize.BLOB, - get: function(this: Instance) { + get: function (this: Instance) { const stored: string | null = this.getDataValue(name); if (!stored) { return {}; @@ -85,7 +85,7 @@ export class EncryptedField { } } }, - set: function(this: Instance, value: any) { + set: function (this: Instance, value: any) { // if new data is set, we will use a new IV const new_iv = crypto.randomBytes(self._iv_length); @@ -113,13 +113,13 @@ export class EncryptedField { return { type: this.Sequelize.VIRTUAL, - set: function(this: any, val) { + set: function (this: any, val) { // the proxying breaks if you don't use this local const encrypted = this[encrypted_field_name]; encrypted[name] = val; this[encrypted_field_name] = encrypted; }, - get: function(this: any) { + get: function (this: any) { return this[encrypted_field_name][name]; }, };