├── .babelrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── Dockerfile ├── HISTORY.md ├── LICENSE ├── README.md ├── docker-compose.yaml ├── index.js ├── package.json └── test ├── index.js └── mocha.opts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-async-to-generator", 4 | "transform-es2015-modules-commonjs", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "4" 6 | 7 | addons: 8 | hosts: 9 | - db 10 | 11 | services: 12 | - postgres 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:6.2.2 2 | 3 | WORKDIR /app 4 | ADD package.json /app/ 5 | RUN npm install 6 | 7 | ADD . /app/ 8 | 9 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2016-11-13) 2 | 3 | * add support for validations, allowNull, and defaultValues 4 | * add option to specify encrypted field name 5 | 6 | # 0.1.0 (2014-11-23) 7 | 8 | * initial 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Roman Shtylman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-encrypted 2 | 3 | Encrypted fields for Sequelize ORM 4 | 5 | ```js 6 | var Sequelize = require('sequelize'); 7 | var EncryptedField = require('sequelize-encrypted'); 8 | 9 | // secret key should be 32 bytes hex encoded (64 characters) 10 | var key = process.env.SECRET_KEY_HERE; 11 | 12 | var enc_fields = EncryptedField(Sequelize, key); 13 | 14 | var User = sequelize.define('user', { 15 | name: Sequelize.STRING, 16 | encrypted: enc_fields.vault('encrypted'), 17 | 18 | // encrypted virtual fields 19 | private_1: enc_fields.field('private_1'), 20 | 21 | // Optional second argument allows you 22 | // to pass in a validation configuration 23 | // as well as an optional return type 24 | private_2: enc_fields.field('private_2', { 25 | type: Sequelize.TEXT, 26 | validate: { 27 | isInt: true 28 | }, 29 | defaultValue: null 30 | }) 31 | }) 32 | 33 | var user = User.build(); 34 | user.private_1 = 'test'; 35 | ``` 36 | 37 | ## How it works 38 | 39 | The `safe` returns a sequelize BLOB field configured with getters/setters for decrypting and encrypting data. Encrypted JSON encodes the value you set and then encrypts this value before storing in the database. 40 | 41 | Additionally, there are `.field` methods which return sequelize VIRTUAL fields that provide access to specific fields in the encrypted vault. It is recommended that these are used to get/set values versus using the encrypted field directly. 42 | 43 | When calling `.vault` or `.field` you must specify the field name. This cannot be auto-detected by the module. 44 | 45 | ## Generating a key 46 | 47 | By default, AES-SHA256-CBC is used to encrypt data. You should generate a random key that is 32 bytes. 48 | 49 | ``` 50 | openssl rand -hex 32 51 | ``` 52 | 53 | Do not save this key with the source code, ideally you should use an environment variable or other configuration injection to provide the key during app startup. 54 | 55 | ## Tips 56 | 57 | You might find it useful to override the default `toJSON` implementation for your model to omit the encrypted field or other sensitive fields. 58 | 59 | ## License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: postgres:9.5.3 5 | 6 | app: 7 | build: . 8 | links: 9 | - db 10 | command: sleep infinity 11 | volumes: 12 | - .:/app 13 | - /app/node_modules 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | function EncryptedField(Sequelize, key, opt) { 4 | if (!(this instanceof EncryptedField)) { 5 | return new EncryptedField(Sequelize, key, opt); 6 | } 7 | 8 | var self = this; 9 | self.key = new Buffer(key, 'hex'); 10 | self.Sequelize = Sequelize; 11 | 12 | opt = opt || {}; 13 | self._algorithm = opt.algorithm || 'aes-256-cbc'; 14 | self._iv_length = opt.iv_length || 16; 15 | self.encrypted_field_name = undefined; 16 | }; 17 | 18 | EncryptedField.prototype.vault = function(name) { 19 | var self = this; 20 | 21 | if (self.encrypted_field_name) { 22 | throw new Error('vault already initialized'); 23 | } 24 | 25 | self.encrypted_field_name = name; 26 | 27 | return { 28 | type: self.Sequelize.BLOB, 29 | get: function() { 30 | var previous = this.getDataValue(name); 31 | if (!previous) { 32 | return {}; 33 | } 34 | 35 | previous = new Buffer(previous); 36 | 37 | var iv = previous.slice(0, self._iv_length); 38 | var content = previous.slice(self._iv_length, previous.length); 39 | var decipher = crypto.createDecipheriv(self._algorithm, self.key, iv); 40 | 41 | var json = decipher.update(content, undefined, 'utf8') + decipher.final('utf8'); 42 | return JSON.parse(json); 43 | }, 44 | set: function(value) { 45 | // if new data is set, we will use a new IV 46 | var new_iv = crypto.randomBytes(self._iv_length); 47 | 48 | var cipher = crypto.createCipheriv(self._algorithm, self.key, new_iv); 49 | 50 | cipher.end(JSON.stringify(value), 'utf-8'); 51 | var enc_final = Buffer.concat([new_iv, cipher.read()]); 52 | var previous = this.setDataValue(name, enc_final); 53 | } 54 | } 55 | }; 56 | 57 | EncryptedField.prototype.field = function(name, config) { 58 | var self = this; 59 | config = config || {}; 60 | config.validate = config.validate || {}; 61 | 62 | var hasValidations = [undefined,null].indexOf(typeof config.validate) === -1; 63 | 64 | if (!self.encrypted_field_name) { 65 | throw new Error('you must initialize the vault field before using encrypted fields'); 66 | } 67 | 68 | var encrypted_field_name = self.encrypted_field_name; 69 | 70 | return { 71 | type: self.Sequelize.VIRTUAL(config.type || null), 72 | set: function set_encrypted(val) { 73 | 74 | if (hasValidations) { 75 | this.setDataValue(name, val); 76 | } 77 | 78 | // use `this` not self because we need to reference the sequelize instance 79 | // not our EncryptedField instance 80 | var encrypted = this[encrypted_field_name]; 81 | encrypted[name] = val; 82 | this[encrypted_field_name] = encrypted; 83 | }, 84 | get: function get_encrypted() { 85 | var encrypted = this[encrypted_field_name]; 86 | var val = encrypted[name]; 87 | return ([undefined, null].indexOf(val) === -1) ? val : config.defaultValue; 88 | }, 89 | allowNull: ([undefined, null].indexOf(config.allowNull) === -1) ? config.allowNull : true, 90 | validate: config.validate 91 | } 92 | }; 93 | 94 | module.exports = EncryptedField; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-encrypted", 3 | "version": "1.0.0", 4 | "description": "encrypted sequelize fields", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/defunctzombie/sequelize-encrypted.git" 12 | }, 13 | "keywords": [ 14 | "sequelize", 15 | "encrypted" 16 | ], 17 | "author": "Roman Shtylman ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/defunctzombie/sequelize-encrypted/issues" 21 | }, 22 | "homepage": "https://github.com/defunctzombie/sequelize-encrypted", 23 | "devDependencies": { 24 | "babel-plugin-transform-async-to-generator": "6.8.0", 25 | "babel-plugin-transform-es2015-modules-commonjs": "6.10.3", 26 | "babel-polyfill": "6.9.1", 27 | "babel-preset-es2015": "6.9.0", 28 | "babel-register": "6.9.0", 29 | "mocha": "2.0.1", 30 | "pg": "6.0.1", 31 | "sequelize": "3.23.4" 32 | } 33 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Sequelize from 'sequelize'; 3 | import EncryptedField from '../'; 4 | 5 | const sequelize = new Sequelize('postgres://postgres@db:5432/postgres'); 6 | 7 | const key1 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230e'; 8 | const key2 = 'a593e7f567d01031d153b5af6d9a25766b95926cff91c6be3438c7f7ac37230f'; 9 | 10 | const v1 = EncryptedField(Sequelize, key1); 11 | const v2 = EncryptedField(Sequelize, key2); 12 | 13 | describe('sequelize-encrypted', () => { 14 | 15 | const User = sequelize.define('user', { 16 | name: Sequelize.STRING, 17 | encrypted: v1.vault('encrypted'), 18 | another_encrypted: v2.vault('another_encrypted'), 19 | 20 | // encrypted virtual fields 21 | private_1: v1.field('private_1'), 22 | private_2: v2.field('private_2'), 23 | }); 24 | 25 | before('create models', async () => { 26 | await User.sync({force: true}); 27 | }); 28 | 29 | it('should save an encrypted field', async () => { 30 | const user = User.build(); 31 | user.private_1 = 'test'; 32 | 33 | await user.save(); 34 | const found = await User.findById(user.id); 35 | assert.equal(found.private_1, user.private_1); 36 | }); 37 | 38 | it('should support multiple encrypted fields', async() => { 39 | const user = User.build(); 40 | user.private_1 = 'baz'; 41 | user.private_2 = 'foobar'; 42 | await user.save(); 43 | 44 | const vault = EncryptedField(Sequelize, key2); 45 | 46 | const AnotherUser = sequelize.define('user', { 47 | name: Sequelize.STRING, 48 | another_encrypted: vault.vault('another_encrypted'), 49 | private_2: vault.field('private_2'), 50 | private_1: vault.field('private_1'), 51 | }); 52 | 53 | const found = await AnotherUser.findById(user.id); 54 | assert.equal(found.private_2, user.private_2); 55 | 56 | // encrypted with key1 and different field originally 57 | // and thus can't be recovered with key2 58 | assert.equal(found.private_1, undefined); 59 | }); 60 | 61 | it('should support validation', async() => { 62 | const vault = EncryptedField(Sequelize, key2); 63 | const ValidUser = sequelize.define('validUser', { 64 | name: Sequelize.STRING, 65 | encrypted: vault.vault('encrypted'), 66 | 67 | // encrypted virtual fields 68 | private_1: vault.field('private_1', { 69 | type: Sequelize.INTEGER, 70 | validate: { 71 | notEmpty: true 72 | } 73 | }) 74 | }); 75 | const user = ValidUser.build(); 76 | user.private_1 = ''; 77 | 78 | const res = await user.validate(); 79 | assert.equal(res.message, 'Validation error: Validation notEmpty failed'); 80 | }); 81 | 82 | it('should support defaultValue', async() => { 83 | const vault = EncryptedField(Sequelize, key2); 84 | const ValidUser = sequelize.define('validUser', { 85 | name: Sequelize.STRING, 86 | encrypted: vault.vault('encrypted'), 87 | 88 | // encrypted virtual fields 89 | private_1: vault.field('private_1', { 90 | defaultValue: 'hello' 91 | }) 92 | }); 93 | const user = ValidUser.build(); 94 | assert.equal(user.private_1, 'hello'); 95 | }); 96 | 97 | it('should support allowNull', async() => { 98 | const vault = EncryptedField(Sequelize, key2); 99 | const ValidUser = sequelize.define('validUser', { 100 | name: Sequelize.STRING, 101 | encrypted: vault.vault('encrypted'), 102 | 103 | // encrypted virtual fields 104 | private_1: vault.field('private_1', { 105 | allowNull: false 106 | }) 107 | }); 108 | const user = ValidUser.build(); 109 | const res = await user.validate(); 110 | assert.equal(res.message,'notNull Violation: private_1 cannot be null'); 111 | }); 112 | 113 | }); 114 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --check-leaks 3 | --bail 4 | --compilers js:babel-register 5 | --require babel-polyfill 6 | --------------------------------------------------------------------------------