UNPKG

mongoose-encryption

Version:

Simple encryption and authentication plugin for Mongoose

1,319 lines (1,100 loc) 83.8 kB
mongoose = require 'mongoose' bufferEqual = require 'buffer-equal-constant-time' sinon = require 'sinon' chai = require 'chai' assert = chai.assert mongoose.connect 'mongodb://localhost/mongoose-encryption-test' encryptionKey = 'CwBDwGUwoM5YzBmzwWPSI+KjBKvWHaablbrEiDYh43Q=' signingKey = 'dLBm74RU4NW3e2i3QSifZDNXIXBd54yr7mZp0LKugVUa1X1UP9qoxoa3xfA7Ea4kdVL+JsPg9boGfREbPCb+kw==' secret = 'correct horse battery staple CtYC/wFXnLQ1Dq8lYZSbnDuz8fTYMALPfgCqdgtpcrc' encrypt = require '../index.js' BasicEncryptedModel = null BasicEncryptedModelSchema = mongoose.Schema text: type: String bool: type: Boolean num: type: Number date: type: Date id2: type: mongoose.Schema.Types.ObjectId arr: [ type: String ] mix: type: mongoose.Schema.Types.Mixed buf: type: Buffer idx: type: String, index: true BasicEncryptedModelSchema.plugin encrypt, secret: secret BasicEncryptedModel = mongoose.model 'Simple', BasicEncryptedModelSchema describe 'encrypt plugin', -> it 'should add field _ct of type Buffer to the schema', -> encryptedSchema = mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.property encryptedSchema.paths, '_ct' assert.propertyVal encryptedSchema.paths._ct, 'instance', 'Buffer' it 'should add field _ac of type Buffer to the schema', -> encryptedSchema = mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.property encryptedSchema.paths, '_ac' assert.propertyVal encryptedSchema.paths._ac, 'instance', 'Buffer' it 'should expose an encrypt method on documents', -> EncryptFnTestModel = mongoose.model 'EncryptFnTest', mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.isFunction (new EncryptFnTestModel).encrypt it 'should expose a decrypt method on documents', -> DecryptFnTestModel = mongoose.model 'DecryptFnTest', mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.isFunction (new DecryptFnTestModel).decrypt it 'should expose a decryptSync method on documents', -> DecryptSyncFnTestModel = mongoose.model 'DecryptSyncFnTest', mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.isFunction (new DecryptSyncFnTestModel).decryptSync it 'should expose a sign method on documents', -> SignFnTestModel = mongoose.model 'SignFnTest', mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.isFunction (new SignFnTestModel).sign it 'should expose a authenticateSync method on documents', -> AuthenticateSyncFnTestModel = mongoose.model 'AuthenticateSyncFnTest', mongoose.Schema({}).plugin(encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'test') assert.isFunction (new AuthenticateSyncFnTestModel).authenticateSync it 'should throw an error if installed twice on the same schema', -> EncryptedSchema = mongoose.Schema text: type: String EncryptedSchema.plugin encrypt, secret: secret assert.throw -> EncryptedSchema.plugin encrypt, secret: secret describe 'new EncryptedModel', -> it 'should remain unaltered', (done) -> simpleTestDoc1 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' assert.propertyVal simpleTestDoc1, 'text', 'Unencrypted text' assert.propertyVal simpleTestDoc1, 'bool', true assert.propertyVal simpleTestDoc1, 'num', 42 assert.property simpleTestDoc1, 'date' assert.equal simpleTestDoc1.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal simpleTestDoc1.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf simpleTestDoc1.arr, 2 assert.equal simpleTestDoc1.arr[0], 'alpha' assert.equal simpleTestDoc1.arr[1], 'bravo' assert.property simpleTestDoc1, 'mix' assert.deepEqual simpleTestDoc1.mix, { str: 'A string', bool: false } assert.property simpleTestDoc1, 'buf' assert.equal simpleTestDoc1.buf.toString(), 'abcdefg' assert.property simpleTestDoc1, '_id' assert.notProperty simpleTestDoc1, '_ct' done() describe 'document.save()', -> before -> @sandbox = sinon.sandbox.create() @sandbox.spy BasicEncryptedModel.prototype, 'sign' @sandbox.spy BasicEncryptedModel.prototype, 'encrypt' @sandbox.spy BasicEncryptedModel.prototype, 'decryptSync' after -> @sandbox.restore() beforeEach (done) -> BasicEncryptedModel.prototype.sign.reset() BasicEncryptedModel.prototype.encrypt.reset() BasicEncryptedModel.prototype.decryptSync.reset() @simpleTestDoc2 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' @simpleTestDoc2.save (err) => assert.equal err, null done() afterEach (done) -> @simpleTestDoc2.remove (err) -> assert.equal err, null done() it 'saves encrypted fields', (done) -> BasicEncryptedModel.find _id: @simpleTestDoc2._id _ct: $exists: true text: $exists: false bool: $exists: false num: $exists: false date: $exists: false id2: $exists: false arr: $exists: false mix: $exists: false buf: $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 done() return it 'returns decrypted data after save', (done) -> @simpleTestDoc2.save (err, doc) -> return done(err) if err try assert.equal doc._ct, undefined assert.equal doc._ac, undefined assert.equal doc.text, 'Unencrypted text' assert.equal doc.bool, true assert.equal doc.num, 42 assert.deepEqual doc.date, new Date('2014-05-19T16:39:07.536Z') assert.equal doc.id2, '5303e65d34e1e80d7a7ce212' assert.equal doc.arr.toString(), ['alpha', 'bravo'].toString() assert.deepEqual doc.mix, { str: 'A string', bool: false } assert.deepEqual doc.buf, new Buffer 'abcdefg' done() catch err done err it 'should have called encryptSync then authenticateSync then decryptSync', -> assert.equal @simpleTestDoc2.sign.callCount, 1 assert.equal @simpleTestDoc2.encrypt.callCount, 1 assert.equal @simpleTestDoc2.decryptSync.callCount, 1 assert @simpleTestDoc2.encrypt.calledBefore @simpleTestDoc2.decryptSync assert @simpleTestDoc2.encrypt.calledBefore @simpleTestDoc2.sign, 'encrypted before signed' assert @simpleTestDoc2.sign.calledBefore @simpleTestDoc2.decryptSync, 'signed before decrypted' describe 'document.save() on encrypted document which contains nesting', -> before -> @schemaWithNest = mongoose.Schema nest: birdColor: type: String areBirdsPretty: type: Boolean @schemaWithNest.plugin encrypt, secret: secret @ModelWithNest = mongoose.model 'SimpleNest', @schemaWithNest beforeEach (done) -> @nestTestDoc = new @ModelWithNest nest: birdColor: 'blue' areBirdsPretty: true @nestTestDoc.save (err) -> assert.equal err, null done() afterEach (done) -> @nestTestDoc.remove (err) -> assert.equal err, null done() it 'encrypts nested fields', (done) -> @ModelWithNest.find( _id: @nestTestDoc._id _ct: $exists: true nest: $exists: false ).lean().exec (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 done() it 'saves encrypted fields', (done) -> @ModelWithNest.find _id: @nestTestDoc._id _ct: $exists: true , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.isObject docs[0].nest assert.propertyVal docs[0].nest, 'birdColor', 'blue' assert.propertyVal docs[0].nest, 'areBirdsPretty', true done() return describe 'document.save() on encrypted nested document', -> before -> @schema = mongoose.Schema birdColor: type: String areBirdsPretty: type: Boolean @schema.plugin encrypt, secret: secret, collectionId: 'schema', encryptedFields: ['birdColor'] @schemaWithNest = mongoose.Schema nest: @schema @ModelWithNest = mongoose.model 'SimpleNestedBird', @schemaWithNest beforeEach (done) -> @nestTestDoc = new @ModelWithNest nest: birdColor: 'blue' areBirdsPretty: true @nestTestDoc.save (err, doc) -> assert.equal err, null done() afterEach (done) -> @nestTestDoc.remove (err) -> assert.equal err, null done() it 'encrypts nested fields', (done) -> @ModelWithNest.find( _id: @nestTestDoc._id 'nest._ct': $exists: true 'nest.birdColor': $exists: false ).lean().exec (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 done() it 'saves encrypted fields', (done) -> @ModelWithNest.find _id: @nestTestDoc._id 'nest._ct': $exists: true , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.isObject docs[0].nest assert.propertyVal docs[0].nest, 'birdColor', 'blue' assert.propertyVal docs[0].nest, 'areBirdsPretty', true done() return describe 'document.save() when only certain fields are encrypted', -> before -> PartiallyEncryptedModelSchema = mongoose.Schema encryptedText: type: String unencryptedText: type: String PartiallyEncryptedModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'PartiallyEncrypted', encryptedFields: ['encryptedText'] @PartiallyEncryptedModel = mongoose.model 'PartiallyEncrypted', PartiallyEncryptedModelSchema beforeEach (done) -> @partiallyEncryptedDoc = new @PartiallyEncryptedModel encryptedText: 'Encrypted Text' unencryptedText: 'Unencrypted Text' @partiallyEncryptedDoc.save (err) -> assert.equal err, null done() afterEach (done) -> @partiallyEncryptedDoc.remove (err) -> assert.equal err, null done() it 'should have decrypted fields', -> assert.equal @partiallyEncryptedDoc.encryptedText, 'Encrypted Text' assert.propertyVal @partiallyEncryptedDoc, 'unencryptedText', 'Unencrypted Text' it 'should have encrypted fields undefined when encrypt is called', (done) -> @partiallyEncryptedDoc.encrypt => assert.equal @partiallyEncryptedDoc.encryptedText, undefined assert.propertyVal @partiallyEncryptedDoc, 'unencryptedText', 'Unencrypted Text' done() it 'should have a field _ct containing a mongoose Buffer object which appears encrypted when encrypted', (done) -> @partiallyEncryptedDoc.encrypt => assert.isObject @partiallyEncryptedDoc._ct assert.property @partiallyEncryptedDoc.toObject()._ct, 'buffer' assert.instanceOf @partiallyEncryptedDoc.toObject()._ct.buffer, Buffer assert.isString @partiallyEncryptedDoc.toObject()._ct.toString(), 'ciphertext can be converted to a string' assert.throw -> JSON.parse @partiallyEncryptedDoc.toObject()._ct.toString(), 'ciphertext is not parsable json' done() it 'should not overwrite _ct or _ac when saved after a find that didnt retrieve _ct or _ac', (done) -> @PartiallyEncryptedModel.findById(@partiallyEncryptedDoc).select('unencryptedText').exec (err, doc) => assert.equal err, null assert.equal doc._ct, undefined assert.equal doc._ac, undefined assert.propertyVal doc, 'unencryptedText', 'Unencrypted Text', 'selected unencrypted fields should be found' doc.save (err) => assert.equal err, null @PartiallyEncryptedModel.findById(@partiallyEncryptedDoc).select('unencryptedText _ct _ac').exec (err, finalDoc) -> assert.equal err, null assert.equal finalDoc._ct, undefined assert.propertyVal finalDoc, 'unencryptedText', 'Unencrypted Text', 'selected unencrypted fields should still be found after the select -> save' assert.propertyVal finalDoc, 'encryptedText', 'Encrypted Text', 'encrypted fields werent overwritten during the select -> save' done() describe 'EncryptedModel.create()', -> beforeEach -> @docContents = text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' afterEach (done) -> BasicEncryptedModel.remove (err) -> assert.equal err, null done() return it 'when doc created, it should pass an unencrypted version to the callback', (done) -> BasicEncryptedModel.create @docContents, (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.propertyVal doc, 'bool', true assert.propertyVal doc, 'num', 42 assert.property doc, 'date' assert.equal doc.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal doc.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf doc.arr, 2 assert.equal doc.arr[0], 'alpha' assert.equal doc.arr[1], 'bravo' assert.property doc, 'mix' assert.deepEqual doc.mix, { str: 'A string', bool: false } assert.property doc, 'buf' assert.equal doc.buf.toString(), 'abcdefg' assert.property doc, '_id' assert.notProperty doc, '_ct' done() it 'after doc created, should be encrypted in db', (done) -> BasicEncryptedModel.create @docContents, (err, doc) -> assert.equal err, null assert.ok doc._id BasicEncryptedModel.find _id: doc._id _ct: $exists: true text: $exists: false bool: $exists: false num: $exists: false date: $exists: false id2: $exists: false arr: $exists: false mix: $exists: false buf: $exists: false , (err, docs) -> assert.lengthOf docs, 1 done err describe 'EncryptedModel.find()', -> simpleTestDoc3 = null before (done) -> @sandbox = sinon.sandbox.create() @sandbox.spy BasicEncryptedModel.prototype, 'authenticateSync' @sandbox.spy BasicEncryptedModel.prototype, 'decryptSync' simpleTestDoc3 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' simpleTestDoc3.save (err) -> assert.equal err, null done() beforeEach -> BasicEncryptedModel.prototype.authenticateSync.reset() BasicEncryptedModel.prototype.decryptSync.reset() after (done) -> @sandbox.restore() simpleTestDoc3.remove (err) -> assert.equal err, null done() it 'when doc found, should pass an unencrypted version to the callback', (done) -> BasicEncryptedModel.findById simpleTestDoc3._id, (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.propertyVal doc, 'bool', true assert.propertyVal doc, 'num', 42 assert.property doc, 'date' assert.equal doc.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal doc.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf doc.arr, 2 assert.equal doc.arr[0], 'alpha' assert.equal doc.arr[1], 'bravo' assert.property doc, 'mix' assert.deepEqual doc.mix, { str: 'A string', bool: false } assert.property doc, 'buf' assert.equal doc.buf.toString(), 'abcdefg' assert.property doc, '_id' assert.notProperty doc, '_ct' done() return it 'when doc not found by id, should pass null to the callback', (done) -> BasicEncryptedModel.findById '534ec48d60069bc13338b354', (err, doc) -> assert.equal err, null assert.equal doc, null done() return it 'when doc not found by query, should pass [] to the callback', (done) -> BasicEncryptedModel.find text: 'banana', (err, doc) -> assert.equal err, null assert.isArray doc assert.lengthOf doc, 0 done() return it 'should have called authenticateSync then decryptSync', (done) -> BasicEncryptedModel.findById simpleTestDoc3._id, (err, doc) -> assert.equal err, null assert.ok doc assert.equal doc.authenticateSync.callCount, 1 assert.equal doc.decryptSync.callCount, 1 assert doc.authenticateSync.calledBefore doc.decryptSync, 'authenticated before decrypted' done() return it 'if all authenticated fields selected, should not throw an error', (done) -> BasicEncryptedModel.findById(simpleTestDoc3._id).select('_ct _ac').exec (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.propertyVal doc, 'bool', true assert.propertyVal doc, 'num', 42 done() return it 'if only some authenticated fields selected, should throw an error', (done) -> BasicEncryptedModel.findById(simpleTestDoc3._id).select('_ct').exec (err, doc) -> assert.ok err BasicEncryptedModel.findById(simpleTestDoc3._id).select('_ac').exec (err, doc) -> assert.ok err done() return describe 'EncryptedModel.find() lean option', -> simpleTestDoc4 = null before (done) -> simpleTestDoc4 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' simpleTestDoc4.save (err) -> assert.equal err, null done() after (done) -> simpleTestDoc4.remove (err) -> assert.equal err, null done() it 'should have encrypted fields undefined on saved document', (done) -> BasicEncryptedModel.findById(simpleTestDoc4._id).lean().exec (err, doc) -> assert.equal doc.text, undefined assert.equal doc.bool, undefined assert.equal doc.num, undefined assert.equal doc.date, undefined assert.equal doc.id2, undefined assert.equal doc.arr, undefined assert.equal doc.mix, undefined assert.equal doc.buf, undefined done() it 'should have a field _ct containing a mongoose Buffer object which appears encrypted', (done) -> BasicEncryptedModel.findById(simpleTestDoc4._id).lean().exec (err, doc) -> assert.isObject doc._ct assert.property doc._ct, 'buffer' assert.instanceOf doc._ct.buffer, Buffer assert.isString doc._ct.toString(), 'ciphertext can be converted to a string' assert.throw -> JSON.parse doc._ct.toString(), 'ciphertext is not parsable json' done() describe 'document.encrypt()', -> simpleTestDoc5 = null beforeEach (done) -> simpleTestDoc5 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' idx: 'Indexed' simpleTestDoc5.encrypt (err) -> assert.equal err, null done() it 'should have encrypted fields undefined', (done) -> assert.equal simpleTestDoc5.text, undefined assert.equal simpleTestDoc5.bool, undefined assert.equal simpleTestDoc5.num, undefined assert.equal simpleTestDoc5.date, undefined assert.equal simpleTestDoc5.id2, undefined assert.equal simpleTestDoc5.arr, undefined assert.equal simpleTestDoc5.mix, undefined assert.equal simpleTestDoc5.buf, undefined done() it 'should not encrypt indexed fields by default', (done) -> assert.propertyVal simpleTestDoc5, 'idx', 'Indexed' done() it 'should have a field _ct containing a mongoose Buffer object which appears encrypted', (done) -> assert.isObject simpleTestDoc5._ct assert.property simpleTestDoc5.toObject()._ct, 'buffer' assert.instanceOf simpleTestDoc5.toObject()._ct.buffer, Buffer assert.isString simpleTestDoc5.toObject()._ct.toString(), 'ciphertext can be converted to a string' assert.throw -> JSON.parse simpleTestDoc5.toObject()._ct.toString(), 'ciphertext is not parsable json' done() it 'should have non-ascii characters in ciphertext as a result of encryption even if all input is ascii', (done) -> allAsciiDoc = new BasicEncryptedModel text: 'Unencrypted text' allAsciiDoc.encrypt (err) -> assert.equal err, null assert.notMatch allAsciiDoc.toObject()._ct.toString(), /^[\x00-\x7F]*$/ done() it 'should pass an error when called on a document which is already encrypted', (done) -> simpleTestDoc5.encrypt (err) -> assert.ok err done() describe 'document.decrypt()', -> beforeEach (done) -> @encryptedSimpleTestDoc = new BasicEncryptedModel _id: '584b1e7de752fcf3be8cd086' idx: 'Indexed' _ct: new Buffer("610bbddbf35455e9a4fcf2428bb6cd68f39fdaece7e851cb213b1be81b10559d1af6d7c205752d2a6620100871d0e" + "95d3609d4ee81795dcc7ef5130b80f117eb12f557a08d4837609f37d24af8d64f8b5072747e1a9e4585fc07d76720" + "5e8289235019f818ad7ed9dbb90844d6a42189ab5a8cdc303e60256dbc5daa76386422de8cf1af40ea1c07b7720e5" + "3787515a959537f4dffc663c69d29e614621bc7a345ab31f9b8931277d7577962e9558119b9d5d7db0a3b1c298afd" + "eabe11581684b62ffaa58a9877d7ceeeb2ea158df3db7881bfedb40ed4d4de7a6465cf1e1148582714279bd0e0cbf" + "f145e0bddc1ff3f5e2e6cc8b39f9640e433e4c4140e2095e6", 'hex'); @simpleTestDoc6 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' idx: 'Indexed' @simpleTestDoc6.encrypt (err) -> assert.equal err, null done() it 'should return an unencrypted version', (done) -> @encryptedSimpleTestDoc.decrypt (err) => assert.equal err, null assert.propertyVal @encryptedSimpleTestDoc, 'text', 'Unencrypted text' assert.propertyVal @encryptedSimpleTestDoc, 'bool', true assert.propertyVal @encryptedSimpleTestDoc, 'num', 42 assert.property @encryptedSimpleTestDoc, 'date' assert.equal @encryptedSimpleTestDoc.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal @encryptedSimpleTestDoc.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf @encryptedSimpleTestDoc.arr, 2 assert.equal @encryptedSimpleTestDoc.arr[0], 'alpha' assert.equal @encryptedSimpleTestDoc.arr[1], 'bravo' assert.property @encryptedSimpleTestDoc, 'mix' assert.deepEqual @encryptedSimpleTestDoc.mix, { str: 'A string', bool: false } assert.property @encryptedSimpleTestDoc, 'buf' assert.equal @encryptedSimpleTestDoc.buf.toString(), 'abcdefg' assert.propertyVal @encryptedSimpleTestDoc, 'idx', 'Indexed' assert.property @encryptedSimpleTestDoc, '_id' assert.notProperty @encryptedSimpleTestDoc, '_ct' done() it 'should return an unencrypted version when run after #encrypt', (done) -> @simpleTestDoc6.decrypt (err) => assert.equal err, null assert.propertyVal @simpleTestDoc6, 'text', 'Unencrypted text' assert.propertyVal @simpleTestDoc6, 'bool', true assert.propertyVal @simpleTestDoc6, 'num', 42 assert.property @simpleTestDoc6, 'date' assert.equal @simpleTestDoc6.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal @simpleTestDoc6.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf @simpleTestDoc6.arr, 2 assert.equal @simpleTestDoc6.arr[0], 'alpha' assert.equal @simpleTestDoc6.arr[1], 'bravo' assert.property @simpleTestDoc6, 'mix' assert.deepEqual @simpleTestDoc6.mix, { str: 'A string', bool: false } assert.property @simpleTestDoc6, 'buf' assert.equal @simpleTestDoc6.buf.toString(), 'abcdefg' assert.propertyVal @simpleTestDoc6, 'idx', 'Indexed' assert.property @simpleTestDoc6, '_id' assert.notProperty @simpleTestDoc6, '_ct' done() it 'should return an unencrypted version even if document already decrypted', (done) -> @encryptedSimpleTestDoc.decrypt (err) => assert.equal err, null @encryptedSimpleTestDoc.decrypt (err) => assert.equal err, null assert.propertyVal @encryptedSimpleTestDoc, 'text', 'Unencrypted text' assert.propertyVal @encryptedSimpleTestDoc, 'bool', true assert.propertyVal @encryptedSimpleTestDoc, 'num', 42 assert.property @encryptedSimpleTestDoc, 'date' assert.equal @encryptedSimpleTestDoc.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal @encryptedSimpleTestDoc.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf @encryptedSimpleTestDoc.arr, 2 assert.equal @encryptedSimpleTestDoc.arr[0], 'alpha' assert.equal @encryptedSimpleTestDoc.arr[1], 'bravo' assert.property @encryptedSimpleTestDoc, 'mix' assert.deepEqual @encryptedSimpleTestDoc.mix, { str: 'A string', bool: false } assert.property @encryptedSimpleTestDoc, 'buf' assert.equal @encryptedSimpleTestDoc.buf.toString(), 'abcdefg' assert.propertyVal @encryptedSimpleTestDoc, 'idx', 'Indexed' assert.property @encryptedSimpleTestDoc, '_id' assert.notProperty @encryptedSimpleTestDoc, '_ct' done() describe 'document.decryptSync()', -> simpleTestDoc7 = null before (done) -> simpleTestDoc7 = new BasicEncryptedModel text: 'Unencrypted text' bool: true num: 42 date: new Date '2014-05-19T16:39:07.536Z' id2: '5303e65d34e1e80d7a7ce212' arr: ['alpha', 'bravo'] mix: { str: 'A string', bool: false } buf: new Buffer 'abcdefg' idx: 'Indexed' simpleTestDoc7.encrypt (err) -> assert.equal err, null done() after (done) -> simpleTestDoc7.remove (err) -> assert.equal err, null done() it 'should return an unencrypted version', (done) -> simpleTestDoc7.decryptSync() assert.propertyVal simpleTestDoc7, 'text', 'Unencrypted text' assert.propertyVal simpleTestDoc7, 'bool', true assert.propertyVal simpleTestDoc7, 'num', 42 assert.property simpleTestDoc7, 'date' assert.equal simpleTestDoc7.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal simpleTestDoc7.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf simpleTestDoc7.arr, 2 assert.equal simpleTestDoc7.arr[0], 'alpha' assert.equal simpleTestDoc7.arr[1], 'bravo' assert.property simpleTestDoc7, 'mix' assert.deepEqual simpleTestDoc7.mix, { str: 'A string', bool: false } assert.property simpleTestDoc7, 'buf' assert.equal simpleTestDoc7.buf.toString(), 'abcdefg' assert.propertyVal simpleTestDoc7, 'idx', 'Indexed' assert.property simpleTestDoc7, '_id' assert.notProperty simpleTestDoc7, '_ct' done() it 'should return an unencrypted version even if document already decrypted', (done) -> simpleTestDoc7.decryptSync() assert.propertyVal simpleTestDoc7, 'text', 'Unencrypted text' assert.propertyVal simpleTestDoc7, 'bool', true assert.propertyVal simpleTestDoc7, 'num', 42 assert.property simpleTestDoc7, 'date' assert.equal simpleTestDoc7.date.toString(), new Date("2014-05-19T16:39:07.536Z").toString() assert.equal simpleTestDoc7.id2.toString(), '5303e65d34e1e80d7a7ce212' assert.lengthOf simpleTestDoc7.arr, 2 assert.equal simpleTestDoc7.arr[0], 'alpha' assert.equal simpleTestDoc7.arr[1], 'bravo' assert.property simpleTestDoc7, 'mix' assert.deepEqual simpleTestDoc7.mix, { str: 'A string', bool: false } assert.property simpleTestDoc7, 'buf' assert.equal simpleTestDoc7.buf.toString(), 'abcdefg' assert.propertyVal simpleTestDoc7, 'idx', 'Indexed' assert.property simpleTestDoc7, '_id' assert.notProperty simpleTestDoc7, '_ct' done() describe '"encryptedFields" option', -> it 'should encrypt fields iff they are in the passed in "encryptedFields" array even if those fields are indexed', (done) -> EncryptedFieldsModelSchema = mongoose.Schema text: type: String, index: true bool: type: Boolean num: type: Number EncryptedFieldsModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'EncryptedFields', encryptedFields: ['text', 'bool'] FieldsEncryptedModel = mongoose.model 'Fields', EncryptedFieldsModelSchema fieldsEncryptedDoc = new FieldsEncryptedModel text: 'Unencrypted text' bool: false num: 43 fieldsEncryptedDoc.encrypt (err) -> assert.equal err, null assert.equal fieldsEncryptedDoc.text, undefined assert.equal fieldsEncryptedDoc.bool, undefined assert.propertyVal fieldsEncryptedDoc, 'num', 43 fieldsEncryptedDoc.decrypt (err) -> assert.equal err, null assert.equal fieldsEncryptedDoc.text, 'Unencrypted text' assert.equal fieldsEncryptedDoc.bool, false assert.propertyVal fieldsEncryptedDoc, 'num', 43 done() it 'should override other options', (done) -> EncryptedFieldsOverrideModelSchema = mongoose.Schema text: type: String, index: true bool: type: Boolean num: type: Number EncryptedFieldsOverrideModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'EncryptedFieldsOverride', encryptedFields: ['text', 'bool'], excludeFromEncryption: ['bool'] FieldsOverrideEncryptedModel = mongoose.model 'FieldsOverride', EncryptedFieldsOverrideModelSchema fieldsEncryptedDoc = new FieldsOverrideEncryptedModel text: 'Unencrypted text' bool: false num: 43 fieldsEncryptedDoc.encrypt (err) -> assert.equal err, null assert.equal fieldsEncryptedDoc.text, undefined assert.equal fieldsEncryptedDoc.bool, undefined assert.propertyVal fieldsEncryptedDoc, 'num', 43 fieldsEncryptedDoc.decrypt (err) -> assert.equal err, null assert.equal fieldsEncryptedDoc.text, 'Unencrypted text' assert.equal fieldsEncryptedDoc.bool, false assert.propertyVal fieldsEncryptedDoc, 'num', 43 done() describe '"excludeFromEncryption" option', -> it 'should encrypt all non-indexed fields except those in the passed-in "excludeFromEncryption" array', (done) -> ExcludeEncryptedModelSchema = mongoose.Schema text: type: String bool: type: Boolean num: type: Number idx: type: String, index: true ExcludeEncryptedModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey, collectionId: 'ExcludeEncrypted', excludeFromEncryption: ['num'] ExcludeEncryptedModel = mongoose.model 'Exclude', ExcludeEncryptedModelSchema excludeEncryptedDoc = new ExcludeEncryptedModel text: 'Unencrypted text' bool: false num: 43 idx: 'Indexed' excludeEncryptedDoc.encrypt (err) -> assert.equal err, null assert.equal excludeEncryptedDoc.text, undefined assert.equal excludeEncryptedDoc.bool, undefined assert.propertyVal excludeEncryptedDoc, 'num', 43 assert.propertyVal excludeEncryptedDoc, 'idx', 'Indexed' excludeEncryptedDoc.decrypt (err) -> assert.equal err, null assert.equal excludeEncryptedDoc.text, 'Unencrypted text' assert.equal excludeEncryptedDoc.bool, false assert.propertyVal excludeEncryptedDoc, 'num', 43 assert.propertyVal excludeEncryptedDoc, 'idx', 'Indexed' done() describe '"decryptPostSave" option', -> before -> HighPerformanceModelSchema = mongoose.Schema text: type: String HighPerformanceModelSchema.plugin encrypt, secret: secret, decryptPostSave: false @HighPerformanceModel = mongoose.model 'HighPerformance', HighPerformanceModelSchema beforeEach (done) -> @doc = new @HighPerformanceModel text: 'Unencrypted text' done() afterEach (done) -> @HighPerformanceModel.remove (err) -> assert.equal err, null done() return it 'saves encrypted fields correctly', (done) -> @doc.save (err) => assert.equal err, null @HighPerformanceModel.find _id: @doc._id _ct: $exists: true text: $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.propertyVal docs[0], 'text', 'Unencrypted text' done() it 'returns encrypted data after save', (done) -> @doc.save (err, savedDoc) -> assert.equal err, null assert.property savedDoc, '_ct', 'Document remains encrypted after save' assert.notProperty savedDoc, 'text' savedDoc.decrypt (err) -> assert.equal err, null assert.notProperty savedDoc, '_ct' assert.propertyVal savedDoc, 'text', 'Unencrypted text', 'Document can still be unencrypted' done() describe 'Array EmbeddedDocument', -> describe 'when only child is encrypted', -> describe 'and parent does not have encryptedChildren plugin', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] @ParentModel = mongoose.model 'Parent', ParentModelSchema @ChildModel = mongoose.model 'Child', ChildModelSchema beforeEach (done) -> @parentDoc = new @ParentModel text: 'Unencrypted text' childDoc = new @ChildModel text: 'Child unencrypted text' childDoc2 = new @ChildModel text: 'Second unencrypted text' @parentDoc.children.addToSet childDoc @parentDoc.children.addToSet childDoc2 @parentDoc.save done after (done) -> @parentDoc.remove done describe 'document.save()', -> it 'should not have decrypted fields', -> assert.equal @parentDoc.children[0].text, undefined it 'should persist children as encrypted', (done) -> @ParentModel.find _id: @parentDoc._id 'children._ct': $exists: true 'children.text': $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.propertyVal docs[0].children[0], 'text', 'Child unencrypted text' done() return describe 'document.find()', -> it 'when parent doc found, should pass an unencrypted version of the embedded document to the callback', (done) -> @ParentModel.findById @parentDoc._id, (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.isArray doc.children assert.isObject doc.children[0] assert.property doc.children[0], 'text', 'Child unencrypted text' assert.property doc.children[0], '_id' assert.notProperty doc.children[0], '_ct' done() return describe 'tampering with child documents by swapping their ciphertext', -> it 'should not cause an error because embedded documents are not self-authenticated', (done) -> @ParentModel.findById(@parentDoc._id).lean().exec (err, doc) => assert.equal err, null assert.isArray doc.children childDoc1CipherText = doc.children[0]._ct childDoc2CipherText = doc.children[1]._ct @ParentModel.update { _id: @parentDoc._id } , { $set : {'children.0._ct': childDoc2CipherText, 'children.1._ct': childDoc1CipherText } } , (err) => assert.equal err, null @ParentModel.findById @parentDoc._id, (err, doc) -> assert.equal err, null assert.isArray doc.children assert.property doc.children[0], 'text', 'Second unencrypted text', 'Ciphertext was swapped' assert.property doc.children[1], 'text', 'Child unencrypted text', 'Ciphertext was swapped' done() return describe 'and parent has encryptedChildren plugin', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] ParentModelSchema.plugin encrypt.encryptedChildren @ParentModel = mongoose.model 'ParentEC', ParentModelSchema @ChildModel = mongoose.model 'ChildOfECP', ChildModelSchema beforeEach (done) -> @parentDoc = new @ParentModel text: 'Unencrypted text' childDoc = new @ChildModel text: 'Child unencrypted text' childDoc2 = new @ChildModel text: 'Second unencrypted text' @parentDoc.children.addToSet childDoc @parentDoc.children.addToSet childDoc2 @parentDoc.save done after (done) -> @parentDoc.remove done describe 'document.save()', -> it 'should have decrypted fields', -> assert.equal @parentDoc.children[0].text, 'Child unencrypted text' it 'should persist children as encrypted', (done) -> @ParentModel.find _id: @parentDoc._id 'children._ct': $exists: true 'children.text': $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.propertyVal docs[0].children[0], 'text', 'Child unencrypted text' done() return describe 'document.find()', -> it 'when parent doc found, should pass an unencrypted version of the embedded document to the callback', (done) -> @ParentModel.findById @parentDoc._id, (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.isArray doc.children assert.isObject doc.children[0] assert.property doc.children[0], 'text', 'Child unencrypted text' assert.property doc.children[0], '_id' assert.notProperty doc.children[0], '_ct' done() return describe 'tampering with child documents by swapping their ciphertext', -> it 'should not cause an error because embedded documents are not self-authenticated', (done) -> @ParentModel.findById(@parentDoc._id).lean().exec (err, doc) => assert.equal err, null assert.isArray doc.children childDoc1CipherText = doc.children[0]._ct childDoc2CipherText = doc.children[1]._ct @ParentModel.update { _id: @parentDoc._id } , { $set : {'children.0._ct': childDoc2CipherText, 'children.1._ct': childDoc1CipherText } } , (err) => assert.equal err, null @ParentModel.findById @parentDoc._id, (err, doc) -> assert.equal err, null assert.isArray doc.children assert.property doc.children[0], 'text', 'Second unencrypted text', 'Ciphertext was swapped' assert.property doc.children[1], 'text', 'Child unencrypted text', 'Ciphertext was swapped' done() describe 'when child is encrypted and authenticated', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey signingKey: signingKey ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] ParentModelSchema.plugin encrypt, encryptionKey: encryptionKey signingKey: signingKey encryptedFields: [] additionalAuthenticatedFields: ['children'] @ParentModel = mongoose.model 'ParentWithAuth', ParentModelSchema @ChildModel = mongoose.model 'ChildWithAuth', ChildModelSchema beforeEach (done) -> @parentDoc = new @ParentModel text: 'Unencrypted text' childDoc = new @ChildModel text: 'Child unencrypted text' childDoc2 = new @ChildModel text: 'Second unencrypted text' @parentDoc.children.addToSet childDoc @parentDoc.children.addToSet childDoc2 @parentDoc.save done after (done) -> @parentDoc.remove done it 'should persist children as encrypted after removing a child', (done) -> @ParentModel.findById @parentDoc._id, (err, doc) => return done(err) if err assert.ok doc, 'should have found doc with encrypted children' doc.children.id(doc.children[1]._id).remove() doc.save (err) => return done(err) if err @ParentModel.find _id: @parentDoc._id 'children._ct': $exists: true 'children.text': $exists: false , (err, docs) -> return done(err) if err assert.ok doc, 'should have found doc with encrypted children' assert.equal doc.children.length, 1 done() return it 'should persist children as encrypted after adding a child', (done) -> @ParentModel.findById @parentDoc._id, (err, doc) => return done(err) if err assert.ok doc, 'should have found doc with encrypted children' doc.children.addToSet text: 'new child' doc.save (err) => return done(err) if err @ParentModel.findById @parentDoc._id .exec (err, doc) => return done(err) if err assert.ok doc, 'should have found doc with encrypted children' assert.equal doc.children.length, 3 done() return describe 'when child and parent are encrypted', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] ParentModelSchema.plugin encrypt, encryptionKey: encryptionKey signingKey: signingKey encryptedFields: ['text'] additionalAuthenticatedFields: ['children'] @ParentModel = mongoose.model 'ParentBoth', ParentModelSchema @ChildModel = mongoose.model 'ChildBoth', ChildModelSchema beforeEach (done) -> @parentDoc = new @ParentModel text: 'Unencrypted text' childDoc = new @ChildModel text: 'Child unencrypted text' childDoc2 = new @ChildModel text: 'Second unencrypted text' @parentDoc.children.addToSet childDoc @parentDoc.children.addToSet childDoc2 @parentDoc.save done after (done) -> @parentDoc.remove done describe 'document.save()', -> it 'should have decrypted fields on parent', -> assert.equal @parentDoc.text, 'Unencrypted text' it 'should have decrypted fields', -> assert.equal @parentDoc.children[0].text, 'Child unencrypted text' it 'should persist children as encrypted', (done) -> @ParentModel.find _id: @parentDoc._id 'children._ct': $exists: true 'children.text': $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.propertyVal docs[0].children[0], 'text', 'Child unencrypted text' done() return describe 'document.find()', -> it 'when parent doc found, should pass an unencrypted version of the embedded document to the callback', (done) -> @ParentModel.findById @parentDoc._id , (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.isArray doc.children assert.isObject doc.children[0] assert.property doc.children[0], 'text', 'Child unencrypted text' assert.property doc.children[0], '_id' assert.notProperty doc.children[0], '_ct' done() return describe 'when child field is in additionalAuthenticatedFields on parent and child documents are tampered with by swapping their ciphertext', -> it 'should pass an error', (done) -> @ParentModel.findById(@parentDoc._id).lean().exec (err, doc) => assert.equal err, null assert.isArray doc.children childDoc1CipherText = doc.children[0]._ct childDoc2CipherText = doc.children[1]._ct @ParentModel.update { _id: @parentDoc._id } , { $set : {'children.0._ct': childDoc2CipherText, 'children.1._ct': childDoc1CipherText } } , (err) => assert.equal err, null @ParentModel.findById @parentDoc._id, (err, doc) -> assert.ok err, 'There was an error' assert.propertyVal err, 'message', 'Authentication failed' done() describe 'when entire parent is encrypted', -> before -> ParentModelSchema = mongoose.Schema text: type: String children: [text: type: String] ParentModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey @ParentModel = mongoose.model 'ParentEntire', ParentModelSchema beforeEach (done) -> @parentDoc = new @ParentModel text: 'Unencrypted text' children: [text: 'Child unencrypted text'] @parentDoc.save done after (done) -> @parentDoc.remove done describe 'document.save()', -> it 'should have decrypted fields in document passed to call back', -> assert.equal @parentDoc.text, 'Unencrypted text' assert.equal @parentDoc.children[0].text, 'Child unencrypted text' it 'should persist the entire document as encrypted', (done) -> @ParentModel.find _id: @parentDoc._id '_ct': $exists: true 'children': $exists: false 'children.text': $exists: false , (err, docs) -> assert.equal err, null assert.lengthOf docs, 1 assert.propertyVal docs[0], 'text', 'Unencrypted text' assert.propertyVal docs[0].children[0], 'text', 'Child unencrypted text' done() return describe 'document.find()', -> it 'when parent doc found, should pass an unencrypted version of the embedded document to the callback', (done) -> @ParentModel.findById @parentDoc._id, (err, doc) -> assert.equal err, null assert.propertyVal doc, 'text', 'Unencrypted text' assert.isArray doc.children assert.isObject doc.children[0] assert.property doc.children[0], 'text', 'Child unencrypted text' assert.property doc.children[0], '_id' assert.notProperty doc.children[0], '_ct' done() return describe 'Encrypted embedded document when parent has validation error and doesnt have encryptedChildren plugin', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey encryptedFields: ['text'] ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] ParentModelSchema.pre 'validate', (next) -> @invalidate 'text', 'invalid', this.text next() @ParentModel2 = mongoose.model 'ParentWithoutPlugin', ParentModelSchema @ChildModel2 = mongoose.model 'ChildAgain', ChildModelSchema it 'should return unencrypted embedded documents', (done) -> doc = new @ParentModel2 text: 'here it is' children: [{text: 'Child unencrypted text'}] doc.save (err) -> assert.ok err, 'There should be a validation error' assert.propertyVal doc, 'text', 'here it is' assert.isArray doc.children assert.property doc.children[0], '_id' assert.notProperty doc.children[0], '_ct' assert.property doc.children[0], 'text', 'Child unencrypted text' done() describe 'Encrypted embedded document when parent has validation error and has encryptedChildren plugin', -> before -> ChildModelSchema = mongoose.Schema text: type: String ChildModelSchema.plugin encrypt, encryptionKey: encryptionKey, signingKey: signingKey encryptedFields: ['text'] @ParentModelSchema = mongoose.Schema text: type: String children: [ChildModelSchema] @ParentModelSchema.pre 'validate', (next) -> @invalidate 'text',