@medikura/mongoose-vault
Version:
Simple encryption plugin for Mongoose, using the transit backend from Hasicorp's Vault (Encryption as a Service).
287 lines (245 loc) • 10.4 kB
JavaScript
/* global describe, it, describe, before */
require('dotenv').config()
const expect = require('chai').expect
const mongoose = require('mongoose')
const nodeVault = require('node-vault')
const mongooseVault = require('./mongoose-vault')
const Schema = mongoose.Schema
let vault
before(async () => {
mongoose.connect(process.env.MONGO_CONNECTION_STRING, {useNewUrlParser: true})
vault = await nodeVault({endpoint: process.env.VAULT_CONNECTION_STRING, token: process.env.VAULT_DEV_ROOT_TOKEN_ID})
try {
await vault.mount({mount_point: 'transit', type: 'transit'})
} catch (e) { /* Already mounted */ }
})
after(async () => {
mongoose.disconnect()
})
const schemaDefinition = {
firstName: { type: String },
lastName: { type: String },
address: { type: String },
address2: { type: String },
email: { type: String },
objectOfStrings: {
string1: { type: String },
string2: { type: String },
string3: { type: String }
},
emptyString: { type: String },
bool: { type: Boolean },
num: { type: Number },
date: { type: Date },
arr: [ { type: String } ],
mix: { type: mongoose.Schema.Types.Mixed },
buf: { type: Buffer },
idx: { type: String, index: true }
}
var validIdentity = {
'firstName': 'Max',
'lastName': 'Mustermann',
'address': 'Karlstr. 1',
'email': 'test@test.de',
'bool': true,
'num': 42,
'emptyString': '',
'objectOfStrings': {
'string1': 'content1',
'string2': 'content2',
'string3': 'content3'
},
'date': new Date('2014-05-19T16:39:07.536Z'),
'arr': ['alpha', 'bravo'],
'mix': { str: 'A string', bool2: false },
'buf': Buffer.from('abcdefg'),
'idx': '10000000'
}
describe('Test Mongoose Vault initialisation and options', function () {
it('correct defined encryptedFields should work. ', async function () {
var Identity = new Schema(schemaDefinition)
expect(() => {
Identity.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
keyCreationConvergentEncryption: true,
keyName: 'per_document',
encryptedFields: [ 'firstName', 'lastName', 'objectOfStrings.string1' ]
})
}).to.not.throw()
})
it('not existing encryptedField should throw error. ', async function () {
var Identity = new Schema(schemaDefinition)
expect(() => {
Identity.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
keyCreationConvergentEncryption: true,
keyName: 'per_document',
encryptedFields: [ 'firstName', 'lastName', 'objectOfStrings.isNotDefined' ]
})
}).to.throw(Error, 'unknown field objectOfStrings.isNotDefined')
})
it('existing encryptedField but no String should throw error. ', async function () {
var Identity = new Schema(schemaDefinition)
expect(() => {
Identity.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
keyCreationConvergentEncryption: true,
keyName: 'per_document',
encryptedFields: [ 'firstName', 'lastName', 'num' ]
})
}).to.throw(Error, 'So far, only Strings are supported as encrypted Field. num is a Number. Create a FeatureRequest if you wish to support more types.')
})
})
describe('Test Mongoose Vault with convergentEncryption disabled', function () {
// Setup Mongose Schema and Model
var Identity = new Schema(schemaDefinition)
var IdentityNoEncryption = new Schema(schemaDefinition)
Identity.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
keyCreationConvergentEncryption: false
})
var IdentityModel = mongoose.model('Identity', Identity)
var IdentityNoEncryptionModel = mongoose.model('IdentityNoEncryption', IdentityNoEncryption, 'identities')
before(async () => {
await IdentityModel.deleteMany({})
await IdentityModel.connectVault(vault)
})
it('Mongoose create should encrypt and decrypt the document.', async function () {
let model = await IdentityModel.create(validIdentity)
let modelEncrypted = await IdentityNoEncryptionModel.findById(model.id)
expect(modelEncrypted).property('firstName').to.contain('vault:v1:')
expect(modelEncrypted).property('lastName').to.contain('vault:v1:')
expect(modelEncrypted).property('email').to.contain('vault:v1:')
expect(modelEncrypted).nested.property('objectOfStrings.string1').to.contain('vault:v1:')
expect(modelEncrypted).nested.property('objectOfStrings.string2').to.contain('vault:v1:')
expect(model._doc).to.deep.include(validIdentity)
})
it('Mongoose encrypt should not include the plaintext Field', async function () {
let model = new IdentityModel(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
await model.encrypt()
expect(model).property('firstName').to.contain('vault:v1:')
expect(model).property('lastName').to.contain('vault:v1:')
expect(model).property('email').to.contain('vault:v1:')
expect(model).nested.property('objectOfStrings.string1').to.contain('vault:v1:')
expect(model).nested.property('objectOfStrings.string2').to.contain('vault:v1:')
})
it('Mongoose findOne should decrypt', async function () {
let model = await IdentityModel.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
let modelNew = await IdentityModel.findById(model.id)
expect(modelNew._doc).to.deep.include(validIdentity)
})
it('Mongoose findById should decrypt', async function () {
let models = await IdentityModel.find({})
expect(models).to.be.an('array')
expect(models).to.have.lengthOf.at.least(2)
expect(models[0]._doc).deep.to.include(validIdentity)
})
it('Mongoose find by encrypted field should not return any', async function () {
let models = await IdentityModel.find({firstName: 'Max'})
expect(models).to.be.an('array')
expect(models).to.have.lengthOf(0)
})
it('Mongoose create document without encrypted field should not error', async function () {
await IdentityModel.create({ num: 1 })
})
it('Mongoose find selecting unencrypted field should not error', async function () {
await IdentityModel.find({}, ['num'])
})
})
describe('Test Mongoose Vault with convergentEncryption enabled', function () {
// Setup Mongose Schema and Model
var Identity2 = new Schema(schemaDefinition)
Identity2.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
// THIS WILL ENABLE SEARCH TO WORK
keyCreationConvergentEncryption: true
})
var Identity2Model = mongoose.model('Identity2', Identity2)
before(async () => {
await Identity2Model.deleteMany({})
await Identity2Model.connectVault(vault)
})
it('Mongoose find by encrypted fields should return matching documents', async function () {
let model = await Identity2Model.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
let models = await Identity2Model.find({firstName: 'Max'})
expect(models).to.be.an('array').and.have.lengthOf.at.least(1)
expect(models[0]._doc).to.deep.include(validIdentity)
})
it('Mongoose query builder with encrypted fields should return matching documents', async function () {
let model = await Identity2Model.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
let models = await Identity2Model.find({firstName: 'Max'})
expect(models).to.be.an('array').and.have.lengthOf.at.least(1)
expect(models[0]._doc).to.deep.include(validIdentity)
models = await Identity2Model
.where('objectOfStrings.string1').equals('content1')
.or([{ 'objectOfStrings.string2': 'content1' }, { 'objectOfStrings.string2': 'content2' }])
expect(models).to.be.an('array').and.have.lengthOf.at.least(1)
expect(models[0]._doc).to.deep.include(validIdentity)
})
it('Mongoose findOne by encrypted fields should return matching document', async function () {
let model = await Identity2Model.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
let models = await Identity2Model.findOne({firstName: 'Max'})
expect(models._doc).to.deep.include(validIdentity)
})
it('Mongoose findOne query builder with encrypted fields should return matching document', async function () {
let model = await Identity2Model.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
let models = await Identity2Model.findOne({firstName: 'Max'})
expect(models._doc).to.deep.include(validIdentity)
models = await Identity2Model.findOne()
.where('objectOfStrings.string1').equals('content1')
.or([{ 'objectOfStrings.string2': 'content1' }, { 'objectOfStrings.string2': 'content2' }])
expect(models._doc).to.deep.include(validIdentity)
})
it('Mongoose countDocuments query should return correct count', async function () {
let uniqueFirstName = 'countDocuments' + Date.now()
let uniqueDocument = {
...validIdentity,
firstName: uniqueFirstName
}
let model = await Identity2Model.create(uniqueDocument)
expect(model._doc).to.deep.include(uniqueDocument)
let count = await Identity2Model.countDocuments({firstName: uniqueFirstName})
expect(count).to.equal(1)
})
it('Mongoose deleteMany should delete document', async function () {
let uniqueFirstName = 'deleteMany' + Date.now()
let uniqueDocument = {
...validIdentity,
firstName: uniqueFirstName
}
let model = await Identity2Model.create(uniqueDocument)
expect(model._doc).to.deep.include(uniqueDocument)
let { deletedCount } = await Identity2Model.deleteMany({firstName: uniqueFirstName})
expect(deletedCount).to.equal(1)
})
})
describe('Test Mongoose Vault with keyName per_document', function () {
// Setup Mongose Schema and Model
var Identity3 = new Schema(schemaDefinition)
Identity3.plugin(mongooseVault, {
middleware: true,
decryptPostSave: true,
keyCreationConvergentEncryption: true,
keyName: 'per_document'
})
var Identity3Model = mongoose.model('Identity3', Identity3)
before(async () => {
await Identity3Model.deleteMany({})
await Identity3Model.connectVault(vault)
})
it('Mongoose create should encrypt and decrypt the document.', async function () {
let model = await Identity3Model.create(validIdentity)
expect(model._doc).to.deep.include(validIdentity)
})
})