'use strict';
const assert = require('assert');
const mdb = require('mongodb');
const isBsonType = require('../../lib/helpers/isBsonType');
const { Schema, createConnection } = require('../../lib');
const { ObjectId, Double, Int32, Decimal128 } = require('mongodb/lib/bson');
const fs = require('fs');
const mongoose = require('../../lib');
const { Map } = require('../../lib/types');
const { join } = require('path');
const { readFile } = require('fs/promises');
const LOCAL_KEY = Buffer.from('Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk', 'base64');
const { UUID } = require('mongodb/lib/bson');
* @param {string} path
*
* @returns {boolean}
*/
function exists(path) {
try {
fs.statSync(path);
return true;
} catch {
return false;
}
}
* @param {object} object
* @param {string} property
*/
function isEncryptedValue(object, property) {
const value = object[property];
assert.ok(isBsonType(value, 'Binary'), `auto encryption for property ${property} failed: not a BSON binary.`);
assert.ok(value.sub_type === 6, `auto encryption for property ${property} failed: not subtype 6.`);
}
describe('encryption integration tests', () => {
let cryptSharedLibPath;
let clusterUri;
const autoEncryptionOptions = () => ({
dbName: 'db',
autoEncryption: {
keyVaultNamespace: 'keyvault.datakeys',
kmsProviders: { local: { key: LOCAL_KEY } },
extraOptions: {
cryptdSharedLibRequired: true,
cryptSharedLibPath: cryptSharedLibPath
}
}
});
before(async function() {
const expansionFile = join(__dirname, '../..', 'fle-cluster-config.json');
if (!exists(expansionFile)) {
throw new Error('must setup a cluster using `npm run setup-test-encryption`.');
}
const configuration = JSON.parse(await readFile(expansionFile, { encoding: 'utf-8' }));
cryptSharedLibPath = configuration.cryptShared;
clusterUri = configuration.uri;
});
describe('meta: environmental variables are correctly set up', () => {
it('clusterUri is set', async function() {
const uri = clusterUri;
assert.ok(uri);
});
it('clusterUri points to running cluster', async function() {
try {
const connection = await mongoose.connect(clusterUri);
await connection.disconnect();
} catch (error) {
throw new Error('Unable to connect to running cluster', { cause: error });
}
});
it('cryptSharedLibPath is set', async function() {
const shared_library_path = cryptSharedLibPath;
assert.ok(shared_library_path);
});
});
const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Random';
let keyId, keyId2, keyId3;
let utilClient;
beforeEach(async function() {
const keyVaultClient = new mdb.MongoClient(clusterUri);
await keyVaultClient.connect();
await keyVaultClient.db('keyvault').collection('datakeys');
const clientEncryption = new mdb.ClientEncryption(keyVaultClient, {
keyVaultNamespace: 'keyvault.datakeys',
kmsProviders: { local: { key: LOCAL_KEY } }
});
keyId = await clientEncryption.createDataKey('local');
keyId2 = await clientEncryption.createDataKey('local');
keyId3 = await clientEncryption.createDataKey('local');
await keyVaultClient.close();
utilClient = new mdb.MongoClient(clusterUri);
});
afterEach(async function() {
await utilClient.db('db').dropDatabase({
w: 'majority'
});
await utilClient.close();
});
describe('Tests that fields of valid schema types can be declared as encrypted schemas', function() {
let connection;
let schema;
let model;
const basicSchemaTypes = [
{ type: String, name: 'string', input: 3, expected: 3 },
{ type: Schema.Types.Boolean, name: 'boolean', input: true, expected: true },
{ type: Schema.Types.Buffer, name: 'buffer', input: Buffer.from([1, 2, 3]) },
{ type: Schema.Types.UUID, name: 'uuid', input: new UUID('53ca5a3e-19cd-405d-8ef7-5d88823939d9'), expected: '53ca5a3e-19cd-405d-8ef7-5d88823939d9' },
{ type: Date, name: 'date', input: new Date(12, 12, 2012), expected: new Date(12, 12, 2012) },
{ type: ObjectId, name: 'objectid', input: new ObjectId() },
{ type: BigInt, name: 'bigint', input: 3n },
{ type: Decimal128, name: 'Decimal128', input: new Decimal128('1.5') },
{ type: Int32, name: 'int32', input: new Int32(5), expected: 5 },
{ type: Double, name: 'double', input: new Double(1.5) }
];
afterEach(async function() {
await connection?.close();
});
for (const { type, name, input, expected } of basicSchemaTypes) {
async function test() {
const [{ _id }] = await model.insertMany([{ field: input }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'field');
const doc = await model.findOne({ _id });
if (Buffer.isBuffer(input) || input instanceof UUID) {
assert.ok(doc.field.equals(input));
} else {
assert.deepEqual(doc.field, expected ?? input);
}
}
describe('CSFLE', function() {
beforeEach(async function() {
schema = new Schema({
field: {
type, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
});
it(`${name} encrypts and decrypts`, test);
});
describe('queryableEncryption', function() {
beforeEach(async function() {
schema = new Schema({
field: {
type, encrypt: { keyId: keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
});
it(`${name} encrypts and decrypts`, test);
});
}
describe('mongoose Maps', function() {
describe('CSFLE', function() {
it('encrypts and decrypts', async function() {
const schema = new Schema({
a: {
type: Schema.Types.Map,
of: String,
encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
const model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: {
name: 'bailey'
} }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id });
assert.ok(doc.a instanceof Map);
const rawObject = Object.fromEntries(doc.a);
assert.deepEqual(rawObject, { name: 'bailey' });
});
});
describe('queryable encryption', function() {
it('encrypts and decrypts', async function() {
const schema = new Schema({
a: {
type: Schema.Types.Map,
of: String,
encrypt: { keyId: keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
const model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: {
name: 'bailey'
} }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id });
assert.ok(doc.a instanceof Map);
const rawObject = Object.fromEntries(doc.a);
assert.deepEqual(rawObject, { name: 'bailey' });
});
});
});
describe('nested object schemas', function() {
const tests = {
'nested object schemas for CSFLE': {
modelFactory: () => {
const schema = new Schema({
a: {
b: {
c: {
type: String,
encrypt: { keyId: [keyId], algorithm }
}
}
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
},
'nested object schemas for QE': {
modelFactory: () => {
const schema = new Schema({
a: {
b: {
c: {
type: String,
encrypt: { keyId: keyId }
}
}
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
},
'nested schemas for csfle': {
modelFactory: () => {
const nestedSchema = new Schema({
b: {
c: {
type: String,
encrypt: { keyId: [keyId], algorithm }
}
}
}, {
encryptionType: 'csfle'
});
const schema = new Schema({
a: nestedSchema
}, {
encryptionType: 'csfle'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
},
'nested schemas for QE': {
modelFactory: () => {
const nestedSchema = new Schema({
b: {
c: {
type: String,
encrypt: { keyId: keyId }
}
}
}, {
encryptionType: 'queryableEncryption'
});
const schema = new Schema({
a: nestedSchema
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
}
};
for (const [description, { modelFactory }] of Object.entries(tests)) {
describe(description, function() {
it('encrypts and decrypts', async function() {
const { model } = modelFactory();
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: { b: { c: 'hello' } } }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc.a.b, 'c');
const doc = await model.findOne({ _id });
assert.deepEqual(doc.a.b.c, 'hello');
});
});
}
});
describe('array encrypted fields', function() {
describe('primitive array fields for CSFLE', function() {
it('encrypts and decrypts', async function() {
const schema = new Schema({
a: {
type: [Int32],
encrypt: {
keyId: [keyId],
algorithm
}
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
const model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: [new Int32(3)] }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id });
assert.deepEqual(doc.a, [3]);
});
});
describe('document array fields for CSFLE', function() {
it('encrypts and decrypts', async function() {
const nestedSchema = new Schema({ name: String }, { _id: false });
const schema = new Schema({
a: {
type: [nestedSchema],
encrypt: {
keyId: [keyId],
algorithm
}
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
const model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: [{ name: 'bailey' }] }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id }, {}, { lean: true });
assert.deepEqual(doc.a, [{ name: 'bailey' }]);
});
});
describe('primitive array field for QE', function() {
it('encrypts and decrypts', async function() {
const schema = new Schema({
a: {
type: [Int32],
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
const model = connection.model('Schema', schema); await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: [new Int32(3)] }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id });
assert.deepEqual(doc.a, [3]);
});
});
describe('document array fields for QE', function() {
it('encrypts and decrypts', async function() {
const nestedSchema = new Schema({ name: String }, { _id: false });
const schema = new Schema({
a: {
type: [nestedSchema],
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
const model = connection.model('Schema', schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: [{ name: 'bailey' }] }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id }, {}, { lean: true });
assert.deepEqual(doc.a, [{ name: 'bailey' }]);
});
});
});
describe('multiple encrypted fields in a model', function() {
const tests = {
'multiple fields in a schema for CSFLE': {
modelFactory: () => {
const encrypt = {
keyId: [keyId],
algorithm
};
const schema = new Schema({
a: {
type: String,
encrypt
},
b: {
type: BigInt
},
c: {
d: {
type: String,
encrypt
}
}
}, {
encryptionType: 'csfle'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
},
'multiple fields in a schema for QE': {
modelFactory: () => {
const schema = new Schema({
a: {
type: String,
encrypt: {
keyId
}
},
b: {
type: BigInt
},
c: {
d: {
type: String,
encrypt: {
keyId: keyId2
}
}
}
}, {
encryptionType: 'queryableEncryption'
});
connection = createConnection();
model = connection.model('Schema', schema);
return { model };
}
}
};
for (const [description, { modelFactory }] of Object.entries(tests)) {
describe(description, function() {
it('encrypts and decrypts', async function() {
const { model } = modelFactory();
await connection.openUri(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: 'hello', b: 1n, c: { d: 'world' } }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
assert.ok(typeof encryptedDoc.b === 'number');
isEncryptedValue(encryptedDoc.c, 'd');
const doc = await model.findOne({ _id }, {});
assert.deepEqual(doc.a, 'hello');
assert.deepEqual(doc.b, 1n);
assert.deepEqual(doc.c, { d: 'world' });
});
});
}
});
describe('multiple schemas', function() {
const tests = {
'multiple schemas for CSFLE': {
modelFactory: () => {
connection = createConnection();
const encrypt = {
keyId: [keyId],
algorithm
};
const model1 = connection.model('Model1', new Schema({
a: {
type: String,
encrypt
}
}, {
encryptionType: 'csfle'
}));
const model2 = connection.model('Model2', new Schema({
b: {
type: String,
encrypt
}
}, {
encryptionType: 'csfle'
}));
return { model1, model2 };
}
},
'multiple schemas for QE': {
modelFactory: () => {
connection = createConnection();
const model1 = connection.model('Model1', new Schema({
a: {
type: String,
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
}));
const model2 = connection.model('Model2', new Schema({
b: {
type: String,
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
}));
return { model1, model2 };
}
}
};
for (const [description, { modelFactory }] of Object.entries(tests)) {
describe(description, function() {
it('encrypts and decrypts', async function() {
const { model1, model2 } = modelFactory();
await connection.openUri(clusterUri, autoEncryptionOptions());
{
const [{ _id }] = await model1.insertMany([{ a: 'hello' }]);
const encryptedDoc = await utilClient.db('db').collection('model1').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model1.findOne({ _id });
assert.deepEqual(doc.a, 'hello');
}
{
const [{ _id }] = await model2.insertMany([{ b: 'world' }]);
const encryptedDoc = await utilClient.db('db').collection('model2').findOne({ _id });
isEncryptedValue(encryptedDoc, 'b');
const doc = await model2.findOne({ _id });
assert.deepEqual(doc.b, 'world');
}
});
});
}
});
describe('CSFLE and QE schemas on the same connection', function() {
it('encrypts and decrypts', async function() {
connection = createConnection();
const model1 = connection.model('Model1', new Schema({
a: {
type: String,
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
}));
const model2 = connection.model('Model2', new Schema({
b: {
type: String,
encrypt: {
keyId: [keyId],
algorithm
}
}
}, {
encryptionType: 'csfle'
}));
await connection.openUri(clusterUri, autoEncryptionOptions());
{
const [{ _id }] = await model1.insertMany([{ a: 'hello' }]);
const encryptedDoc = await utilClient.db('db').collection('model1').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model1.findOne({ _id });
assert.deepEqual(doc.a, 'hello');
}
{
const [{ _id }] = await model2.insertMany([{ b: 'world' }]);
const encryptedDoc = await utilClient.db('db').collection('model2').findOne({ _id });
isEncryptedValue(encryptedDoc, 'b');
const doc = await model2.findOne({ _id });
assert.deepEqual(doc.b, 'world');
}
});
});
describe('Models with discriminators', function() {
let discrim1, discrim2, model;
describe('csfle', function() {
beforeEach(async function() {
connection = createConnection();
const schema = new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
model = connection.model('Schema', schema);
discrim1 = model.discriminator('Test', new Schema({
age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
}));
discrim2 = model.discriminator('Test2', new Schema({
dob: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
}));
await connection.openUri(clusterUri, autoEncryptionOptions());
});
it('encrypts', async function() {
{
const doc = new discrim1({ name: 'bailey', age: 32 });
await doc.save();
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
isEncryptedValue(encryptedDoc, 'age');
}
{
const doc = new discrim2({ name: 'bailey', dob: 32 });
await doc.save();
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
isEncryptedValue(encryptedDoc, 'dob');
}
});
it('decrypts', async function() {
{
const doc = new discrim1({ name: 'bailey', age: 32 });
await doc.save();
const decryptedDoc = await discrim1.findOne({ _id: doc._id });
assert.equal(decryptedDoc.age, 32);
}
{
const doc = new discrim2({ name: 'bailey', dob: 32 });
await doc.save();
const decryptedDoc = await discrim2.findOne({ _id: doc._id });
assert.equal(decryptedDoc.dob, 32);
}
});
});
describe('queryableEncryption', function() {
beforeEach(async function() {
connection = createConnection();
const schema = new Schema({
name: {
type: String, encrypt: { keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
model = connection.model('Schema', schema);
discrim1 = model.discriminator('Test', new Schema({
age: {
type: Int32, encrypt: { keyId: keyId2 }
}
}, {
encryptionType: 'queryableEncryption'
}));
discrim2 = model.discriminator('Test2', new Schema({
dob: {
type: Int32, encrypt: { keyId: keyId3 }
}
}, {
encryptionType: 'queryableEncryption'
}));
await connection.openUri(clusterUri, autoEncryptionOptions());
});
it('encrypts', async function() {
{
const doc = new discrim1({ name: 'bailey', age: 32 });
await doc.save();
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
isEncryptedValue(encryptedDoc, 'age');
}
{
const doc = new discrim2({ name: 'bailey', dob: 32 });
await doc.save();
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
isEncryptedValue(encryptedDoc, 'dob');
}
});
it('decrypts', async function() {
{
const doc = new discrim1({ name: 'bailey', age: 32 });
await doc.save();
const decryptedDoc = await discrim1.findOne({ _id: doc._id });
assert.equal(decryptedDoc.age, 32);
}
{
const doc = new discrim2({ name: 'bailey', dob: 32 });
await doc.save();
const decryptedDoc = await discrim2.findOne({ _id: doc._id });
assert.equal(decryptedDoc.dob, 32);
}
});
});
describe('cloned parent schema before declaring discriminator', function() {
beforeEach(async function() {
connection = createConnection();
});
describe('csfle', function() {
it('throws on duplicate keys declared on different discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
model = connection.model('Schema', schema);
assert.throws(() => {
const clonedSchema = schema.clone().add({
age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
});
model.discriminator('Test', clonedSchema);
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator/);
});
});
describe('queryable encryption', function() {
it('throws on duplicate keys declared on different discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
model = connection.model('Schema', schema);
assert.throws(() => {
const clonedSchema = schema.clone().add({
age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
});
model.discriminator('Test', clonedSchema);
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator/);
});
});
});
describe('duplicate keys in discriminators', function() {
beforeEach(async function() {
connection = createConnection();
});
describe('csfle', function() {
it('throws on duplicate keys declared on different discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
model = connection.model('Schema', schema);
discrim1 = model.discriminator('Test', new Schema({
age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
}));
discrim2 = model.discriminator('Test2', new Schema({
age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
}));
const error = await connection.openUri(clusterUri, autoEncryptionOptions()).catch(e => e);
assert.ok(error instanceof Error);
assert.match(error.message, /Cannot have duplicate keys in discriminators with encryption/);
});
it('throws on duplicate keys declared on root and child discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
model = connection.model('Schema', schema);
assert.throws(() => model.discriminator('Test', new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
})),
/encrypted fields cannot be declared on both the base schema and the child schema in a discriminator\. path=name/
);
});
});
describe('queryable encryption', function() {
it('throws on duplicate keys declared on different discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
model = connection.model('Schema', schema);
discrim1 = model.discriminator('Test', new Schema({
age: {
type: Int32, encrypt: { keyId: keyId2 }
}
}, {
encryptionType: 'queryableEncryption'
}));
discrim2 = model.discriminator('Test2', new Schema({
age: {
type: Int32, encrypt: { keyId: keyId3 }
}
}, {
encryptionType: 'queryableEncryption'
}));
const error = await connection.openUri(clusterUri, autoEncryptionOptions()).catch(e => e);
assert.ok(error instanceof Error);
assert.match(error.message, /Cannot have duplicate keys in discriminators with encryption/);
});
it('throws on duplicate keys declared on root and child discriminators', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
model = connection.model('Schema', schema);
assert.throws(() => model.discriminator('Test', new Schema({
name: {
type: String, encrypt: { keyId: keyId2 }
}
}, {
encryptionType: 'queryableEncryption'
})),
/encrypted fields cannot be declared on both the base schema and the child schema in a discriminator\. path=name/
);
});
it('throws on duplicate keys declared on root and child discriminators, parent with fle, child without', async function() {
const schema = new Schema({
name: {
type: String, encrypt: { keyId }
}
}, {
encryptionType: 'queryableEncryption'
});
model = connection.model('Schema', schema);
assert.throws(() => model.discriminator('Test', new Schema({
name: {
type: String
}
})),
/encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=name/
);
});
it('throws on duplicate keys declared on root and child discriminators, child with fle, parent without', async function() {
const schema = new Schema({
name: String
});
model = connection.model('Schema', schema);
assert.throws(() => model.discriminator('Test', new Schema({
name: {
type: String, encrypt: { keyId: [keyId], algorithm } }
}, {
encryptionType: 'queryableEncryption'
})),
/encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=name/
);
});
});
});
describe('Nested paths in discriminators with conflicting definitions for the same key', function() {
beforeEach(async function() {
connection = createConnection();
});
describe('same definition on parent and child', function() {
it('throws an error', function() {
model = connection.model('Schema', new Schema({
name: {
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}
}, { encryptionType: 'csfle' }));
assert.throws(() => {
model.discriminator('Test', new Schema({
name: { first: { type: String, encrypt: { keyId: [keyId], algorithm } } }
}, { encryptionType: 'csfle' }));
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator. path/);
});
});
describe('child overrides parent\'s encryption', function() {
it('throws an error', function() {
model = connection.model('Schema', new Schema({
name: {
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}
}, { encryptionType: 'csfle' }));
assert.throws(() => {
model.discriminator('Test', new Schema({
name: { first: Number }
}));
}, /encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=name/);
});
});
});
describe('Nested schemas in discriminators with conflicting definitions for the same key', function() {
beforeEach(async function() {
connection = createConnection();
});
describe('same definition on parent and child', function() {
it('throws an error', function() {
model = connection.model('Schema', new Schema({
name: new Schema({
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' }));
assert.throws(() => {
model.discriminator('Test', new Schema({
name: new Schema({
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' }));
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator. path/);
});
});
describe('child overrides parent\'s encryption', function() {
it('throws an error', function() {
model = connection.model('Schema', new Schema({
name: new Schema({
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' }));
assert.throws(() => {
model.discriminator('Test', new Schema({
name: new Schema({
first: Number
})
}));
}, /encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=name.first/);
});
});
describe('multiple levels of nesting', function() {
it('throws an error', function() {
model = connection.model('Schema', new Schema({
name: new Schema({
first: new Schema({
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' }));
assert.throws(() => {
model.discriminator('Test', new Schema({
name: new Schema({
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
}, { encryptionType: 'csfle' })
}, { encryptionType: 'csfle' }));
}, /encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=name.first/);
});
});
});
});
});
describe('Encryption can be configured on the default mongoose connection', function() {
afterEach(async function() {
mongoose.deleteModel('Schema');
await mongoose.disconnect();
});
it('encrypts and decrypts', async function() {
const schema = new Schema({
a: {
type: Schema.Types.Int32,
encrypt: { keyId: [keyId], algorithm }
}
}, {
encryptionType: 'csfle'
});
const model = mongoose.model('Schema', schema);
await mongoose.connect(clusterUri, autoEncryptionOptions());
const [{ _id }] = await model.insertMany([{ a: 2 }]);
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id });
isEncryptedValue(encryptedDoc, 'a');
const doc = await model.findOne({ _id });
assert.deepEqual(doc.a, 2);
});
});
describe('Key Vault API', function() {
let connection;
let model;
afterEach(async function() {
await connection?.close();
});
describe('No FLE configured', function() {
it('returns `null`', async function() {
connection = mongoose.createConnection();
const model = connection.model('Name', { age: String });
await connection.openUri(clusterUri);
assert.equal(model.clientEncryption(), null);
});
});
describe('Client not connected', function() {
it('returns `null`', async function() {
connection = mongoose.createConnection();
const model = connection.model('Name', { age: String });
assert.equal(model.clientEncryption(), null);
});
});
describe('auto encryption enabled for a collection', function() {
beforeEach(async function() {
connection = createConnection();
model = connection.model('Model1', new Schema({
a: {
type: String,
encrypt: {
keyId
}
}
}, {
encryptionType: 'queryableEncryption'
}));
await connection.openUri(clusterUri, autoEncryptionOptions());
});
it('returns a client encryption object', async function() {
assert.ok(model.clientEncryption() instanceof mdb.ClientEncryption);
});
it('the client encryption is usable as a key vault', async function() {
const clientEncryption = model.clientEncryption();
const dataKey = await clientEncryption.createDataKey('local');
const keys = await clientEncryption.getKeys().toArray();
assert.ok(keys.length > 0);
const key = keys.find(
({ _id }) => _id.toString() === dataKey.toString()
);
assert.ok(key);
});
it('uses the same keyvaultNamespace', async function() {
assert.equal(model.clientEncryption()._keyVaultNamespace, 'keyvault.datakeys');
});
it('uses the same kms providers', async function() {
assert.deepEqual(model.clientEncryption()._kmsProviders, { local: { key: LOCAL_KEY } });
});
it('uses the same proxy options', async function() {
const options = model.collection.conn.client.options.autoEncryption;
options.proxyOptions = { name: 'bailey' };
assert.deepEqual(model.clientEncryption()._proxyOptions, { name: 'bailey' });
});
it('uses the same TLS options', async function() {
const options = model.collection.conn.client.options.autoEncryption;
options.tlsOptions = {
tlsCAFile: 'some file'
};
assert.deepEqual(model.clientEncryption()._tlsOptions, {
tlsCAFile: 'some file'
});
});
it('uses the same credentialProviders', async function() {
const options = model.collection.conn.client.options.autoEncryption;
const credentialProviders = {
aws: async() => {}
};
options.credentialProviders = credentialProviders;
options.kmsProviders = { aws: {} };
assert.equal(model.clientEncryption()._credentialProviders, credentialProviders);
});
it('uses the underlying MongoClient as the keyvault client', async function() {
const options = model.collection.conn.client.options.autoEncryption;
assert.ok(model.clientEncryption()._client === options.keyVaultClient, 'client not the same');
assert.equal(model.clientEncryption()._keyVaultClient, options.keyVaultClient, 'keyvault client not the same');
});
});
});
describe('auto index creation', function() {
let connection;
describe('CSFLE', function() {
it('automatically creates indexes for CSFLE models', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId: [keyId], algorithm } },
age: Number
}, { encryptionType: 'csfle' });
schema.index({ age: 1 });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.init();
const indexes = await model.listIndexes();
assert.ok(indexes.find(({ name }) => name === 'age_1'));
});
});
describe('Queryable Encryption', function() {
it('automatically creates indexes for QE models', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId } },
age: Number
}, { encryptionType: 'queryableEncryption' });
schema.index({ age: 1 });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.init();
const indexes = await model.listIndexes();
assert.ok(indexes.find(({ name }) => name === 'age_1'));
});
});
});
describe('auto collection creation', function() {
let connection;
afterEach(async function() {
await connection?.close();
});
describe('CSFLE', function() {
it('automatically creates the model\'s collection', async function() {
connection = mongoose.createConnection();
const name = new UUID().toHexString();
const schema = new Schema({
name: { type: String, encrypt: { keyId: [keyId], algorithm } },
age: Number
}, { encryptionType: 'csfle', autoCreate: true, collection: name });
const model = connection.model(name, schema);
await connection.openUri(clusterUri, { ... autoEncryptionOptions(), dbName: name });
await model.init();
const collections = await connection.db.listCollections({}, { readPreference: 'primary' }).map(({ name }) => name).toArray();
assert.deepEqual(collections, [name]);
});
});
describe('Queryable Encryption', function() {
it('automatically creates the model\'s collection', async function() {
connection = mongoose.createConnection();
const name = new UUID().toHexString();
const schema = new Schema({
name: { type: String, encrypt: { keyId: keyId } }
}, { encryptionType: 'queryableEncryption', autoCreate: true, collection: name });
const model = connection.model(name, schema);
await connection.openUri(clusterUri, { ... autoEncryptionOptions(), dbName: name });
await model.init();
const collections = await connection.db.listCollections({}, { readPreference: 'primary' }).map(({ name }) => name).toArray();
collections.sort((a, b) => {
const aIsEnxcol = a.includes('enxcol_');
const bIsEnxcol = b.includes('enxcol_');
if (!aIsEnxcol && bIsEnxcol) {
return -1;
}
if (aIsEnxcol && !bIsEnxcol) {
return 1;
}
return a.localeCompare(b);
});
assert.deepEqual(collections, [
name,
`enxcol_.${name}.ecoc`,
`enxcol_.${name}.esc`
]);
});
});
});
describe('read operations', function() {
let connection;
afterEach(async function() {
await connection?.close();
});
describe('CSFLE', function() {
it('encrypted documents can be read', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId: [keyId], algorithm } },
age: Number
}, { encryptionType: 'csfle', autoCreate: true });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.insertMany([
{ name: 'bailey', age: 1 },
{ name: 'john', age: 2 }
]);
assert.equal((await model.find()).length, 2);
assert.deepEqual(await model.findOne({ age: 1 }, { _id: 0, name: 1 }, { lean: true }), { name: 'bailey' });
});
it('deterministically encrypted fields can be equality queried', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId: [keyId], algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' } },
age: Number
}, { encryptionType: 'csfle', autoCreate: true });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.insertMany([
{ name: 'bailey', age: 1 },
{ name: 'john', age: 2 }
]);
assert.equal((await model.find()).length, 2);
assert.deepEqual(await model.findOne({ name: 'bailey' }, { _id: 0, name: 1 }, { lean: true }), { name: 'bailey' });
});
});
describe('QE encrypted queries', function() {
describe('when a field is configured for equality queries', function() {
it('can be queried with mongoose', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId, queries: { queryType: 'equality' } } }
}, { encryptionType: 'queryableEncryption' });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.init();
await model.insertMany([{ name: 'bailey' }, { name: 'john' }]);
const doc = await model.findOne({ name: 'bailey' });
assert.ok(doc);
});
});
describe('when a field is not configured for equality queries', function() {
it('cannot be queried directly', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId } }
}, { encryptionType: 'queryableEncryption' });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.init();
await model.insertMany([{ name: 'bailey' }, { name: 'john' }]);
await assert.rejects(() => {
return model.findOne({ name: 'bailey' });
}, /Can only execute encrypted equality queries with an encrypted equality index/);
});
});
it('queried documents can be modified and saved', async function() {
connection = mongoose.createConnection();
const schema = new Schema({
name: { type: String, encrypt: { keyId, queries: { queryType: 'equality' } } }
}, { encryptionType: 'queryableEncryption' });
const model = connection.model(new UUID().toHexString(), schema);
await connection.openUri(clusterUri, autoEncryptionOptions());
await model.init();
await model.insertMany([{ name: 'bailey' }, { name: 'john' }]);
const doc = await model.findOne({ name: 'bailey' });
doc.name = 'new name!';
await doc.save();
assert.ok(await model.findOne({ name: 'new name!' }));
});
});
});
});