├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── development ├── docker-compose-dev.yml └── init-mongo-dev.js ├── lib ├── mongoose-field-encryption.d.ts └── mongoose-field-encryption.js ├── package-lock.json ├── package.json ├── test ├── setup.js ├── test-basic-usage.js ├── test-db.js ├── test-encryption-options.js ├── test-manual-encryption.js ├── test-setup.js └── test-statics.js └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci-test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | #services: 13 | # mongodb: 14 | # image: mongo:3.4.23 15 | # ports: 16 | # - 27017:27017 17 | 18 | strategy: 19 | matrix: 20 | #node-version: [12.x] 21 | node-version: [14.x,16.x,18.x] 22 | mongodb-version: [4.4, 5.0, 6.0] 23 | 24 | steps: 25 | - name: Git checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: Start MongoDB 34 | uses: supercharge/mongodb-github-action@1.3.0 35 | with: 36 | mongodb-version: ${{ matrix.mongodb-version }} 37 | 38 | - run: npm install 39 | 40 | - run: npm test 41 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 6 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v2 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v2 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage 12 | .coveralls.yml 13 | lib-cov 14 | coverage 15 | .nyc_output 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional cache 28 | .npm 29 | .node_repl_history 30 | 31 | # IDE 32 | .vscode/ 33 | 34 | # development 35 | development/docker-compose.yml 36 | development/init-mongo.js 37 | 38 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wheresvic/mongoose-field-encryption/e4480794f573587e8c6581addbe16bb7ae61dee1/.npmignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | See README for changelog 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | All contributions are very welcome. 4 | 5 | ## Code status 6 | 7 | Note that all code is automatically formatted using `prettier` regularly so you don't need to worry about it :) 8 | 9 | Also see [Tabs vs spaces]( http://wheresvic.gitlab.io/software-dawg/2018-08-01-the-holy-wars-tabs-vs-spaces-et-al.html) for an interesting read. 10 | 11 | ## Testing 12 | 13 | If you are unsure as to how to test your changes or what sort of test to write, please get in touch and we will definitely help you. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Victor Parmar 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongoose-field-encryption 2 | 3 | ![Build Status](https://github.com/wheresvic/mongoose-field-encryption/workflows/ci-test/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/wheresvic/mongoose-field-encryption/badge.svg?branch=master)](https://coveralls.io/github/wheresvic/mongoose-field-encryption?branch=master) 4 | 5 | A zero dependency simple symmetric encryption plugin for individual fields. The goal of this plugin is to encrypt data but still allow searching over fields with string values. This plugin relies on the Node `crypto` module. Encryption and decryption happen transparently during save and find. 6 | 7 | While this plugin works on individual fields of any type, note that for non-string fields, the original value is set to undefined after encryption. This is because if the schema has defined a field as an array, it would not be possible to replace it with a string value. 8 | 9 | As of the stable 2.3.0 release, this plugin requires provision of a custom salt generation function (which would always provide a constant salt given the secret) in order to retain symmetric decryption capability. 10 | 11 | Also consider [mongoose-encryption](https://github.com/joegoldbeck/mongoose-encryption) if you are looking to encrypt the entire document. 12 | 13 | ## How it works 14 | 15 | Encryption is performed using `AES-256-CBC`. To encrypt, the relevant fields are encrypted with the provided secret + random salt (or a custom salt via the provided `saltGenerator` function). The generated salt and the resulting encrypted value is concatenated together using a `:` character and the final string is put in place of the actual value for `string` values. An extra `boolean` field with the prefix `__enc_` is added to the document which indicates if the provided field is encrypted or not. 16 | 17 | Fields which are either objects or of a different type are converted to strings using `JSON.stringify` and the value stored in an extra marker field of type `string` with a naming scheme of `__enc_` as prefix and `_d` as suffix on the original field name. The original field is then set to `undefined`. Please note that this might break any custom validation and application of this plugin on non-string fields needs to be done with care. 18 | 19 | ## Requirements 20 | 21 | - Node `>=14` (Use `2.3.4` for Node `>=4.4.7 && <6.x.x`, `5.0.3` for Node `>=6.x.x && <=10.x.x`, `6.3.0` for Node `12.x.x`) 22 | - MongoDB `>=4.4` 23 | - Mongoose `>=7.0.0` (use `6.3.0` for Mongoose `<7.0.0`) 24 | 25 | ## Installation 26 | 27 | `npm install mongoose-field-encryption --save-exact` 28 | 29 | ## Security Notes 30 | 31 | - _Always store your keys and secrets outside of version control and separate from your database._ An environment variable on your application server works well for this. 32 | - Additionally, store your encryption key offline somewhere safe. If you lose it, there is no way to retrieve your encrypted data. 33 | - Encrypting passwords is no substitute for appropriately hashing them. `bcrypt` is one great option. You can also encrypt the password afer hashing it although it is not necessary. 34 | - If an attacker gains access to your application server, they likely have access to both the database and the key. At that point, neither encryption nor authentication do you any good. 35 | 36 | ## Usage 37 | 38 | ### Basic 39 | 40 | For example, given a schema as follows: 41 | 42 | ```js 43 | const mongoose = require("mongoose"); 44 | const mongooseFieldEncryption = require("mongoose-field-encryption").fieldEncryption; 45 | const Schema = mongoose.Schema; 46 | 47 | const PostSchema = new Schema({ 48 | title: String, 49 | message: String, 50 | references: { 51 | author: String, 52 | date: Date, 53 | }, 54 | }); 55 | 56 | PostSchema.plugin(mongooseFieldEncryption, { 57 | fields: ["message", "references"], 58 | secret: "some secret key", 59 | saltGenerator: function (secret) { 60 | return "1234567890123456"; 61 | // should ideally use the secret to return a string of length 16, 62 | // default = `const defaultSaltGenerator = secret => crypto.randomBytes(16);`, 63 | // see options for more details 64 | }, 65 | }); 66 | 67 | const Post = mongoose.model("Post", PostSchema); 68 | 69 | const post = new Post({ title: "some text", message: "hello all" }); 70 | 71 | post.save(function (err) { 72 | console.log(post.title); // some text (only the message field was set to be encrypted via options) 73 | console.log(post.message); // a9ad74603a91a2e97a803a367ab4e04d:93c64bf4c279d282deeaf738fabebe89 74 | console.log(post.__enc_message); // true 75 | }); 76 | ``` 77 | 78 | The resulting documents will have the following format: 79 | 80 | ```js 81 | { 82 | _id: ObjectId, 83 | title: String, 84 | message: String, // encrypted salt and hex value as string, e.g. 9d6a0ca4ac2c80fc84df0a06de36b548:cee57185fed78c055ed31ca6a8be9bf20d303283200a280d0f4fc8a92902e0c1 85 | __enc_message: true, // boolean marking if the field is encrypted or not 86 | references: undefined, // encrypted object set to undefined 87 | __enc_references: true, // boolean marking if the field is encrypted or not 88 | __enc_references_d: String // encrypted salt and hex object value as string, e.g. 6df2171f25fd1d32adc4a4059f867a82:5909152856cf9cdb7dc32c6af321c8fe69390c359c6b19d967eaa6e7a0a97216 89 | } 90 | ``` 91 | 92 | `find` works transparently and you can make new documents as normal, but you should not use the `lean` option on a find if you want the fields of the document to be decrypted. `findOne`, `findById` and `save` also all work as normal. `updateOne` works _only for string fields_ and you would also need to manually set the `__enc_` field value to false if you're updating an encrypted field. Currently `updateMany` is not supported. 93 | 94 | From the mongoose package documentation: _Note that findAndUpdate/Remove do not execute any hooks or validation before making the change in the database. If you need hooks and validation, first query for the document and then save it._ 95 | 96 | Note that as of `1.2.0` release, support for `findOneAndUpdate` has also been added. Note that you would need to specifically set the encryption field marker for it to be encrypted. For example: 97 | 98 | ```js 99 | Post.findOneAndUpdate({ _id: postId }, { $set: { message: "snoop", __enc_message: false } }); 100 | ``` 101 | 102 | The above also works for non-string fields. See changelog for more details. 103 | 104 | Also note that if you manually set the value `__enc_` prefix field to true then the encryption is not run on the corresponding field and this may result in the plain value being stored in the db. 105 | 106 | ### Search over encrypted fields 107 | 108 | Note that in order to use this option a _fixed_ salt generator must be provided. See example as follows: 109 | 110 | ```js 111 | const messageSchema = new Schema({ 112 | title: String, 113 | message: String, 114 | name: String, 115 | }); 116 | 117 | messageSchema.plugin(mongooseFieldEncryption, { 118 | fields: ["message", "name"], 119 | secret: "some secret key", 120 | saltGenerator: function (secret) { 121 | return "1234567890123456"; 122 | // should ideally use the secret to return a string of length 16, 123 | // default = `const defaultSaltGenerator = secret => crypto.randomBytes(16);`, 124 | // see options for more details 125 | }, 126 | }); 127 | 128 | const title = "some text"; 129 | const name = "victor"; 130 | const message = "hello all"; 131 | 132 | const Message = mongoose.model("Message", messageSchema); 133 | 134 | const messageToSave = new Message({ title, message, name }); 135 | await messageToSave.save(); 136 | 137 | // note that we are only providing the field we would like to search with 138 | const messageToSearchWith = new Message({ name }); 139 | messageToSearchWith.encryptFieldsSync(); 140 | 141 | // `messageToSearchWith.name` contains the encrypted string text 142 | const results = await Message.find({ name: messageToSearchWith.name }); 143 | 144 | // results is an array of length 1 (assuming that there is only 1 message with the name "victor" in the collection) 145 | // and the message in the results array corresponds to the one saved previously 146 | ``` 147 | 148 | ### Options 149 | 150 | - `fields` (required): an array list of the required fields 151 | - `secret` (required): a string cipher (or a synchronous factory function which returns a string cipher) which is used to encrypt the data (don't lose this!) 152 | - `useAes256Ctr` (optional, default `false`): a boolean indicating whether the older `aes-256-ctr` algorithm should be used. Note that this is strictly a backwards compatibility feature and for new installations it is recommended to leave this at default. 153 | - `saltGenerator` (optional, default `const defaultSaltGenerator = secret => crypto.randomBytes(16);`): a function that should return either a `utf-8` encoded string that is 16 characters in length or a `Buffer` of length 16. This function is also passed the secret as shown in the default function example. 154 | - `encryptNull` (optional, default `true`): An option to enable or disable encryption of null values. 155 | - `notifyDecryptFails` (optional, default `true`): An option to enable or disable an exception on decryption failures. When disabled, exceptions will be inhibited, and an empty field will be returned. 156 | 157 | ### Static methods 158 | 159 | For performance reasons, once the document has been encrypted, it remains so. The following methods are thus added to the schema: 160 | 161 | - `encryptFieldsSync()`: synchronous call that encrypts all fields as given by the plugin options 162 | - `decryptFieldsSync()`: synchronous call that decrypts encrypted fields as given by the plugin options 163 | - `stripEncryptionFieldMarkers()`: synchronous call that removes the encryption field markers (useful for returning documents without letting the user know that something was encrypted) 164 | 165 | Multiple calls to the above methods have no effect, i.e. once a field is encrypted and the `__enc_` marker field value is set to true then the ecrypt operation is ignored. Same for the decrypt operation. Of course if the field markers have been removed via the `stripEncryptionFieldMarkers()` call, then the encryption will be executed if invoked. 166 | 167 | ### Searching 168 | 169 | To enable searching over the encrypted fields the `encrypt` and `decrypt` methods have also been exposed (see `test/test-manual-encryption` for detailed usage). 170 | 171 | ```js 172 | const fieldEncryption = require('mongoose-field-encryption'); 173 | 174 | const defaultSaltGenerator = (secret) => crypto.randomBytes(16); 175 | const _hash = (secret) => crypto.createHash("sha256").update(secret).digest("hex").substring(0, 32); 176 | const encrypted = fieldEncryption.encrypt('some text', _hash('secret')), defaultSaltGenerator); 177 | const decrypted = fieldEncryption.decrypt(encrypted, _hash('secret'))); // decrypted = 'some text' 178 | ``` 179 | 180 | ### encryption of nested fields 181 | 182 | Note that while this plugin is designed to encrypt only top level fields, nested fields can be easily encrypted by creating a mongoose schema for the nested objects and adding the plugin to them. 183 | 184 | See comment for discussion: [https://github.com/wheresvic/mongoose-field-encryption/issues/34#issuecomment-577383776](https://github.com/wheresvic/mongoose-field-encryption/issues/34#issuecomment-577383776). 185 | 186 | _Please also note that this example is provided as a best-effort basis and this plugin does not take responsibility for what quirks mongoose might bring if you use this feature._ 187 | 188 | See relevant test in `test/test-db.js`: 189 | 190 | ```js 191 | // subdocument encryption 192 | 193 | const UserExtraSchema = new mongoose.Schema({ 194 | city: { type: String }, 195 | country: { type: String }, 196 | address: { type: String }, 197 | postalCode: { type: String }, 198 | }); 199 | 200 | UserExtraSchema.plugin(fieldEncryptionPlugin, { 201 | fields: ["address"], 202 | secret: "icanhazcheeseburger", 203 | saltGenerator: (secret) => secret.slice(0, 16), 204 | }); 205 | 206 | const UserSchema = new mongoose.Schema( 207 | { 208 | name: { type: String, required: true }, 209 | surname: { type: String, required: true }, 210 | email: { type: String, required: true }, 211 | extra: UserExtraSchema, 212 | }, 213 | { collection: "users" } 214 | ); 215 | 216 | UserSchema.plugin(fieldEncryptionPlugin, { 217 | fields: ["name", "surname"], 218 | secret: "icanhazcheeseburger", 219 | saltGenerator: (secret) => secret.slice(0, 16), 220 | }); 221 | 222 | const UserModel = mongoose.model("User", UserSchema); 223 | ``` 224 | 225 | ## Development 226 | 227 | As of version 3.0.5, one can setup a local development mongodb instance using docker: 228 | 229 | - copy `development/docker-compose-dev.yml` to `development/docker-compose.yml` 230 | - copy `development/init-mongo-dev.js` to `development/init-mongo.js` 231 | - run `docker-compose up` in the `development` folder 232 | 233 | Feel free to make changes to the default docker configuration as required. 234 | 235 | ### Testing 236 | 237 | 1. Install dependencies with `npm install` and [install mongo](http://docs.mongodb.org/manual/installation/) if you don't have it yet. 238 | 2. Start mongo via `docker-compose up` under the `development` folder. 239 | 3. Run tests with `npm run test:auth`. Additionally you can pass your own mongodb uri as an environment variable if you would like to test against your own database, for e.g. `URI='mongodb://username:password@127.0.0.1:27017/mongoose-field-encryption-test' npm test` 240 | 241 | ### Publishing 242 | 243 | #### release-it 244 | 245 | `release-it patch,minor,major` 246 | 247 | #### Manual 248 | 249 | - `npm version patch,minor,major` 250 | - `npm publish` 251 | 252 | ## Changelog 253 | 254 | ### 7.0.1 255 | 256 | - Update README 257 | - No functionality affected 258 | 259 | ### 7.0.0 260 | 261 | - _BREAKING:_ Update mongoose peer dependency to 7.0.0 which drops `update` and replaces it with `updateOne`. Use previous version if you are still on monogoose `6.x`. 262 | - _BREAKING:_ Drop node 12 support. Mongoose 7.x requires Node 14. 263 | 264 | ### 6.3.0 265 | 266 | - _FEATURE_ Add an optional `encryptNull` configuration option to handle whether `null` values should be encrypted or not. Defaults to `true` to maintain backwards compatibility. 267 | 268 | ### 6.2.0 269 | 270 | - _FEATURE_ Add an optional `notifyDecryptFails` configuration option (default: `true`). 271 | - Update dev dependencies 272 | 273 | ### 6.1.0 274 | 275 | - _FEATURE_ Add support for `insertMany` [PR #94](https://github.com/wheresvic/mongoose-field-encryption/pull/94) 276 | - Update dev dependencies 277 | 278 | ### 6.0.0 279 | 280 | - _BREAKING:_ Drop Node 6, Node 8 & Node 10 support. Note that the library should still work for node 6, 8 and 10 however it is not actively tested and supported anymore. 281 | 282 | ### 5.0.1, 5.0.2, 5.0.3 283 | 284 | - Update README 285 | - Add test for manual encryption 286 | - No functionality affected 287 | - Update mongoose peer dependency 288 | 289 | ### 5.0.0 290 | 291 | - _BREAKING:_ support encrypting falsy values, e.g. an empty string field, a field with the `0` number value, a field with the `false` boolean value, etc. See relevant test: [test/test-db.js#L631](https://github.com/wheresvic/mongoose-field-encryption/blob/a1f543c11a43fd62426efefa84255d3f14a8fd6d/test/test-db.js#L631) 292 | 293 | Note that previously falsy values were not encrypted and stored as is. On document retrieval the plugin checks for the existence of encrypted data so this should in theory not break existing documents. Existing documents would need to be re-encrypted to take advantage of falsy encryption however. 294 | 295 | As always, please test thoroughly before upgrading. 296 | 297 | ### 4.0.4, 4.0.5, 4.0.6, 4.0.7 298 | 299 | - Update README 300 | - Update development dependencies 301 | - No functionality affected 302 | 303 | ### 4.0.3 304 | 305 | - Add typescript types 306 | - Update development dependencies 307 | 308 | ### 4.0.2 309 | 310 | - Update documentation for subdocument encryption 311 | 312 | ### 4.0.1 313 | 314 | - Update documentation to add nested field encryption example 315 | - Switch from Travis to Github actions 316 | 317 | ### 4.0.0 318 | 319 | - _FEATURE_: Add support for an optional synchronous secret function instead of a fixed string. Note that while this change should be backwards compatible, care should be taken as an issues with the secret could lead to irrecoverable documents! 320 | - Add support for `updateOne` ([https://mongoosejs.com/docs/api.html#query_Query-updateOne](https://mongoosejs.com/docs/api.html#query_Query-updateOne)). 321 | 322 | ### 3.1.0 323 | 324 | - Do not use 325 | 326 | ### 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.0.5, 3.0.6 327 | 328 | - Update development dependencies, fix unit tests, no functionality affected 329 | - Add development db via docker (3.0.5) 330 | 331 | ### 3.0.0 332 | 333 | - _BREAKING:_ Drop Node 4 support 334 | 335 | ### 2.3.5 336 | 337 | - Update development dependencies, no functionality affected 338 | 339 | ### 2.3.2, 2.3.3, 2.3.4 340 | 341 | - Update documentation, no functionality affected 342 | 343 | ### 2.3.1 344 | 345 | - Update documentation, no functionality affected 346 | 347 | ### 2.3.0 348 | 349 | - _FEATURE:_ Add provision for a custom salt generator, [PR #27](https://github.com/wheresvic/mongoose-field-encryption/pull/27). Note that by using a custom salt, _fixed_ search capability is now restored. 350 | 351 | ### 2.2.0 352 | 353 | - Update dependencies, no functionality affected 354 | 355 | ### 2.1.3 356 | 357 | - _FIX:_ Fix bug where decryption fails when the field in question is not retrieved, [PR #26](https://github.com/wheresvic/mongoose-field-encryption/pull/26). 358 | 359 | ### 2.1.1 360 | 361 | - _FIX:_ Fix bug where data was not getting decrypted on a `find()`, [#23](https://github.com/wheresvic/mongoose-field-encryption/issues/23). 362 | 363 | ### 2.0.0 364 | 365 | - _BREAKING:_ Use `cipheriv` instead of plain `cipher`, [#17](https://github.com/wheresvic/mongoose-field-encryption/issues/17). 366 | 367 | Note that this might break any _fixed_ search capability as the encrypted values are now based on a random salt. 368 | 369 | Also note that while this version maintains backward compatibility, i.e. decryption will automatically fall back to using the `aes-256-ctr` algorithm, any further updates will lead to the value being encrypted with the salt. In order to fully maintain backwards compatibilty, an new option `useAes256Ctr` has been introduced (default `false`), which can be set to `true` to continue using the plugin as before. It is highly recommended to start using the newer algorithm however, see issue for more details. 370 | 371 | ### 1.2.0 372 | 373 | - _FEATURE:_ Added support for `findOneAndUpdate` [https://github.com/wheresvic/mongoose-field-encryption/pull/20](https://github.com/wheresvic/mongoose-field-encryption/pull/20) 374 | 375 | ### 1.1.0 376 | 377 | - _FEATURE:_ Added support for mongoose 5 [https://github.com/wheresvic/mongoose-field-encryption/pull/16](https://github.com/wheresvic/mongoose-field-encryption/pull/16). 378 | - _FIX:_ Removed mongoose dependency, moved to `peerDependencies`. 379 | - Formatted source code using prettier. 380 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The currently supported versions are: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.0.x | :white_check_mark: | 10 | | < 3.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report any security issues via email to vic@smalldata.tech. Thank you! 15 | -------------------------------------------------------------------------------- /development/docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: mongo 5 | container_name: "mongoose-field-encryption-db" 6 | restart: unless-stopped 7 | environment: 8 | MONGO_INITDB_DATABASE: "mongoose-field-encryption-test" # database name 9 | MONGO_INITDB_ROOT_USERNAME: "root" # container root username 10 | MONGO_INITDB_ROOT_PASSWORD: "mfe" # container root password 11 | ports: 12 | - "27017-27019:27017-27019" 13 | volumes: 14 | - ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro 15 | - mongoose-field-encryption-db:/data/db 16 | 17 | volumes: 18 | mongoose-field-encryption-db: 19 | 20 | -------------------------------------------------------------------------------- /development/init-mongo-dev.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: "mfe", 3 | pwd: "mfe", 4 | roles: [ 5 | { 6 | role: "readWrite", 7 | db: "mongoose-field-encryption-test" 8 | } 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /lib/mongoose-field-encryption.d.ts: -------------------------------------------------------------------------------- 1 | export function fieldEncryption(schema: any, options: any): void; 2 | export function encrypt(clearText: any, secret: any, saltGenerator: any): string; 3 | /** 4 | * Decryption has a default fallback for the deprecated algorithm 5 | * 6 | * @param {*} encryptedHex 7 | * @param {*} secret 8 | */ 9 | export function decrypt(encryptedHex: any, secret: any): string; 10 | export function encryptAes256Ctr(text: any, secret: any): string; 11 | -------------------------------------------------------------------------------- /lib/mongoose-field-encryption.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require("crypto"); 4 | const algorithm = "aes-256-cbc"; 5 | const deprecatedAlgorithm = "aes-256-ctr"; 6 | const encryptedFieldNamePrefix = "__enc_"; 7 | const encryptedFieldDataSuffix = "_d"; 8 | 9 | const encryptAes256Ctr = function (text, secret) { 10 | const cipher = crypto.createCipher(deprecatedAlgorithm, secret); 11 | let crypted = cipher.update(text, "utf8", "hex"); 12 | crypted += cipher.final("hex"); 13 | return crypted; 14 | }; 15 | 16 | const decryptAes256Ctr = function (encryptedHex, secret) { 17 | const decipher = crypto.createDecipher(deprecatedAlgorithm, secret); 18 | let dec = decipher.update(encryptedHex, "hex", "utf8"); 19 | dec += decipher.final("utf8"); 20 | return dec; 21 | }; 22 | 23 | const encrypt = function (clearText, secret, saltGenerator) { 24 | const iv = saltGeneratorWrapper(saltGenerator(secret)); 25 | const cipher = crypto.createCipheriv(algorithm, secret, iv); 26 | const encrypted = cipher.update(clearText); 27 | const finalBuffer = Buffer.concat([encrypted, cipher.final()]); 28 | const encryptedHex = iv.toString("hex") + ":" + finalBuffer.toString("hex"); 29 | return encryptedHex; 30 | }; 31 | 32 | const saltGeneratorWrapper = (iv) => { 33 | if (iv instanceof Buffer) { 34 | if (iv.length !== 16) { 35 | throw new Error("Invalid salt provided, please ensure that the salt is a Buffer of length 16"); 36 | } 37 | return iv; 38 | } 39 | 40 | if (typeof iv === "string" || iv instanceof String) { 41 | if (iv.length !== 16) { 42 | throw new Error("Invalid salt, please ensure that the salt is a string of length 16"); 43 | } 44 | return Buffer.from(iv); 45 | } 46 | 47 | throw new Error("Invalid salt, please ensure that the salt is either a string or a Buffer of length 16"); 48 | }; 49 | 50 | const defaultSaltGenerator = (secret) => crypto.randomBytes(16); 51 | 52 | /** 53 | * Decryption has a default fallback for the deprecated algorithm 54 | * 55 | * @param {*} encryptedHex 56 | * @param {*} secret 57 | */ 58 | const decrypt = function (encryptedHex, secret, decryptOptions = {}) { 59 | try { 60 | const encryptedArray = encryptedHex.split(":"); 61 | 62 | // maintain backwards compatibility 63 | if (encryptedArray.length === 1) { 64 | return decryptAes256Ctr(encryptedArray[0], secret); 65 | } 66 | 67 | // @ts-ignore 68 | const iv = new Buffer.from(encryptedArray[0], "hex"); 69 | // @ts-ignore 70 | const encrypted = new Buffer.from(encryptedArray[1], "hex"); 71 | const decipher = crypto.createDecipheriv(algorithm, secret, iv); 72 | const decrypted = decipher.update(encrypted); 73 | const clearText = Buffer.concat([decrypted, decipher.final()]).toString(); 74 | return clearText; 75 | } catch (err) { 76 | if (decryptOptions.notifyDecryptFails) { 77 | throw err; 78 | } 79 | } 80 | return ""; 81 | }; 82 | 83 | const fieldEncryption = function (schema, options) { 84 | if (!options || !options.secret) { 85 | throw new Error("missing required secret"); 86 | } 87 | 88 | const useAes256Ctr = options.useAes256Ctr || false; 89 | const fieldsToEncrypt = options.fields || []; 90 | 91 | const _secret = typeof options.secret === "function" ? options.secret : () => options.secret; 92 | 93 | const _hash = (secret) => crypto.createHash("sha256").update(secret).digest("hex").substring(0, 32); 94 | 95 | const secret = useAes256Ctr ? _secret : () => _hash(_secret()); 96 | const encryptionStrategy = useAes256Ctr ? encryptAes256Ctr : encrypt; 97 | const saltGenerator = options.saltGenerator ? options.saltGenerator : defaultSaltGenerator; 98 | 99 | // Added option for a user to skip null values encryption. 100 | // The default is true, for the sake of backward compitability, where user already encrypts null. 101 | const encryptNull = options.encryptNull !== undefined ? options.encryptNull : true; 102 | 103 | // Added option for a user to get an exception if decrypt fails. 104 | // Maintained default behaviour that mongoose-field-encryption notifies decrypt failures. 105 | const notifyDecryptFails = options.notifyDecryptFails !== undefined ? options.notifyDecryptFails : true; 106 | 107 | // add marker fields to schema 108 | for (const field of fieldsToEncrypt) { 109 | const encryptedFieldName = encryptedFieldNamePrefix + field; 110 | const encryptedFieldData = encryptedFieldName + encryptedFieldDataSuffix; 111 | const schemaField = {}; 112 | 113 | schemaField[encryptedFieldName] = { type: Boolean }; 114 | schemaField[encryptedFieldData] = { type: String }; 115 | schema.add(schemaField); 116 | } 117 | 118 | // 119 | // local methods 120 | // 121 | 122 | // for mongoose 4/5 compatibility 123 | const defaultNext = function defaultNext(err) { 124 | if (err) { 125 | throw err; 126 | } 127 | }; 128 | 129 | function getCompatitibleNextFunc(next) { 130 | if (typeof next !== "function") { 131 | return defaultNext; 132 | } 133 | return next; 134 | } 135 | 136 | function getCompatibleData(next, data) { 137 | // in mongoose5, 'data' field is undefined 138 | if (!data) { 139 | return next; 140 | } 141 | return data; 142 | } 143 | 144 | function encryptFields(obj, fields, secret) { 145 | for (const field of fields) { 146 | const encryptedFieldName = encryptedFieldNamePrefix + field; 147 | const encryptedFieldData = encryptedFieldName + encryptedFieldDataSuffix; 148 | const fieldValue = obj[field]; 149 | 150 | if (!obj[encryptedFieldName] && typeof fieldValue !== "undefined") { 151 | if (fieldValue === null && encryptNull === false) { 152 | // protect null value field, and do not try to encrypt it 153 | continue; 154 | } 155 | if (typeof fieldValue === "string") { 156 | // handle strings separately to maintain searchability 157 | const value = encryptionStrategy(fieldValue, secret, saltGenerator); 158 | obj[field] = value; 159 | } else { 160 | const value = encryptionStrategy(JSON.stringify(fieldValue), secret, saltGenerator); 161 | obj[field] = undefined; 162 | obj[encryptedFieldData] = value; 163 | } 164 | 165 | obj[encryptedFieldName] = true; 166 | } 167 | } 168 | } 169 | 170 | function decryptFields(obj, fields, secret) { 171 | for (const field of fields) { 172 | const encryptedFieldName = encryptedFieldNamePrefix + field; 173 | const encryptedFieldData = encryptedFieldName + encryptedFieldDataSuffix; 174 | 175 | if (obj[encryptedFieldName]) { 176 | if (obj[encryptedFieldData]) { 177 | const encryptedValue = obj[encryptedFieldData]; 178 | 179 | obj[field] = JSON.parse(decrypt(encryptedValue, secret)); 180 | obj[encryptedFieldName] = false; 181 | obj[encryptedFieldData] = ""; 182 | } else { 183 | // If the field has been marked to not be retrieved, it'll be undefined 184 | if (obj[field]) { 185 | // handle strings separately to maintain searchability 186 | const encryptedValue = obj[field]; 187 | obj[field] = decrypt(encryptedValue, secret, { notifyDecryptFails: notifyDecryptFails }); 188 | obj[encryptedFieldName] = false; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | function updateHook(_next) { 196 | const next = getCompatitibleNextFunc(_next); 197 | for (const field of fieldsToEncrypt) { 198 | const encryptedFieldName = encryptedFieldNamePrefix + field; 199 | this._update.$set = this._update.$set || {}; 200 | const plainTextValue = this._update.$set[field] || this._update[field]; 201 | const encryptedFieldValue = this._update.$set[encryptedFieldName] || this._update[encryptedFieldName]; 202 | 203 | if (!encryptedFieldValue && plainTextValue) { 204 | const updateObj = {}; 205 | if (typeof plainTextValue === "string" || plainTextValue instanceof String) { 206 | const encryptedData = encryptionStrategy(plainTextValue, secret(), saltGenerator); 207 | 208 | updateObj[field] = encryptedData; 209 | updateObj[encryptedFieldName] = true; 210 | } else { 211 | const encryptedFieldData = encryptedFieldName + encryptedFieldDataSuffix; 212 | 213 | updateObj[field] = undefined; 214 | updateObj[encryptedFieldData] = encryptionStrategy(JSON.stringify(plainTextValue), secret(), saltGenerator); 215 | updateObj[encryptedFieldName] = true; 216 | } 217 | this.updateOne({}, Object.keys(this._update.$set).length > 0 ? { $set: updateObj } : updateObj); 218 | } 219 | } 220 | 221 | next(); 222 | } 223 | 224 | // 225 | // static methods 226 | // 227 | 228 | schema.methods.stripEncryptionFieldMarkers = function () { 229 | for (const field of fieldsToEncrypt) { 230 | const encryptedFieldName = encryptedFieldNamePrefix + field; 231 | const encryptedFieldData = encryptedFieldName + encryptedFieldDataSuffix; 232 | 233 | this.set(encryptedFieldName, undefined); 234 | this.set(encryptedFieldData, undefined); 235 | } 236 | }; 237 | 238 | schema.methods.decryptFieldsSync = function () { 239 | decryptFields(this, fieldsToEncrypt, secret()); 240 | }; 241 | 242 | schema.methods.encryptFieldsSync = function () { 243 | encryptFields(this, fieldsToEncrypt, secret()); 244 | }; 245 | 246 | // 247 | // hooks 248 | // 249 | 250 | schema.post("init", function (_next, _data) { 251 | const next = getCompatitibleNextFunc(_next); 252 | const data = getCompatibleData(_next, _data); 253 | try { 254 | decryptFields(data, fieldsToEncrypt, secret()); 255 | next(); 256 | } catch (err) { 257 | next(err); 258 | } 259 | }); 260 | 261 | schema.pre("save", function (_next) { 262 | const next = getCompatitibleNextFunc(_next); 263 | 264 | try { 265 | encryptFields(this, fieldsToEncrypt, secret()); 266 | next(); 267 | } catch (err) { 268 | next(err); 269 | } 270 | }); 271 | 272 | schema.pre("insertMany", function (_next, docs) { 273 | const next = getCompatitibleNextFunc(_next); 274 | 275 | try { 276 | for (let doc of docs) { 277 | encryptFields(doc, fieldsToEncrypt, secret()); 278 | } 279 | 280 | next(); 281 | } catch (err) { 282 | next(err); 283 | } 284 | }); 285 | 286 | schema.pre("findOneAndUpdate", updateHook); 287 | 288 | // schema.pre("update", updateHook); 289 | schema.pre("updateOne", updateHook); 290 | }; 291 | 292 | module.exports.fieldEncryption = fieldEncryption; 293 | module.exports.encrypt = encrypt; 294 | module.exports.decrypt = decrypt; 295 | module.exports.encryptAes256Ctr = encryptAes256Ctr; 296 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-field-encryption", 3 | "version": "7.0.1", 4 | "description": "A simple symmetric encryption plugin for individual fields. Dependency free, only mongoose peer dependency.", 5 | "main": "lib/mongoose-field-encryption.js", 6 | "types": "lib/mongoose-field-encryption.d.ts", 7 | "files": [ 8 | "lib/" 9 | ], 10 | "scripts": { 11 | "test": "mocha", 12 | "test:travis": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 13 | "test:auth": "URI='mongodb://mfe:mfe@127.0.0.1:27017/mongoose-field-encryption-test' npm test", 14 | "test-coverage": "nyc --reporter=html --reporter=text ./node_modules/mocha/bin/_mocha && chromium ./coverage/index.html", 15 | "test-coverage:auth": "URI='mongodb://mfe:mfe@127.0.0.1:27017/mongoose-field-encryption-test' nyc --reporter=html --reporter=text ./node_modules/mocha/bin/_mocha && chromium ./coverage/index.html", 16 | "test-coverage:auth:coveralls": "URI='mongodb://mfe:mfe@127.0.0.1:27017/mongoose-field-encryption-test' nyc npm test && nyc report --reporter=text-lcov | coveralls", 17 | "release-it": "release-it" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/wheresvic/mongoose-field-encryption" 22 | }, 23 | "keywords": [ 24 | "mongoose", 25 | "encryption", 26 | "field", 27 | "cqrs", 28 | "string", 29 | "encrypt", 30 | "security", 31 | "search", 32 | "searchable", 33 | "mongo" 34 | ], 35 | "author": { 36 | "name": "Victor Parmar", 37 | "email": "victorparmar@gmail.com", 38 | "url": "https://smalldata.tech" 39 | }, 40 | "contributors": [], 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/victorparmar/mongoose-field-encryption/issues" 44 | }, 45 | "peerDependencies": { 46 | "mongoose": ">=7.4.0" 47 | }, 48 | "devDependencies": { 49 | "bluebird": "3.7.2", 50 | "chai": "4.3.7", 51 | "coveralls": "3.1.1", 52 | "mocha": "10.2.0", 53 | "mocha-lcov-reporter": "1.3.0", 54 | "mongoose": "^7.4.1", 55 | "nyc": "15.1.0", 56 | "release-it": "16.1.2", 57 | "sinon": "15.2.0", 58 | "typescript": "5.1.6" 59 | }, 60 | "release-it": { 61 | "hooks": { 62 | "before:init": "npm run test:auth", 63 | "before:bump": null, 64 | "after:bump": null, 65 | "before:release": null, 66 | "after:release": "git describe --abbrev=0 --tags" 67 | }, 68 | "npm": { 69 | "publish": true 70 | }, 71 | "github": { 72 | "release": true 73 | }, 74 | "gitlab": { 75 | "release": false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupMongoose: function (mongoose) { 3 | if (mongoose.version) { 4 | const semanticPieces = mongoose.version.split("."); 5 | const majorVersion = parseInt(semanticPieces[0]); 6 | if (majorVersion < 6) { 7 | mongoose.set("useFindAndModify", false); 8 | } 9 | } 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/test-basic-usage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require("crypto"); 4 | const expect = require("chai").expect; 5 | const mongoose = require("mongoose"); 6 | const Promise = require("bluebird"); 7 | const { setupMongoose } = require("./setup"); 8 | const Schema = mongoose.Schema; 9 | 10 | mongoose.Promise = Promise; 11 | mongoose.set("bufferCommands", false); 12 | 13 | const mongooseFieldEncryption = require("../lib/mongoose-field-encryption").fieldEncryption; 14 | 15 | const uri = process.env.URI || "mongodb://127.0.0.1:27017/mongoose-field-encryption-test"; 16 | 17 | const postSchema = new Schema({ 18 | title: String, 19 | message: String, 20 | }); 21 | 22 | postSchema.plugin(mongooseFieldEncryption, { fields: ["message"], secret: "some secret key" }); 23 | 24 | const Post = mongoose.model("Post", postSchema); 25 | 26 | describe("basic usage", function () { 27 | this.timeout(5000); 28 | 29 | before(async function () { 30 | await mongoose.connect(uri, { useNewUrlParser: true, autoIndex: false, useUnifiedTopology: true }); 31 | 32 | setupMongoose(mongoose); 33 | }); 34 | 35 | after(async function () { 36 | await mongoose.disconnect(); 37 | }); 38 | 39 | beforeEach(async function () { 40 | await Post.deleteMany({}); 41 | }); 42 | 43 | it("should save a document", async function () { 44 | const post = new Post({ title: "some text", message: "hello all" }); 45 | 46 | // when 47 | await post.save(); 48 | 49 | expect(post.title).to.equal("some text"); 50 | expect(post.message).to.not.be.undefined; 51 | const split = post.message.split(":"); 52 | expect(split.length).to.equal(2); 53 | expect(post.__enc_message).to.be.true; 54 | 55 | console.dir(post.toObject()); 56 | }); 57 | 58 | it("should save many documents", async function () { 59 | const posts = [ 60 | new Post({ title: "some text 0", message: "hello all" }), 61 | new Post({ title: "some text 1", message: "hello many" }), 62 | new Post({ title: "some text 2", message: "hello aloha!" }), 63 | ]; 64 | 65 | // when 66 | await Post.insertMany(posts); 67 | 68 | let i = 0; 69 | for (const post of posts) { 70 | expect(post.title).to.equal(`some text ${i}`); 71 | expect(post.message).to.not.be.undefined; 72 | const split = post.message.split(":"); 73 | expect(split.length).to.equal(2); 74 | expect(post.__enc_message).to.be.true; 75 | i++; 76 | } 77 | 78 | { 79 | const posts = await Post.find({}); 80 | 81 | console.log(posts); 82 | expect(posts).to.not.be.null; 83 | expect(posts.length).to.equal(3); 84 | 85 | let i = 0; 86 | for (const post of posts) { 87 | expect(post.title).to.equal(`some text ${i}`); 88 | expect(post.message).to.not.be.undefined; 89 | expect(post.message.startsWith("hello")).to.be.true; 90 | expect(post.__enc_message).to.be.false; 91 | i++; 92 | } 93 | } 94 | }); 95 | 96 | it("should search for a document on an encrypted field", function (done) { 97 | // given 98 | const messageSchema = new Schema({ 99 | title: String, 100 | message: String, 101 | name: String, 102 | }); 103 | 104 | messageSchema.plugin(mongooseFieldEncryption, { 105 | fields: ["message", "name"], 106 | secret: "some secret key", 107 | saltGenerator: function (secret) { 108 | return "1234567890123456"; 109 | }, 110 | }); 111 | 112 | const title = "some text"; 113 | const name = random(10); 114 | const message = "hello all"; 115 | 116 | const Message = mongoose.model("Message", messageSchema); 117 | 118 | const messageToSave = new Message({ title: title, message: message, name: name }); 119 | 120 | messageToSave 121 | .save() 122 | .then(function (savedMessage) { 123 | const messageToSearchWith = new Message({ name: name }); 124 | messageToSearchWith.encryptFieldsSync(); 125 | 126 | // when 127 | return Message.find({ name: messageToSearchWith.name }); 128 | }) 129 | .then(function (results) { 130 | // then 131 | expect(results.length).to.equal(1); 132 | const ret = results[0].toObject(); 133 | 134 | expect(ret.title).to.equal(title); 135 | expect(ret.message).to.equal(message); 136 | expect(ret.name).to.equal(name); 137 | 138 | done(); 139 | }); 140 | }); 141 | }); 142 | 143 | function random(howMany, chars) { 144 | chars = chars || "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789"; 145 | const rnd = crypto.randomBytes(howMany); 146 | const value = new Array(howMany); 147 | const len = Math.min(256, chars.length); 148 | const d = 256 / len; 149 | 150 | for (var i = 0; i < howMany; i++) { 151 | value[i] = chars[Math.floor(rnd[i] / d)]; 152 | } 153 | 154 | return value.join(""); 155 | } 156 | -------------------------------------------------------------------------------- /test/test-db.js: -------------------------------------------------------------------------------- 1 | const expect = require("chai").expect; 2 | 3 | const mongoose = require("mongoose"); 4 | const { setupMongoose } = require("./setup"); 5 | mongoose.set("bufferCommands", false); 6 | 7 | const fieldEncryptionPlugin = require("../lib/mongoose-field-encryption").fieldEncryption; 8 | 9 | const uri = process.env.URI || "mongodb://127.0.0.1:27017/mongoose-field-encryption-test"; 10 | 11 | describe("mongoose-field-encryption plugin db", function () { 12 | this.timeout(5000); 13 | 14 | before(function (done) { 15 | mongoose 16 | .connect(uri, { 17 | useNewUrlParser: true, 18 | autoIndex: false, 19 | useUnifiedTopology: true, 20 | }) 21 | .then(function () { 22 | done(); 23 | }); 24 | 25 | setupMongoose(mongoose); 26 | }); 27 | 28 | after(function (done) { 29 | mongoose.disconnect().then(function () { 30 | done(); 31 | }); 32 | }); 33 | 34 | const MongooseSchema = { 35 | toEncryptString: { type: String, required: true }, 36 | toEncryptStringNotRetrieved: { type: String, select: false }, 37 | toEncryptObject: { 38 | nested: String, 39 | }, 40 | toEncryptArray: [], 41 | toEncryptDate: Date, 42 | }; 43 | 44 | function getSut(MongooseModel) { 45 | const sut = new MongooseModel({ 46 | toEncryptString: "hide me!", 47 | toEncryptObject: { 48 | nested: "some stuff to encrypt", 49 | }, 50 | toEncryptArray: [1, 2, 3], 51 | toEncryptDate: new Date(1485641048338), 52 | }); 53 | 54 | return sut; 55 | } 56 | 57 | const fieldEncryptionPluginOptions = { 58 | fields: ["toEncryptString", "toEncryptObject", "toEncryptArray", "toEncryptDate", "toEncryptStringNotRetrieved"], 59 | secret: "icanhazcheezburger", // should ideally be process.env.SECRET 60 | }; 61 | 62 | describe("simple setup", function () { 63 | const NestedFieldEncryptionSchema = new mongoose.Schema(MongooseSchema); 64 | 65 | NestedFieldEncryptionSchema.plugin(fieldEncryptionPlugin, fieldEncryptionPluginOptions); 66 | 67 | const NestedFieldEncryption = mongoose.model("NestedFieldEncryption", NestedFieldEncryptionSchema); 68 | 69 | function expectEncryptionValues(sut) { 70 | expect(sut.__enc_toEncryptString).to.be.true; 71 | 72 | expect(sut.toObject().toEncryptObject).to.be.undefined; 73 | expect(sut.__enc_toEncryptObject).to.be.true; 74 | 75 | expect(sut.toObject().toEncryptArray).to.be.undefined; 76 | expect(sut.__enc_toEncryptArray).to.be.true; 77 | 78 | expect(sut.toObject().toEncryptDate).to.be.undefined; 79 | expect(sut.__enc_toEncryptDate).to.be.true; 80 | } 81 | 82 | function expectDecryptionValues(found) { 83 | expect(found.toEncryptString).to.equal("hide me!"); 84 | expect(found.__enc_toEncryptString).to.be.false; 85 | 86 | expect(JSON.stringify(found.toEncryptObject)).to.equal('{"nested":"some stuff to encrypt"}'); 87 | expect(found.__enc_toEncryptObject).to.be.false; 88 | expect(found.__enc_toEncryptObject_d).to.equal(""); 89 | 90 | expect(JSON.stringify(found.toEncryptArray)).to.equal("[1,2,3]"); 91 | expect(found.__enc_toEncryptArray).to.be.false; 92 | expect(found.__enc_toEncryptArray_d).to.equal(""); 93 | 94 | expect(JSON.stringify(found.toEncryptDate)).to.equal('"2017-01-28T22:04:08.338Z"'); 95 | expect(found.__enc_toEncryptDate).to.be.false; 96 | expect(found.__enc_toEncryptDate_d).to.equal(""); 97 | } 98 | 99 | runTests(NestedFieldEncryption, getSut, expectEncryptionValues, expectDecryptionValues); 100 | }); 101 | 102 | describe("simple setup with salt factory", function () { 103 | const NestedFieldEncryptionSaltFactorySchema = new mongoose.Schema(MongooseSchema); 104 | 105 | const options = {}; 106 | const optionsWithSecret = Object.assign(options, fieldEncryptionPluginOptions); 107 | optionsWithSecret.secret = function () { 108 | return fieldEncryptionPluginOptions.secret; 109 | }; 110 | 111 | NestedFieldEncryptionSaltFactorySchema.plugin(fieldEncryptionPlugin, optionsWithSecret); 112 | 113 | const NestedFieldEncryptionSaltFactory = mongoose.model( 114 | "NestedFieldEncryptionSaltFactory", 115 | NestedFieldEncryptionSaltFactorySchema 116 | ); 117 | 118 | function expectEncryptionValues(sut) { 119 | expect(sut.__enc_toEncryptString).to.be.true; 120 | 121 | expect(sut.toObject().toEncryptObject).to.be.undefined; 122 | expect(sut.__enc_toEncryptObject).to.be.true; 123 | 124 | expect(sut.toObject().toEncryptArray).to.be.undefined; 125 | expect(sut.__enc_toEncryptArray).to.be.true; 126 | 127 | expect(sut.toObject().toEncryptDate).to.be.undefined; 128 | expect(sut.__enc_toEncryptDate).to.be.true; 129 | } 130 | 131 | function expectDecryptionValues(found) { 132 | expect(found.toEncryptString).to.equal("hide me!"); 133 | expect(found.__enc_toEncryptString).to.be.false; 134 | 135 | expect(JSON.stringify(found.toEncryptObject)).to.equal('{"nested":"some stuff to encrypt"}'); 136 | expect(found.__enc_toEncryptObject).to.be.false; 137 | expect(found.__enc_toEncryptObject_d).to.equal(""); 138 | 139 | expect(JSON.stringify(found.toEncryptArray)).to.equal("[1,2,3]"); 140 | expect(found.__enc_toEncryptArray).to.be.false; 141 | expect(found.__enc_toEncryptArray_d).to.equal(""); 142 | 143 | expect(JSON.stringify(found.toEncryptDate)).to.equal('"2017-01-28T22:04:08.338Z"'); 144 | expect(found.__enc_toEncryptDate).to.be.false; 145 | expect(found.__enc_toEncryptDate_d).to.equal(""); 146 | } 147 | 148 | runTests(NestedFieldEncryptionSaltFactory, getSut, expectEncryptionValues, expectDecryptionValues); 149 | }); 150 | 151 | describe("custom salt", function () { 152 | const NestedFieldEncryptionCustomSaltSchema = new mongoose.Schema(MongooseSchema); 153 | 154 | NestedFieldEncryptionCustomSaltSchema.plugin( 155 | fieldEncryptionPlugin, 156 | Object.assign({ saltGenerator: (secret) => secret.slice(0, 16) }, fieldEncryptionPluginOptions) 157 | ); 158 | 159 | const NestedFieldEncryptionCustomSalt = mongoose.model( 160 | "NestedFieldEncryptionCustomSalt", 161 | NestedFieldEncryptionCustomSaltSchema 162 | ); 163 | 164 | function expectEncryptionValues(sut) { 165 | const toObject = sut.toObject(); 166 | 167 | expect(sut.__enc_toEncryptString).to.be.true; 168 | expect(sut.toEncryptString).to.equal("37373539656562373263336135633161:853640c6ba4c570e2818068ac79af248"); 169 | 170 | expect(toObject.toEncryptObject).to.be.undefined; 171 | expect(sut.__enc_toEncryptObject_d).to.equal( 172 | "37373539656562373263336135633161:0c8443e5d5a6620a9840939748789fe1206b3d2f09c3b1caff0a4a56c5283e31ded93b4a8fa821200fa58a5874d41148" 173 | ); 174 | expect(sut.__enc_toEncryptObject).to.be.true; 175 | 176 | expect(toObject.toEncryptArray).to.be.undefined; 177 | expect(sut.__enc_toEncryptArray_d).to.equal("37373539656562373263336135633161:1a94f782f6b93fc68f6059f2af865b84"); 178 | expect(sut.__enc_toEncryptArray).to.be.true; 179 | 180 | expect(toObject.toEncryptDate).to.be.undefined; 181 | expect(sut.__enc_toEncryptDate).to.be.true; 182 | expect(sut.__enc_toEncryptDate_d).to.equal( 183 | "37373539656562373263336135633161:24a096e92ad9e32c8c8015a5bfab93c9fe88d027403c750ff4d71a35bb538ac3" 184 | ); 185 | } 186 | 187 | function expectDecryptionValues(found) { 188 | expect(found.toEncryptString).to.equal("hide me!"); 189 | expect(found.__enc_toEncryptString).to.be.false; 190 | 191 | expect(JSON.stringify(found.toEncryptObject)).to.equal('{"nested":"some stuff to encrypt"}'); 192 | expect(found.__enc_toEncryptObject).to.be.false; 193 | expect(found.__enc_toEncryptObject_d).to.equal(""); 194 | 195 | expect(JSON.stringify(found.toEncryptArray)).to.equal("[1,2,3]"); 196 | expect(found.__enc_toEncryptArray).to.be.false; 197 | expect(found.__enc_toEncryptArray_d).to.equal(""); 198 | 199 | expect(JSON.stringify(found.toEncryptDate)).to.equal('"2017-01-28T22:04:08.338Z"'); 200 | expect(found.__enc_toEncryptDate).to.be.false; 201 | expect(found.__enc_toEncryptDate_d).to.equal(""); 202 | } 203 | 204 | runTests(NestedFieldEncryptionCustomSalt, getSut, expectEncryptionValues, expectDecryptionValues); 205 | }); 206 | 207 | describe("subdocument encryption", function () { 208 | it("should encrypt and decrypt subdocuments", function (done) { 209 | const UserExtraSchema = new mongoose.Schema({ 210 | city: { type: String }, 211 | country: { type: String }, 212 | address: { type: String }, 213 | postalCode: { type: String }, 214 | }); 215 | 216 | UserExtraSchema.plugin(fieldEncryptionPlugin, { 217 | fields: ["address"], 218 | secret: "icanhazcheeseburger", 219 | saltGenerator: (secret) => secret.slice(0, 16), 220 | }); 221 | 222 | const UserSchema = new mongoose.Schema( 223 | { 224 | name: { type: String, required: true }, 225 | surname: { type: String, required: true }, 226 | email: { type: String, required: true }, 227 | extra: UserExtraSchema, 228 | }, 229 | { collection: "users" } 230 | ); 231 | 232 | UserSchema.plugin(fieldEncryptionPlugin, { 233 | fields: ["name", "surname"], 234 | secret: "icanhazcheeseburger", 235 | saltGenerator: (secret) => secret.slice(0, 16), 236 | }); 237 | 238 | const UserModel = mongoose.model("User", UserSchema); 239 | 240 | // given 241 | const sut = new UserModel({ 242 | name: "snoop", 243 | surname: "dawg", 244 | email: "snoop.dawg@dadadadada.com", 245 | extra: { 246 | city: "cali", 247 | country: "usa", 248 | address: "bowchickabowwow", 249 | postalCode: "90210", 250 | }, 251 | }); 252 | 253 | // when 254 | sut 255 | .save() 256 | .then(() => { 257 | // then 258 | expect(sut.name).to.equal("31393663303733396265616337353364:5b8c2d2160c935152c7dd6bf6a31f894"); 259 | expect(sut.extra.address).to.equal("31393663303733396265616337353364:b9610446ddefafef177ffe0cdfec2d2d"); 260 | 261 | return UserModel.findById(sut._id); 262 | }) 263 | .then((found) => { 264 | expect(found.name).to.equal("snoop"); 265 | expect(found.extra.address).to.equal("bowchickabowwow"); 266 | }) 267 | .finally(() => { 268 | done(); 269 | }); 270 | }); 271 | }); 272 | 273 | function runTests(MongooseModel, getSut, expectEncryptionValues, expectDecryptionValues) { 274 | it("should encrypt fields on save and decrypt fields on findById", function () { 275 | // given 276 | const sut = getSut(MongooseModel); 277 | 278 | // when 279 | return sut 280 | .save() 281 | .then(() => { 282 | // then 283 | expectEncryptionValues(sut); 284 | 285 | return MongooseModel.findById(sut._id); 286 | }) 287 | .then((found) => { 288 | expectDecryptionValues(found); 289 | }); 290 | }); 291 | 292 | it("should encrypt fields on save and decrypt fields on findOne", function () { 293 | // given 294 | const sut = getSut(MongooseModel); 295 | 296 | // when 297 | return sut 298 | .save() 299 | .then(() => { 300 | expectEncryptionValues(sut); 301 | return MongooseModel.findOne({ _id: sut._id }); 302 | }) 303 | .then((found) => { 304 | expectDecryptionValues(found); 305 | }); 306 | }); 307 | 308 | it("should store encrypted fields as plaintext on findOneAndUpdate", function () { 309 | // given 310 | const sut = getSut(MongooseModel); 311 | 312 | // when 313 | return sut 314 | .save() 315 | .then(() => { 316 | expectEncryptionValues(sut); 317 | 318 | return MongooseModel.findOneAndUpdate( 319 | { _id: sut._id }, 320 | { 321 | $set: { toEncryptString: "snoop", __enc_toEncryptString: false }, 322 | }, 323 | { new: true, useFindAndModify: false } 324 | ); 325 | }) 326 | .then((found) => { 327 | // then 328 | expect(found.__enc_toEncryptString).to.be.false; 329 | expect(found.toEncryptString).to.equal("snoop"); 330 | }); 331 | }); 332 | 333 | it("should encrypt string fields on updateOne", function () { 334 | // given 335 | const sut = getSut(MongooseModel); 336 | 337 | // when 338 | return sut 339 | .save() 340 | .then(() => { 341 | expectEncryptionValues(sut); 342 | 343 | return MongooseModel.updateOne( 344 | { _id: sut._id }, 345 | { $set: { toEncryptString: "snoop", __enc_toEncryptString: false } } 346 | ); 347 | }) 348 | .then(() => { 349 | return MongooseModel.findById(sut._id); 350 | }) 351 | .then((found) => { 352 | // then 353 | expect(found.__enc_toEncryptString).to.be.false; 354 | expect(found.toEncryptString).to.equal("snoop"); 355 | }); 356 | }); 357 | 358 | it("should encrypt string fields on update without $set", function () { 359 | // given 360 | const sut = getSut(MongooseModel); 361 | 362 | // when 363 | return sut 364 | .save() 365 | .then(() => { 366 | expectEncryptionValues(sut); 367 | 368 | return MongooseModel.updateOne({ _id: sut._id }, { toEncryptString: "snoop" }); 369 | }) 370 | .then(() => { 371 | return MongooseModel.findById(sut._id); 372 | }) 373 | .then((found) => { 374 | // then 375 | expect(found.__enc_toEncryptString).to.be.false; 376 | expect(found.toEncryptString).to.equal("snoop"); 377 | }); 378 | }); 379 | 380 | it("should encrypt string fields on fineOneAndUpdate", function () { 381 | // given 382 | const sut = getSut(MongooseModel); 383 | 384 | // when 385 | return sut 386 | .save() 387 | .then(() => { 388 | expectEncryptionValues(sut); 389 | 390 | return MongooseModel.findOneAndUpdate( 391 | { _id: sut._id }, 392 | { $set: { toEncryptString: "snoop", __enc_toEncryptString: false } } 393 | ); 394 | }) 395 | .then(() => { 396 | return MongooseModel.findById(sut._id); 397 | }) 398 | .then((found) => { 399 | // then 400 | expect(found.__enc_toEncryptString).to.be.false; 401 | expect(found.toEncryptString).to.equal("snoop"); 402 | }); 403 | }); 404 | 405 | it("should encrypt non string fields on update", function () { 406 | // given 407 | const sut = getSut(MongooseModel); 408 | 409 | // when 410 | return sut 411 | .save() 412 | .then(() => { 413 | expectEncryptionValues(sut); 414 | 415 | return MongooseModel.updateOne( 416 | { 417 | _id: sut._id, 418 | }, 419 | { 420 | $set: { 421 | toEncryptObject: { nested: "snoop" }, 422 | __enc_toEncryptObject: false, 423 | }, 424 | } 425 | ); 426 | }) 427 | .then(() => { 428 | return MongooseModel.findById(sut._id); 429 | }) 430 | .then((found) => { 431 | expect(found.toEncryptObject.nested).to.eql("snoop"); 432 | }); 433 | }); 434 | 435 | it("should encrypt non string fields on fineOneAndUpdate without $set", function () { 436 | // given 437 | const sut = getSut(MongooseModel); 438 | 439 | // when 440 | return sut 441 | .save() 442 | .then(() => { 443 | expectEncryptionValues(sut); 444 | 445 | return MongooseModel.findOneAndUpdate( 446 | { 447 | _id: sut._id, 448 | }, 449 | { 450 | toEncryptObject: { nested: "snoop" }, 451 | } 452 | ); 453 | }) 454 | .then(() => { 455 | return MongooseModel.findById(sut._id); 456 | }) 457 | .then((found) => { 458 | expect(found.toEncryptObject.nested).to.eql("snoop"); 459 | }); 460 | }); 461 | 462 | it("should decrypt data on find() method call", function () { 463 | // given 464 | const sut = getSut(MongooseModel); 465 | 466 | // when 467 | return sut 468 | .save() 469 | .then(() => { 470 | expectEncryptionValues(sut); 471 | 472 | return MongooseModel.findOneAndUpdate( 473 | { 474 | _id: sut._id, 475 | }, 476 | { 477 | toEncryptString: "yaddayadda", 478 | toEncryptObject: { nested: "snoop" }, 479 | } 480 | ); 481 | }) 482 | .then(() => { 483 | return MongooseModel.find({ _id: sut._id }); 484 | }) 485 | .then((foundArray) => { 486 | const found = foundArray[0]; 487 | expect(found.toEncryptString).to.eql("yaddayadda"); 488 | expect(found.toEncryptObject.nested).to.eql("snoop"); 489 | }); 490 | }); 491 | 492 | it("should decrypt data on find() method call when only selected encrypted fields are retrieved", function () { 493 | // given 494 | const sut = getSut(MongooseModel); 495 | 496 | // when 497 | return sut 498 | .save() 499 | .then(() => { 500 | expectEncryptionValues(sut); 501 | 502 | return MongooseModel.findOneAndUpdate( 503 | { 504 | _id: sut._id, 505 | }, 506 | { 507 | toEncryptString: "yaddayadda", 508 | toEncryptObject: { nested: "snoop" }, 509 | } 510 | ); 511 | }) 512 | .then(() => { 513 | return MongooseModel.find({ _id: sut._id }, "__enc_toEncryptString toEncryptString"); 514 | }) 515 | .then((foundArray) => { 516 | const found = foundArray[0]; 517 | expect(found.toEncryptString).to.equal("yaddayadda"); 518 | }); 519 | }); 520 | 521 | it("should not encrypt already encrypted fields", function () { 522 | // given 523 | const sut = getSut(MongooseModel); 524 | 525 | // when 526 | return sut 527 | .save() 528 | .then(() => { 529 | expectEncryptionValues(sut); 530 | 531 | return MongooseModel.updateOne( 532 | { 533 | _id: sut._id, 534 | }, 535 | { 536 | $set: { 537 | toEncryptString: "already encrypted string", 538 | __enc_toEncryptObject: true, 539 | }, 540 | } 541 | ); 542 | }) 543 | .then(() => { 544 | return MongooseModel.findById(sut._id); 545 | }) 546 | .then((found) => { 547 | expect(found.toEncryptString).to.eql("already encrypted string"); 548 | }); 549 | }); 550 | 551 | it("should decrypt data on find() method call even if some fields are marked as not selectables", function () { 552 | // given 553 | const sut = getSut(MongooseModel); 554 | 555 | // when 556 | return sut 557 | .save() 558 | .then(() => { 559 | expectEncryptionValues(sut); 560 | 561 | return MongooseModel.findOneAndUpdate( 562 | { 563 | _id: sut._id, 564 | }, 565 | { 566 | toEncryptString: "yaddayadda", 567 | toEncryptObject: { nested: "snoop" }, 568 | toEncryptStringNotRetrieved: "dubidubida", 569 | } 570 | ); 571 | }) 572 | .then(() => { 573 | return MongooseModel.find({ _id: sut._id }); 574 | }) 575 | .then((foundArray) => { 576 | const found = foundArray[0]; 577 | expect(found.toEncryptString).to.equal("yaddayadda"); 578 | expect(found.toEncryptStringNotRetrieved).to.be.undefined; 579 | }); 580 | }); 581 | } 582 | 583 | describe("falsy values", function () { 584 | it("should encrypt and decrypt falsy values", function () { 585 | const FalsySchema = { 586 | toEncryptString: { type: String }, 587 | toEncryptArray: [], 588 | toEncryptDate: Date, 589 | toEncryptNumber: { type: Number }, 590 | toEncryptBoolean: { type: Boolean }, 591 | }; 592 | 593 | const FalsyEncryptionSchema = new mongoose.Schema(FalsySchema); 594 | FalsyEncryptionSchema.plugin(fieldEncryptionPlugin, { 595 | fields: ["toEncryptString", "toEncryptArray", "toEncryptDate", "toEncryptNumber", "toEncryptBoolean"], 596 | secret: "icanhazcheezburger", 597 | saltGenerator: (secret) => secret.slice(0, 16), 598 | }); 599 | 600 | const FalsyEncryptionModel = mongoose.model("FalsyEncryptionModel", FalsyEncryptionSchema); 601 | const sut = new FalsyEncryptionModel({ 602 | toEncryptString: "", 603 | toEncryptArray: [], 604 | toEncryptDate: 0, 605 | toEncryptNumber: 0, 606 | toEncryptBoolean: false, 607 | }); 608 | 609 | return sut 610 | .save() 611 | .then(() => { 612 | // console.log(sut); 613 | 614 | expect(sut.__enc_toEncryptString).to.be.true; 615 | expect(sut.toEncryptString).to.equal("37373539656562373263336135633161:9af345f139de3d5397f513a2ab105607"); 616 | 617 | expect(sut.toEncryptArray).to.be.undefined; 618 | expect(sut.__enc_toEncryptArray).to.be.true; 619 | expect(sut.__enc_toEncryptArray_d).to.equal( 620 | "37373539656562373263336135633161:b897c78694f3ad8533e246e21386e5d4" 621 | ); 622 | 623 | expect(sut.toEncryptDate).to.be.undefined; 624 | expect(sut.__enc_toEncryptDate).to.be.true; 625 | expect(sut.__enc_toEncryptDate_d).to.equal( 626 | "37373539656562373263336135633161:da4568a1046a687ecdf7dc65793d44725d67efcefa88508339750a074e485f25" 627 | ); 628 | 629 | expect(sut.toEncryptNumber).to.be.undefined; 630 | expect(sut.__enc_toEncryptNumber).to.be.true; 631 | expect(sut.__enc_toEncryptNumber_d).to.equal( 632 | "37373539656562373263336135633161:512c4a442a26f20f8ef81577d2fded37" 633 | ); 634 | 635 | expect(sut.toEncryptBoolean).to.be.undefined; 636 | expect(sut.__enc_toEncryptBoolean).to.be.true; 637 | expect(sut.__enc_toEncryptBoolean_d).to.equal( 638 | "37373539656562373263336135633161:2eee0dc63b89e2f6bc99febe113989f3" 639 | ); 640 | 641 | return FalsyEncryptionModel.findOne({ _id: sut._id }); 642 | }) 643 | .then((found) => { 644 | // console.log(found); 645 | 646 | expect(found.toEncryptString).to.equal(""); 647 | expect(found.__enc_toEncryptString).to.be.false; 648 | 649 | expect(found.toEncryptArray).to.deep.equal([]); 650 | expect(found.__enc_toEncryptArray).to.be.false; 651 | 652 | expect(found.toEncryptDate).to.deep.equal(new Date(0)); 653 | expect(found.__enc_toEncryptDate).to.be.false; 654 | 655 | expect(found.toEncryptNumber).to.equal(0); 656 | expect(found.__enc_toEncryptDate).to.be.false; 657 | 658 | expect(found.toEncryptBoolean).to.equal(false); 659 | expect(found.__enc_toEncryptDate).to.be.false; 660 | }); 661 | }); 662 | 663 | it("should encrypt and decrypt empty string and record a change", async function () { 664 | // given 665 | const FalsyRecordSchema = { 666 | toEncryptString: { type: String, required: false, default: "" }, 667 | }; 668 | 669 | const FalsyRecordEncryptionSchema = new mongoose.Schema(FalsyRecordSchema); 670 | FalsyRecordEncryptionSchema.plugin(fieldEncryptionPlugin, { 671 | fields: ["toEncryptString"], 672 | secret: "icanhazcheezburger", 673 | saltGenerator: (secret) => secret.slice(0, 16), 674 | }); 675 | 676 | const FalsyRecordEncryptionModel = mongoose.model("FalsyRecordEncryptionModel", FalsyRecordEncryptionSchema); 677 | const sut = new FalsyRecordEncryptionModel({ 678 | toEncryptString: "", 679 | }); 680 | 681 | // when 682 | await sut.save(); 683 | 684 | // then 685 | expect(sut.__enc_toEncryptString).to.be.true; 686 | expect(sut.toEncryptString).to.equal("37373539656562373263336135633161:9af345f139de3d5397f513a2ab105607"); 687 | 688 | const found = await FalsyRecordEncryptionModel.findOne({ _id: sut._id }); 689 | 690 | expect(found.toEncryptString).to.equal(""); 691 | expect(found.__enc_toEncryptString).to.be.false; 692 | 693 | found.toEncryptString = "value"; 694 | await found.save(); 695 | 696 | expect(found.toEncryptString).to.equal("37373539656562373263336135633161:8335a0fb01aab80cfea6a61653a329aa"); 697 | expect(found.__enc_toEncryptString).to.be.true; 698 | }); 699 | }); 700 | }); 701 | -------------------------------------------------------------------------------- /test/test-encryption-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require("chai").expect; 3 | const mongoose = require("mongoose"); 4 | const Promise = require("bluebird"); 5 | 6 | mongoose.Promise = Promise; 7 | mongoose.set("bufferCommands", false); 8 | 9 | const fieldEncryptionPlugin = require("../lib/mongoose-field-encryption").fieldEncryption; 10 | 11 | describe("Test fieldEncryption options behaviour", function () { 12 | before(function (done) { 13 | 14 | done(); 15 | }); 16 | 17 | it("Demonstrate notifyDecryptFails: false - inhibit error, and return empty value", function (done) { 18 | const FieldEncryptionSchema = new mongoose.Schema({ 19 | noEncrypt: { type: String }, 20 | toEncrypt1: { type: String }, 21 | toEncrypt2: { type: String }, 22 | }); 23 | 24 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 25 | fields: ["toEncrypt1", "toEncrypt2"], 26 | secret: "letsdothis", 27 | notifyDecryptFails: false, 28 | }); 29 | 30 | const FieldEncryptionOptionsTest1 = mongoose.model("FieldEncryptionOptionsTest1", FieldEncryptionSchema); 31 | // given 32 | const sut = new FieldEncryptionOptionsTest1({ 33 | noEncrypt: "clear", 34 | toEncrypt1: "some stuff", 35 | toEncrypt2: "after exception", 36 | }); 37 | 38 | // when 39 | sut.encryptFieldsSync(); 40 | sut.toEncrypt1 = sut.toEncrypt1.substring(0, sut.toEncrypt1.length - 1); 41 | 42 | // then 43 | sut.decryptFieldsSync(); 44 | expect(sut.noEncrypt).to.equal("clear"); 45 | expect(sut.__enc_noEncrypt).to.be.undefined; 46 | 47 | expect(sut.__enc_toEncrypt1).to.be.false; 48 | expect(sut.toEncrypt1).to.eql(""); 49 | 50 | expect(sut.__enc_toEncrypt2).to.be.false; 51 | expect(sut.toEncrypt2).to.eql("after exception"); 52 | done(); 53 | }); 54 | 55 | it("Demonstrate notifyDecryptFails: true (default) - throw error", function (done) { 56 | const FieldEncryptionSchema = new mongoose.Schema({ 57 | noEncrypt: { type: String }, 58 | toEncrypt1: { type: String }, 59 | toEncrypt2: { type: String }, 60 | }); 61 | 62 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 63 | fields: ["toEncrypt1", "toEncrypt2"], 64 | secret: "letsdothis", 65 | }); 66 | 67 | const FieldEncryptionOptionsTest2 = mongoose.model("FieldEncryptionOptionsTest2", FieldEncryptionSchema); 68 | // given 69 | const sut = new FieldEncryptionOptionsTest2({ 70 | noEncrypt: "clear", 71 | toEncrypt1: "some stuff", 72 | toEncrypt2: "after exception", 73 | }); 74 | 75 | // when 76 | sut.encryptFieldsSync(); 77 | sut.toEncrypt1 = sut.toEncrypt1.substring(0, sut.toEncrypt1.length - 1); 78 | 79 | // then 80 | try { 81 | sut.decryptFieldsSync(); 82 | } catch (err) { 83 | console.log(err); 84 | expect(err.reason).to.equal("wrong final block length"); 85 | done(); 86 | return; 87 | } 88 | 89 | done(new Error("should have thrown an exception")); 90 | }); 91 | 92 | it("Demonstrate encryptNull: false option", function (done) { 93 | const FieldEncryptionSchema = new mongoose.Schema({ 94 | noEncrypt: { type: String }, 95 | toEncrypt1: { type: String }, 96 | toEncrypt2: { type: String }, 97 | }); 98 | 99 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 100 | fields: ["toEncrypt1", "toEncrypt2"], 101 | secret: "letsdothis", 102 | encryptNull: false, 103 | }); 104 | 105 | const FieldEncryptionOptionsTest3 = mongoose.model("FieldEncryptionOptionsTest3", FieldEncryptionSchema); 106 | const sut = new FieldEncryptionOptionsTest3({ 107 | noEncrypt: "clear", 108 | toEncrypt1: "some stuff", 109 | toEncrypt2: null 110 | }); 111 | 112 | // when 113 | sut.encryptFieldsSync(); 114 | 115 | // then 116 | expect(sut.noEncrypt).to.equal("clear"); 117 | expect(sut.__enc_noEncrypt).to.be.undefined; 118 | 119 | expect(sut.__enc_toEncrypt1).to.be.true; 120 | expect(sut.toEncrypt1).to.not.eql("some stuff"); 121 | 122 | expect(sut.__enc_toEncrypt2).to.be.undefined; 123 | expect(sut.toEncrypt2).to.eql(null); 124 | done(); 125 | }); 126 | 127 | it("Demonstrate encryptNull: true option (Default behaviour)", function (done) { 128 | const FieldEncryptionSchema = new mongoose.Schema({ 129 | noEncrypt: { type: String }, 130 | toEncrypt1: { type: String }, 131 | toEncrypt2: { type: String }, 132 | }); 133 | 134 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 135 | fields: ["toEncrypt1", "toEncrypt2"], 136 | secret: "letsdothis", 137 | }); 138 | 139 | const FieldEncryptionOptionsTest4 = mongoose.model("FieldEncryptionOptionsTest4", FieldEncryptionSchema); 140 | const sut = new FieldEncryptionOptionsTest4({ 141 | noEncrypt: "clear", 142 | toEncrypt1: "some stuff", 143 | toEncrypt2: null 144 | }); 145 | 146 | // when 147 | sut.encryptFieldsSync(); 148 | 149 | // then 150 | expect(sut.noEncrypt).to.equal("clear"); 151 | expect(sut.__enc_noEncrypt).to.be.undefined; 152 | 153 | expect(sut.__enc_toEncrypt1).to.be.true; 154 | expect(sut.toEncrypt1).to.not.eql("some stuff"); 155 | 156 | expect(sut.__enc_toEncrypt2).to.be.true; 157 | expect(sut.toEncrypt2).to.be.undefined; 158 | expect(sut.__enc_toEncrypt2_d).to.exist; 159 | done(); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/test-manual-encryption.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require("crypto"); 4 | const expect = require("chai").expect; 5 | 6 | const { encrypt, decrypt } = require("../lib/mongoose-field-encryption"); 7 | 8 | describe("manual usage", function () { 9 | it("encrypt and decrypt text", function (done) { 10 | // given 11 | const password = "thisissomepassword"; 12 | const saltGenerator = (password) => password.substring(0, 16); 13 | const _hash = (secret) => crypto.createHash("sha256").update(secret).digest("hex").substring(0, 32); 14 | 15 | // when 16 | const encrypted = encrypt("some text", _hash(password), saltGenerator); 17 | const decrypted = decrypt(encrypted, _hash(password)); 18 | 19 | // then 20 | expect(encrypted).to.equal("61373334306262636435636339333336:fbd69a23d9123e8df17325618e6c23f8"); 21 | expect(decrypted).to.equal("some text"); 22 | 23 | done(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test-setup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require("chai").expect; 4 | 5 | const Promise = require("bluebird"); 6 | const mongoose = require("mongoose"); 7 | mongoose.Promise = Promise; 8 | 9 | const fieldEncryptionPlugin = require("../lib/mongoose-field-encryption").fieldEncryption; 10 | 11 | describe("mongoose-field-encryption plugin setup", function () { 12 | const FieldEncryptionSchema = new mongoose.Schema({ 13 | a: { type: String, required: true }, 14 | }); 15 | 16 | it("should not initialize plugin without a secret", function (done) { 17 | try { 18 | // when 19 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin); 20 | } catch (err) { 21 | // then 22 | expect(err.message).to.equal("missing required secret"); 23 | return done(); 24 | } 25 | 26 | expect.fail("Should not have initialized plugin"); 27 | done(); 28 | }); 29 | 30 | it("should initialize plugin without any fields", function (done) { 31 | // when 32 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 33 | secret: "icanhazcheezburger", 34 | }); 35 | 36 | // then 37 | done(); 38 | }); 39 | 40 | it("should initialize plugin with secret factory function", function (done) { 41 | // when 42 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 43 | secret: () => "icanhazcheezburger", 44 | }); 45 | 46 | // then 47 | done(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/test-statics.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const sinon = require("sinon"); 4 | const crypto = require("crypto"); 5 | const expect = require("chai").expect; 6 | 7 | const Promise = require("bluebird"); 8 | const mongoose = require("mongoose"); 9 | mongoose.Promise = Promise; 10 | 11 | const fieldEncryptionPlugin = require("../lib/mongoose-field-encryption").fieldEncryption; 12 | 13 | describe("mongoose-field-encryption plugin static methods", function () { 14 | describe("aes-256-cbc", function () { 15 | const FieldEncryptionSchema = new mongoose.Schema({ 16 | noEncrypt: { type: String, required: true }, 17 | toEncrypt1: { type: String, required: true }, 18 | toEncrypt2: { type: String, required: true }, 19 | toEncryptObject: { 20 | nested: { type: String }, 21 | }, 22 | }); 23 | 24 | FieldEncryptionSchema.plugin(fieldEncryptionPlugin, { 25 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 26 | secret: "letsdothis", // should ideally be process.env.SECRET 27 | }); 28 | 29 | const FieldEncryptionStaticsTest = mongoose.model("FieldEncryptionStaticsTest", FieldEncryptionSchema); 30 | 31 | it("should encrypt fields", function () { 32 | // given 33 | const sut = new FieldEncryptionStaticsTest({ 34 | noEncrypt: "clear", 35 | toEncrypt1: "some stuff", 36 | toEncrypt2: "should be hidden", 37 | toEncryptObject: { 38 | nested: "nested", 39 | }, 40 | }); 41 | 42 | // when 43 | sut.encryptFieldsSync(); 44 | 45 | // then 46 | expect(sut.noEncrypt).to.equal("clear"); 47 | expect(sut.__enc_noEncrypt).to.be.undefined; 48 | 49 | expect(sut.__enc_toEncrypt1).to.be.true; 50 | expect(sut.toEncrypt1).to.not.eql("some stuff"); 51 | 52 | expect(sut.__enc_toEncrypt2).to.be.true; 53 | expect(sut.toEncrypt2).to.not.eql("should be hidden"); 54 | 55 | expect(sut.__enc_toEncryptObject).to.be.true; 56 | expect(sut.toObject().toEncryptObject).to.be.undefined; 57 | 58 | sut.decryptFieldsSync(); 59 | expect(sut.__enc_toEncrypt1).to.be.false; 60 | expect(sut.noEncrypt).to.eql("clear"); 61 | expect(sut.toEncrypt1).to.eql("some stuff"); 62 | expect(sut.toEncrypt2).to.eql("should be hidden"); 63 | expect(sut.toEncryptObject.nested).to.eql("nested"); 64 | }); 65 | 66 | it("should not encrypt already encrypted fields", function () { 67 | // given 68 | const sut = new FieldEncryptionStaticsTest({ 69 | noEncrypt: "clear", 70 | toEncrypt1: "some stuff", 71 | toEncrypt2: "should be hidden", 72 | toEncryptObject: { 73 | nested: "nested", 74 | }, 75 | }); 76 | 77 | const createCipherivSpy = sinon.spy(crypto, "createCipheriv"); 78 | 79 | // when 80 | sut.encryptFieldsSync(); 81 | const encryptedFieldCount = createCipherivSpy.callCount; 82 | sut.encryptFieldsSync(); 83 | const encryptedFieldCountAfterTwoEncryptFieldCalls = createCipherivSpy.callCount; 84 | 85 | // then 86 | expect(encryptedFieldCount).to.eql(3); 87 | expect(encryptedFieldCountAfterTwoEncryptFieldCalls).to.eql(3); 88 | createCipherivSpy.restore(); 89 | }); 90 | 91 | it("should decrypt fields", function () { 92 | // given 93 | const sut = new FieldEncryptionStaticsTest({ 94 | noEncrypt: "clear", 95 | toEncrypt1: "test", 96 | toEncrypt2: "test2", 97 | toEncryptObject: { 98 | nested: "test3", 99 | }, 100 | }); 101 | 102 | sut.encryptFieldsSync(); 103 | expect(sut.toEncrypt1).not.to.eql("test"); 104 | expect(sut.toEncrypt2).not.to.eql("test2"); 105 | 106 | // when 107 | sut.decryptFieldsSync(); 108 | 109 | // then 110 | expect(sut.__enc_toEncrypt1).to.be.false; 111 | expect(sut.toEncrypt1).to.equal("test"); 112 | 113 | expect(sut.__enc_toEncrypt2).to.be.false; 114 | expect(sut.toEncrypt2).to.equal("test2"); 115 | 116 | expect(sut.__enc_toEncryptObject).to.be.false; 117 | expect(sut.__enc_toEncryptObject_d).to.equal(""); 118 | expect(sut.toEncryptObject.nested).to.equal("test3"); 119 | }); 120 | 121 | it("should ignore multiple decrypt field calls", function () { 122 | // given 123 | const sut = new FieldEncryptionStaticsTest({ 124 | noEncrypt: "clear", 125 | toEncrypt1: "test", 126 | toEncrypt2: "test2", 127 | toEncryptObject: { 128 | nested: "test3", 129 | }, 130 | }); 131 | 132 | sut.encryptFieldsSync(); 133 | const createDecipherivSpy = sinon.spy(crypto, "createDecipheriv"); 134 | 135 | // when 136 | sut.decryptFieldsSync(); 137 | const decryptionCount = createDecipherivSpy.callCount; 138 | sut.decryptFieldsSync(); 139 | const decryptionCountAfterTwoDecryptFieldCalls = createDecipherivSpy.callCount; 140 | 141 | // then 142 | expect(decryptionCount).to.eql(3); 143 | expect(decryptionCountAfterTwoDecryptFieldCalls); 144 | }); 145 | 146 | it("should strip encryption field markers", function () { 147 | // given 148 | const sut = new FieldEncryptionStaticsTest({ 149 | noEncrypt: "clear", 150 | toEncrypt1: "blah", 151 | __enc_toEncrypt1: false, 152 | toEncrypt2: "yo", 153 | __enc_toEncrypt2: false, 154 | toEncryptObject: { 155 | nested: "nested", 156 | }, 157 | __enc_toEncryptObject: false, 158 | __enc_toEncryptObject_d: "", 159 | }); 160 | 161 | // when 162 | sut.stripEncryptionFieldMarkers(); 163 | 164 | // then 165 | expect(sut.__enc_toEncrypt1).to.be.undefined; 166 | expect(sut.toEncrypt1).to.equal("blah"); 167 | 168 | expect(sut.__enc_toEncrypt2).to.be.undefined; 169 | expect(sut.toEncrypt2).to.equal("yo"); 170 | 171 | expect(sut.__enc_toEncryptObject).to.be.undefined; 172 | expect(sut.__enc_toEncryptObject_d).to.be.undefined; 173 | expect(JSON.stringify(sut.toEncryptObject)).to.equal('{"nested":"nested"}'); 174 | }); 175 | }); 176 | 177 | describe("aes-256-cbc with custom salt", function () { 178 | it("should encrypt with a custom string salt", function () { 179 | // given 180 | const FieldEncryptionEncryptCustomSaltStringSchema = new mongoose.Schema({ 181 | noEncrypt: { type: String, required: true }, 182 | toEncrypt1: { type: String, required: true }, 183 | toEncrypt2: { type: String, required: true }, 184 | toEncryptObject: { 185 | nested: { type: String }, 186 | }, 187 | }); 188 | 189 | FieldEncryptionEncryptCustomSaltStringSchema.plugin(fieldEncryptionPlugin, { 190 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 191 | secret: "letsdothis", // should ideally be process.env.SECRET 192 | saltGenerator: function (secret) { 193 | return "1234567890123456"; 194 | }, 195 | }); 196 | 197 | const FieldEncryptionEncryptCustomSaltStringTest = mongoose.model( 198 | "FieldEncryptionEncryptCustomSaltStringTest", 199 | FieldEncryptionEncryptCustomSaltStringSchema 200 | ); 201 | 202 | const sut = new FieldEncryptionEncryptCustomSaltStringTest({ 203 | noEncrypt: "clear", 204 | toEncrypt1: "some stuff", 205 | toEncrypt2: "should be hidden", 206 | toEncryptObject: { 207 | nested: "nested", 208 | }, 209 | }); 210 | 211 | // when 212 | sut.encryptFieldsSync(); 213 | 214 | // then 215 | expect(sut.noEncrypt).to.equal("clear"); 216 | expect(sut.__enc_noEncrypt).to.be.undefined; 217 | 218 | expect(sut.__enc_toEncrypt1).to.be.true; 219 | expect(sut.toEncrypt1).to.equal("31323334353637383930313233343536:5c568b1a61b7d1c61d93ce7523d29007"); 220 | 221 | expect(sut.__enc_toEncrypt2).to.be.true; 222 | expect(sut.toEncrypt2).to.equal( 223 | "31323334353637383930313233343536:2c81e40fc9c00edc33c857a0720fb9c50b5865f803dad888f251478ffa60135d" 224 | ); 225 | 226 | expect(sut.__enc_toEncryptObject).to.be.true; 227 | expect(sut.__enc_toEncryptObject_d).to.equal( 228 | "31323334353637383930313233343536:d95f119990acab8c08f82a6b1c49ff856e6049d29ca1f96a794923c28d8e5baa" 229 | ); 230 | 231 | sut.decryptFieldsSync(); 232 | expect(sut.__enc_toEncrypt1).to.be.false; 233 | expect(sut.noEncrypt).to.eql("clear"); 234 | expect(sut.toEncrypt1).to.eql("some stuff"); 235 | expect(sut.toEncrypt2).to.eql("should be hidden"); 236 | expect(sut.toEncryptObject.nested).to.eql("nested"); 237 | }); 238 | 239 | it("should decrypt with a custom string salt", function () { 240 | // given 241 | const FieldEncryptionDecryptCustomSaltStringSchema = new mongoose.Schema({ 242 | noEncrypt: { type: String, required: true }, 243 | toEncrypt1: { type: String, required: true }, 244 | toEncrypt2: { type: String, required: true }, 245 | toEncryptObject: { 246 | nested: { type: String }, 247 | }, 248 | }); 249 | 250 | FieldEncryptionDecryptCustomSaltStringSchema.plugin(fieldEncryptionPlugin, { 251 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 252 | secret: "letsdothis", // should ideally be process.env.SECRET 253 | saltGenerator: function (secret) { 254 | return "1234567890123456"; 255 | }, 256 | }); 257 | 258 | const FieldEncryptionDecryptCustomSaltStringTest = mongoose.model( 259 | "FieldEncryptionDecryptCustomSaltStringTest", 260 | FieldEncryptionDecryptCustomSaltStringSchema 261 | ); 262 | 263 | const sut = new FieldEncryptionDecryptCustomSaltStringTest({ 264 | _id: "5c73c4f17841f3557c130bab", 265 | noEncrypt: "clear", 266 | toEncrypt1: "31323334353637383930313233343536:5c568b1a61b7d1c61d93ce7523d29007", 267 | toEncrypt2: "31323334353637383930313233343536:2c81e40fc9c00edc33c857a0720fb9c50b5865f803dad888f251478ffa60135d", 268 | __enc_toEncrypt1: true, 269 | __enc_toEncrypt2: true, 270 | __enc_toEncryptObject_d: 271 | "31323334353637383930313233343536:d95f119990acab8c08f82a6b1c49ff856e6049d29ca1f96a794923c28d8e5baa", 272 | __enc_toEncryptObject: true, 273 | }); 274 | 275 | // when 276 | sut.decryptFieldsSync(); 277 | 278 | // then 279 | expect(sut.__enc_toEncrypt1).to.be.false; 280 | expect(sut.noEncrypt).to.eql("clear"); 281 | expect(sut.toEncrypt1).to.eql("some stuff"); 282 | expect(sut.toEncrypt2).to.eql("should be hidden"); 283 | expect(sut.toEncryptObject.nested).to.eql("nested"); 284 | }); 285 | 286 | it("should throw an error when encrypting fields with an invalid custom string salt", function () { 287 | // given 288 | const FieldEncryptionCustomSaltBadStringSchema = new mongoose.Schema({ 289 | noEncrypt: { type: String, required: true }, 290 | toEncrypt1: { type: String, required: true }, 291 | toEncrypt2: { type: String, required: true }, 292 | toEncryptObject: { 293 | nested: { type: String }, 294 | }, 295 | }); 296 | 297 | FieldEncryptionCustomSaltBadStringSchema.plugin(fieldEncryptionPlugin, { 298 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 299 | secret: "letsdothis", // should ideally be process.env.SECRET 300 | saltGenerator: function (secret) { 301 | return "123456789012345"; // only 15 chars 302 | }, 303 | }); 304 | 305 | const FieldEncryptionCustomSaltBadStringTest = mongoose.model( 306 | "FieldEncryptionCustomSaltBadStringTest", 307 | FieldEncryptionCustomSaltBadStringSchema 308 | ); 309 | 310 | const sut = new FieldEncryptionCustomSaltBadStringTest({ 311 | noEncrypt: "clear", 312 | toEncrypt1: "some stuff", 313 | toEncrypt2: "should be hidden", 314 | toEncryptObject: { 315 | nested: "nested", 316 | }, 317 | }); 318 | 319 | // when 320 | try { 321 | sut.encryptFieldsSync(); 322 | } catch (err) { 323 | // then 324 | if (err.message && err.message === "Invalid salt, please ensure that the salt is a string of length 16") { 325 | return; 326 | } 327 | } 328 | 329 | throw new Error("Should not have encrypted using a bad iv"); 330 | }); 331 | 332 | it("should throw an error when encrypting fields with an invalid custom buffer salt", function () { 333 | // given 334 | const FieldEncryptionCustomSaltBadBufferSchema = new mongoose.Schema({ 335 | noEncrypt: { type: String, required: true }, 336 | toEncrypt1: { type: String, required: true }, 337 | toEncrypt2: { type: String, required: true }, 338 | toEncryptObject: { 339 | nested: { type: String }, 340 | }, 341 | }); 342 | 343 | FieldEncryptionCustomSaltBadBufferSchema.plugin(fieldEncryptionPlugin, { 344 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 345 | secret: "letsdothis", // should ideally be process.env.SECRET 346 | saltGenerator: function (secret) { 347 | return crypto.randomBytes(200); 348 | }, 349 | }); 350 | 351 | const FieldEncryptionCustomSaltBadBufferTest = mongoose.model( 352 | "FieldEncryptionCustomSaltBadBufferTest", 353 | FieldEncryptionCustomSaltBadBufferSchema 354 | ); 355 | 356 | const sut = new FieldEncryptionCustomSaltBadBufferTest({ 357 | noEncrypt: "clear", 358 | toEncrypt1: "some stuff", 359 | toEncrypt2: "should be hidden", 360 | toEncryptObject: { 361 | nested: "nested", 362 | }, 363 | }); 364 | 365 | // when 366 | try { 367 | sut.encryptFieldsSync(); 368 | } catch (err) { 369 | // then 370 | if ( 371 | err.message && 372 | err.message === "Invalid salt provided, please ensure that the salt is a Buffer of length 16" 373 | ) { 374 | return; 375 | } 376 | } 377 | 378 | throw new Error("Should not have encrypted using a bad iv"); 379 | }); 380 | 381 | it("should throw an error when encrypting fields with an invalid custom salt", function () { 382 | // given 383 | const FieldEncryptionCustomBadSaltSchema = new mongoose.Schema({ 384 | noEncrypt: { type: String, required: true }, 385 | toEncrypt1: { type: String, required: true }, 386 | toEncrypt2: { type: String, required: true }, 387 | toEncryptObject: { 388 | nested: { type: String }, 389 | }, 390 | }); 391 | 392 | FieldEncryptionCustomBadSaltSchema.plugin(fieldEncryptionPlugin, { 393 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 394 | secret: "letsdothis", // should ideally be process.env.SECRET 395 | saltGenerator: function (secret) { 396 | return { salt: secret }; 397 | }, 398 | }); 399 | 400 | const FieldEncryptionCustomBadSaltTest = mongoose.model( 401 | "FieldEncryptionCustomBadSaltTest", 402 | FieldEncryptionCustomBadSaltSchema 403 | ); 404 | 405 | const sut = new FieldEncryptionCustomBadSaltTest({ 406 | noEncrypt: "clear", 407 | toEncrypt1: "some stuff", 408 | toEncrypt2: "should be hidden", 409 | toEncryptObject: { 410 | nested: "nested", 411 | }, 412 | }); 413 | 414 | // when 415 | try { 416 | sut.encryptFieldsSync(); 417 | } catch (err) { 418 | // then 419 | if ( 420 | err.message && 421 | err.message === "Invalid salt, please ensure that the salt is either a string or a Buffer of length 16" 422 | ) { 423 | return; 424 | } 425 | } 426 | 427 | throw new Error("Should not have encrypted using a bad iv"); 428 | }); 429 | }); 430 | 431 | describe("aes-256-cbc with a synchronous secret function", function () { 432 | it("should encrypt with a synchronous secret function", function () { 433 | // given 434 | const FieldEncryptionEncryptSynchronousSecretFunctionSchema = new mongoose.Schema({ 435 | noEncrypt: { type: String, required: true }, 436 | toEncrypt1: { type: String, required: true }, 437 | toEncrypt2: { type: String, required: true }, 438 | toEncryptObject: { 439 | nested: { type: String }, 440 | }, 441 | }); 442 | 443 | FieldEncryptionEncryptSynchronousSecretFunctionSchema.plugin(fieldEncryptionPlugin, { 444 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 445 | secret: function () { 446 | return "letsdothis"; 447 | }, // should ideally be process.env.SECRET 448 | saltGenerator: function (secret) { 449 | return "1234567890123456"; 450 | }, 451 | }); 452 | 453 | const FieldEncryptionEncryptSynchronousSecretFunctionTest = mongoose.model( 454 | "FieldEncryptionEncryptSynchronousSecretFunctionTest", 455 | FieldEncryptionEncryptSynchronousSecretFunctionSchema 456 | ); 457 | 458 | const sut = new FieldEncryptionEncryptSynchronousSecretFunctionTest({ 459 | noEncrypt: "clear", 460 | toEncrypt1: "some stuff", 461 | toEncrypt2: "should be hidden", 462 | toEncryptObject: { 463 | nested: "nested", 464 | }, 465 | }); 466 | 467 | // when 468 | sut.encryptFieldsSync(); 469 | 470 | // then 471 | expect(sut.noEncrypt).to.equal("clear"); 472 | expect(sut.__enc_noEncrypt).to.be.undefined; 473 | 474 | expect(sut.__enc_toEncrypt1).to.be.true; 475 | expect(sut.toEncrypt1).to.equal("31323334353637383930313233343536:5c568b1a61b7d1c61d93ce7523d29007"); 476 | 477 | expect(sut.__enc_toEncrypt2).to.be.true; 478 | expect(sut.toEncrypt2).to.equal( 479 | "31323334353637383930313233343536:2c81e40fc9c00edc33c857a0720fb9c50b5865f803dad888f251478ffa60135d" 480 | ); 481 | 482 | expect(sut.__enc_toEncryptObject).to.be.true; 483 | expect(sut.__enc_toEncryptObject_d).to.equal( 484 | "31323334353637383930313233343536:d95f119990acab8c08f82a6b1c49ff856e6049d29ca1f96a794923c28d8e5baa" 485 | ); 486 | 487 | sut.decryptFieldsSync(); 488 | expect(sut.__enc_toEncrypt1).to.be.false; 489 | expect(sut.noEncrypt).to.eql("clear"); 490 | expect(sut.toEncrypt1).to.eql("some stuff"); 491 | expect(sut.toEncrypt2).to.eql("should be hidden"); 492 | expect(sut.toEncryptObject.nested).to.eql("nested"); 493 | }); 494 | 495 | it("should decrypt with a synchronous secret function", function () { 496 | // given 497 | const FieldEncryptionDecryptSynchronousSecretFunctionSchema = new mongoose.Schema({ 498 | noEncrypt: { type: String, required: true }, 499 | toEncrypt1: { type: String, required: true }, 500 | toEncrypt2: { type: String, required: true }, 501 | toEncryptObject: { 502 | nested: { type: String }, 503 | }, 504 | }); 505 | 506 | FieldEncryptionDecryptSynchronousSecretFunctionSchema.plugin(fieldEncryptionPlugin, { 507 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 508 | secret: function () { 509 | return "letsdothis"; 510 | }, // should ideally be process.env.SECRET 511 | saltGenerator: function (secret) { 512 | return "1234567890123456"; 513 | }, 514 | }); 515 | 516 | const FieldEncryptionDecryptSynchronousSecretFunctionTest = mongoose.model( 517 | "FieldEncryptionDecryptSynchronousSecretFunctionTest", 518 | FieldEncryptionDecryptSynchronousSecretFunctionSchema 519 | ); 520 | 521 | const sut = new FieldEncryptionDecryptSynchronousSecretFunctionTest({ 522 | _id: "5c73c4f17841f3557c130bab", 523 | noEncrypt: "clear", 524 | toEncrypt1: "31323334353637383930313233343536:5c568b1a61b7d1c61d93ce7523d29007", 525 | toEncrypt2: "31323334353637383930313233343536:2c81e40fc9c00edc33c857a0720fb9c50b5865f803dad888f251478ffa60135d", 526 | __enc_toEncrypt1: true, 527 | __enc_toEncrypt2: true, 528 | __enc_toEncryptObject_d: 529 | "31323334353637383930313233343536:d95f119990acab8c08f82a6b1c49ff856e6049d29ca1f96a794923c28d8e5baa", 530 | __enc_toEncryptObject: true, 531 | }); 532 | 533 | // when 534 | sut.decryptFieldsSync(); 535 | 536 | // then 537 | expect(sut.__enc_toEncrypt1).to.be.false; 538 | expect(sut.noEncrypt).to.eql("clear"); 539 | expect(sut.toEncrypt1).to.eql("some stuff"); 540 | expect(sut.toEncrypt2).to.eql("should be hidden"); 541 | expect(sut.toEncryptObject.nested).to.eql("nested"); 542 | }); 543 | 544 | it("should throw an error when encrypting fields with an invalid custom secret function", function () { 545 | // given 546 | const FieldEncryptionBadSecretFunctionSchema = new mongoose.Schema({ 547 | noEncrypt: { type: String, required: true }, 548 | toEncrypt1: { type: String, required: true }, 549 | toEncrypt2: { type: String, required: true }, 550 | toEncryptObject: { 551 | nested: { type: String }, 552 | }, 553 | }); 554 | 555 | FieldEncryptionBadSecretFunctionSchema.plugin(fieldEncryptionPlugin, { 556 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 557 | secret: function () { 558 | throw new Error("foobar"); 559 | }, // should ideally be process.env.SECRET 560 | }); 561 | 562 | const FieldEncryptionBadSecretFunctionTest = mongoose.model( 563 | "FieldEncryptionBadSecretFunctionTest", 564 | FieldEncryptionBadSecretFunctionSchema 565 | ); 566 | 567 | const sut = new FieldEncryptionBadSecretFunctionTest({ 568 | noEncrypt: "clear", 569 | toEncrypt1: "some stuff", 570 | toEncrypt2: "should be hidden", 571 | toEncryptObject: { 572 | nested: "nested", 573 | }, 574 | }); 575 | 576 | // when 577 | try { 578 | sut.encryptFieldsSync(); 579 | } catch (err) { 580 | // then 581 | if (err.message && err.message === "foobar") { 582 | return; 583 | } 584 | } 585 | 586 | throw new Error("Should not have encrypted using a bad secret function"); 587 | }); 588 | }); 589 | 590 | describe("aes-256-ctr (deprecated)", function () { 591 | const FieldEncryptionSchemaDeprecated = new mongoose.Schema({ 592 | noEncrypt: { type: String, required: true }, 593 | toEncrypt1: { type: String, required: true }, 594 | toEncrypt2: { type: String, required: true }, 595 | toEncryptObject: { 596 | nested: { type: String }, 597 | }, 598 | }); 599 | 600 | FieldEncryptionSchemaDeprecated.plugin(fieldEncryptionPlugin, { 601 | fields: ["toEncrypt1", "toEncrypt2", "toEncryptObject"], 602 | secret: "letsdothis", // should ideally be process.env.SECRET 603 | useAes256Ctr: true, 604 | }); 605 | 606 | const FieldEncryptionStaticsTestDeprecated = mongoose.model( 607 | "FieldEncryptionStaticsTestDeprecated", 608 | FieldEncryptionSchemaDeprecated 609 | ); 610 | 611 | it("should encrypt and decrypt fields", function () { 612 | // given 613 | const sut = new FieldEncryptionStaticsTestDeprecated({ 614 | noEncrypt: "clear", 615 | toEncrypt1: "some stuff", 616 | toEncrypt2: "should be hidden", 617 | toEncryptObject: { 618 | nested: "nested", 619 | }, 620 | }); 621 | 622 | // when 623 | sut.encryptFieldsSync(); 624 | 625 | // then 626 | expect(sut.noEncrypt).to.equal("clear"); 627 | expect(sut.__enc_noEncrypt).to.be.undefined; 628 | 629 | expect(sut.__enc_toEncrypt1).to.be.true; 630 | expect(sut.toEncrypt1).to.eql("b27d5768b82263ece8bd"); 631 | 632 | expect(sut.__enc_toEncrypt2).to.be.true; 633 | expect(sut.toEncrypt2).to.eql("b27a5578f43537fbebfb2e365ab13977"); 634 | 635 | expect(sut.__enc_toEncryptObject).to.be.true; 636 | expect(sut.__enc_toEncryptObject_d).to.eql("ba305468eb2572fdace164315ba6287c6f4181"); 637 | 638 | sut.decryptFieldsSync(); 639 | expect(sut.__enc_toEncrypt1).to.be.false; 640 | expect(sut.noEncrypt).to.eql("clear"); 641 | expect(sut.toEncrypt1).to.eql("some stuff"); 642 | expect(sut.toEncrypt2).to.eql("should be hidden"); 643 | expect(sut.toEncryptObject.nested).to.eql("nested"); 644 | }); 645 | }); 646 | }); 647 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "lib/*.js" 4 | ], 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "checkJs": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "outDir": "dist" 11 | } 12 | } --------------------------------------------------------------------------------