diff --git a/docs/field-level-encryption.md b/docs/field-level-encryption.md index 8cf4f27d551..bc4480deef4 100644 --- a/docs/field-level-encryption.md +++ b/docs/field-level-encryption.md @@ -151,3 +151,68 @@ To declare a field as encrypted, you must: 2. Choose an encryption type for the schema and configure the schema for the encryption type Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation. + +### Registering Models + +Encrypted schemas must be registered on a connection, not the Mongoose global: + +```javascript + +const connection = mongoose.createConnection(); +const UserModel = connection.model('User', encryptedUserSchema); +``` + +### Connecting and configuring encryption options + +CSFLE/QE in Mongoose work by generating the encryption schema that the MongoDB driver expects for each encrypted model on the connection. This happens automatically the model's connection is established. + +Queryable encryption and CSFLE requires all the same configuration as outlined in <>, except for the schemaMap or encryptedFieldsMap options. + +```javascript +const keyVaultNamespace = 'client.encryption'; +const kmsProviders = { local: { key } }; +await connection.openUri(`mongodb://localhost:27017`, { + // Configure auto encryption + autoEncryption: { + keyVaultNamespace: 'datakeys.datakeys', + kmsProviders + } +}); +``` + +Once the connection is established, Mongoose's operations will work as usual. Writes are encrypted automatically by the MongoDB driver prior to sending them to the server and reads are decrypted by the driver after fetching documents from the server. + +### Discriminators + +Discriminators are supported for encrypted models as well: + +```javascript +const connection = createConnection(); + +const schema = new Schema({ + name: { + type: String, encrypt: { keyId } + } +}, { + encryptionType: 'queryableEncryption' +}); + +const Model = connection.model('BaseUserModel', schema); +const ModelWithAge = model.discriminator('ModelWithAge', new Schema({ + age: { + type: Int32, encrypt: { keyId: keyId2 } + } +}, { + encryptionType: 'queryableEncryption' +})); + +const ModelWithBirthday = model.discriminator('ModelWithBirthday', new Schema({ + dob: { + type: Int32, encrypt: { keyId: keyId3 } + } +}, { + encryptionType: 'queryableEncryption' +})); +``` + +When generating encryption schemas, Mongoose merges all discriminators together for the all discriminators declared on the same namespace. As a result, discriminators that declare the same key with different types are not supported. Furthermore, all discriminators must share the same encryption type - it is not possible to configure discriminators on the same model for both CSFLE and QE. diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index e96b89c9398..9e9f8952f6f 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -12,6 +12,7 @@ const pkg = require('../../../package.json'); const processConnectionOptions = require('../../helpers/processConnectionOptions'); const setTimeout = require('../../helpers/timers').setTimeout; const utils = require('../../utils'); +const Schema = require('../../schema'); /** * A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation. @@ -320,6 +321,20 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio }; } + const { schemaMap, encryptedFieldsMap } = this._buildEncryptionSchemas(); + + if ((Object.keys(schemaMap).length > 0 || Object.keys(encryptedFieldsMap).length) && !options.autoEncryption) { + throw new Error('Must provide `autoEncryption` when connecting with encrypted schemas.'); + } + + if (Object.keys(schemaMap).length > 0) { + options.autoEncryption.schemaMap = schemaMap; + } + + if (Object.keys(encryptedFieldsMap).length > 0) { + options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap; + } + this.readyState = STATES.connecting; this._connectionString = uri; @@ -343,6 +358,56 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio return this; }; +/** + * Given a connection, which may or may not have encrypted models, build + * a schemaMap and/or an encryptedFieldsMap for the connection, combining all models + * into a single schemaMap and encryptedFields map. + * + * @returns the generated schemaMap and encryptedFieldsMap + */ +NativeConnection.prototype._buildEncryptionSchemas = function() { + const qeMappings = {}; + const csfleMappings = {}; + + const encryptedModels = Object.values(this.models).filter(model => model.schema._hasEncryptedFields()); + + // If discriminators are configured for the collection, there might be multiple models + // pointing to the same namespace. For this scenario, we merge all the schemas for each namespace + // into a single schema and then generate a schemaMap/encryptedFieldsMap for the combined schema. + for (const model of encryptedModels) { + const { schema, collection: { collectionName } } = model; + const namespace = `${this.$dbName}.${collectionName}`; + const mappings = schema.encryptionType() === 'csfle' ? csfleMappings : qeMappings; + + mappings[namespace] ??= new Schema({}, { encryptionType: schema.encryptionType() }); + + const isNonRootDiscriminator = schema.discriminatorMapping && !schema.discriminatorMapping.isRoot; + if (isNonRootDiscriminator) { + const rootSchema = schema._baseSchema; + schema.eachPath((pathname) => { + if (rootSchema.path(pathname)) return; + if (!mappings[namespace]._hasEncryptedField(pathname)) return; + + throw new Error(`Cannot have duplicate keys in discriminators with encryption. key=${pathname}`); + }); + } + + mappings[namespace].add(schema); + } + + const schemaMap = Object.fromEntries(Object.entries(csfleMappings).map( + ([namespace, schema]) => ([namespace, schema._buildSchemaMap()]) + )); + + const encryptedFieldsMap = Object.fromEntries(Object.entries(qeMappings).map( + ([namespace, schema]) => ([namespace, schema._buildEncryptedFields()]) + )); + + return { + schemaMap, encryptedFieldsMap + }; +}; + /*! * ignore */ diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js index f5c421656da..4baf9ae0fb3 100644 --- a/lib/helpers/model/discriminator.js +++ b/lib/helpers/model/discriminator.js @@ -17,6 +17,53 @@ const CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = { methods: true }; +/** + * Validate fields declared on the child schema when either schema is configured for encryption. Specifically, this function ensures that: + * + * - any encrypted fields are declared on exactly one of the schemas (not both) + * - encrypted fields cannot be declared on either the parent or child schema, where the other schema declares the same field without encryption. + * + * @param {Schema} parentSchema + * @param {Schema} childSchema + */ +function validateDiscriminatorSchemasForEncryption(parentSchema, childSchema) { + if (parentSchema.encryptionType() == null && childSchema.encryptionType() == null) return; + + const allSharedNestedPaths = setIntersection( + allNestedPaths(parentSchema), + allNestedPaths(childSchema) + ); + + for (const path of allSharedNestedPaths) { + if (parentSchema._hasEncryptedField(path) && childSchema._hasEncryptedField(path)) { + throw new Error(`encrypted fields cannot be declared on both the base schema and the child schema in a discriminator. path=${path}`); + } + + if (parentSchema._hasEncryptedField(path) || childSchema._hasEncryptedField(path)) { + throw new Error(`encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=${path}`); + } + } + + function* allNestedPaths(schema) { + const { paths, singleNestedPaths } = schema; + yield* Object.keys(paths); + yield* Object.keys(singleNestedPaths); + } + + /** + * @param {Iterable<string>} i1 + * @param {Iterable<string>} i2 + */ + function* setIntersection(i1, i2) { + const s1 = new Set(i1); + for (const item of i2) { + if (s1.has(item)) { + yield item; + } + } + } +} + /*! * ignore */ @@ -80,6 +127,8 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu value = tiedValue; } + validateDiscriminatorSchemasForEncryption(model.schema, schema); + function merge(schema, baseSchema) { // Retain original schema before merging base schema schema._baseSchema = baseSchema; diff --git a/lib/schema.js b/lib/schema.js index 5679e421109..adae215cf2b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -721,7 +721,6 @@ Schema.prototype.encryptionType = function encryptionType(encryptionType) { Schema.prototype.add = function add(obj, prefix) { if (obj instanceof Schema || (obj != null && obj.instanceOfSchema)) { merge(this, obj); - return this; } @@ -914,6 +913,77 @@ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() { return Object.keys(this.encryptedFields).length > 0; }; +/** + * @api private + */ +Schema.prototype._hasEncryptedField = function _hasEncryptedField(path) { + return path in this.encryptedFields; +}; + + +/** + * Builds an encryptedFieldsMap for the schema. + */ +Schema.prototype._buildEncryptedFields = function() { + const fields = Object.entries(this.encryptedFields).map( + ([path, config]) => { + const bsonType = this.path(path).autoEncryptionType(); + // { path, bsonType, keyId, queries? } + return { path, bsonType, ...config }; + }); + + return { fields }; +}; + +/** + * Builds a schemaMap for the schema, if the schema is configured for client-side field level encryption. + */ +Schema.prototype._buildSchemaMap = function() { + /** + * `schemaMap`s are JSON schemas, which use the following structure to represent objects: + * { field: { bsonType: 'object', properties: { ... } } } + * + * for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as + * `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }` + * + * This function takes an array of path segments, an output object (that gets mutated) and + * a value to associated with the full path, and constructs a valid CSFLE JSON schema path for + * the object. This works for deeply nested properties as well. + * + * @param {string[]} path array of path components + * @param {object} object the object in which to build a JSON schema of `path`'s properties + * @param {object} value the value to associate with the path in object + */ + function buildNestedPath(path, object, value) { + let i = 0, component = path[i]; + for (; i < path.length - 1; ++i, component = path[i]) { + object[component] = object[component] == null ? { + bsonType: 'object', + properties: {} + } : object[component]; + object = object[component].properties; + } + object[component] = value; + } + + const schemaMapPropertyReducer = (accum, [path, propertyConfig]) => { + const bsonType = this.path(path).autoEncryptionType(); + const pathComponents = path.split('.'); + const configuration = { encrypt: { ...propertyConfig, bsonType } }; + buildNestedPath(pathComponents, accum, configuration); + return accum; + }; + + const properties = Object.entries(this.encryptedFields).reduce( + schemaMapPropertyReducer, + {}); + + return { + bsonType: 'object', + properties + }; +}; + /** * Add an alias for `path`. This means getting or setting the `alias` * is equivalent to getting or setting the `path`. diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js index be937eafbf5..47235f47293 100644 --- a/lib/schema/bigint.js +++ b/lib/schema/bigint.js @@ -255,7 +255,7 @@ SchemaBigInt.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaBigInt.prototype.autoEncryptionType = function autoEncryptionType() { - return 'int64'; + return 'long'; }; /*! diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js index ed478b95bf8..da2af1a9019 100644 --- a/lib/schema/boolean.js +++ b/lib/schema/boolean.js @@ -305,7 +305,7 @@ SchemaBoolean.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaBoolean.prototype.autoEncryptionType = function autoEncryptionType() { - return 'boolean'; + return 'bool'; }; /*! diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js index f9d3027367d..2e9fdc71c04 100644 --- a/lib/schema/buffer.js +++ b/lib/schema/buffer.js @@ -315,7 +315,7 @@ SchemaBuffer.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaBuffer.prototype.autoEncryptionType = function autoEncryptionType() { - return 'binary'; + return 'binData'; }; /*! diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js index b3d80d54a6c..9202a364248 100644 --- a/lib/schema/decimal128.js +++ b/lib/schema/decimal128.js @@ -236,7 +236,7 @@ SchemaDecimal128.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaDecimal128.prototype.autoEncryptionType = function autoEncryptionType() { - return 'decimal128'; + return 'decimal'; }; /*! diff --git a/lib/schema/int32.js b/lib/schema/int32.js index 65bfb66e174..81599f76d6d 100644 --- a/lib/schema/int32.js +++ b/lib/schema/int32.js @@ -261,7 +261,7 @@ SchemaInt32.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaInt32.prototype.autoEncryptionType = function autoEncryptionType() { - return 'int32'; + return 'int'; }; diff --git a/lib/schema/map.js b/lib/schema/map.js index c6de8da702b..943c76f1c76 100644 --- a/lib/schema/map.js +++ b/lib/schema/map.js @@ -95,6 +95,10 @@ class SchemaMap extends SchemaType { return result; } + + autoEncryptionType() { + return 'object'; + } } /** diff --git a/lib/schema/objectId.js b/lib/schema/objectId.js index fd379e014d1..cc2515513b0 100644 --- a/lib/schema/objectId.js +++ b/lib/schema/objectId.js @@ -305,7 +305,7 @@ SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) { }; SchemaObjectId.prototype.autoEncryptionType = function autoEncryptionType() { - return 'objectid'; + return 'objectId'; }; /*! diff --git a/scripts/configure-cluster-with-encryption.sh b/scripts/configure-cluster-with-encryption.sh index 7520e00bcd9..d7a93f8a623 100644 --- a/scripts/configure-cluster-with-encryption.sh +++ b/scripts/configure-cluster-with-encryption.sh @@ -4,11 +4,11 @@ # this script downloads all tools required to use FLE with mongodb, then starts a cluster of the provided configuration (sharded on 8.0 server) -export CWD=$(pwd); -export DRIVERS_TOOLS_PINNED_COMMIT=35d0592c76f4f3d25a5607895eb21b491dd52543; +export CWD=$(pwd) +export DRIVERS_TOOLS_PINNED_COMMIT=4e18803c074231ec9fc3ace8f966e2c49d9874bb # install extra dependency -npm install mongodb-client-encryption +npm install --no-save mongodb-client-encryption # set up mongodb cluster and encryption configuration if the data/ folder does not exist if [ ! -d "data" ]; then @@ -33,11 +33,9 @@ if [ ! -d "data" ]; then # configure cluster settings export DRIVERS_TOOLS=$CWD/data/drivers-evergreen-tools - export MONGODB_VERSION=8.0 - export AUTH=true + export AUTH=auth export MONGODB_BINARIES=$DRIVERS_TOOLS/mongodb/bin - export MONGO_ORCHESTRATION_HOME=$DRIVERS_TOOLS/mo - export PROJECT_ORCHESTRATION_HOME=$DRIVERS_TOOLS/.evergreen/orchestration + export MONGO_ORCHESTRATION_HOME=$DRIVERS_TOOLS/.evergreen/orchestration export TOPOLOGY=sharded_cluster export SSL=nossl @@ -46,12 +44,12 @@ if [ ! -d "data" ]; then mkdir mo cd - - rm expansions.sh 2> /dev/null + rm expansions.sh 2>/dev/null echo 'Configuring Cluster...' # start cluster - (bash $DRIVERS_TOOLS/.evergreen/run-orchestration.sh) 1> /dev/null 2> /dev/null + (bash $DRIVERS_TOOLS/.evergreen/run-orchestration.sh) 1>/dev/null 2>/dev/null echo 'Cluster Configuration Finished!' diff --git a/test/encryptedSchema.test.js b/test/encryptedSchema.test.js index 5134d39864e..79791b49859 100644 --- a/test/encryptedSchema.test.js +++ b/test/encryptedSchema.test.js @@ -22,94 +22,197 @@ function schemaHasEncryptedProperty(schema, path) { return path in schema.encryptedFields; } -const KEY_ID = new UUID(); +const KEY_ID = '9fbdace3-4e48-412d-88df-3807e8009522'; const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'; describe('encrypted schema declaration', function() { - describe('Tests that fields of valid schema types can be declared as encrypted schemas', function() { - const basicSchemaTypes = [ - { type: String, name: 'string' }, - { type: Schema.Types.Boolean, name: 'boolean' }, - { type: Schema.Types.Buffer, name: 'buffer' }, - { type: Date, name: 'date' }, - { type: ObjectId, name: 'objectid' }, - { type: BigInt, name: 'bigint' }, - { type: Decimal128, name: 'Decimal128' }, - { type: Int32, name: 'int32' }, - { type: Double, name: 'double' } - ]; - - for (const { type, name } of basicSchemaTypes) { - describe(`When a schema is instantiated with an encrypted field of type ${name}`, function() { + describe('schemaMap generation tests', function() { + for (const { type, name, encryptionType, schemaMap, encryptedFields } of primitiveSchemaMapTests()) { + describe(`When a schema is instantiated with an encrypted field of type ${name} for ${encryptionType}`, function() { let schema; + const encrypt = { + keyId: KEY_ID + }; + encryptionType === 'csfle' && (encrypt.algorithm = algorithm); + beforeEach(function() { schema = new Schema({ field: { - type, encrypt: { keyId: KEY_ID, algorithm } + type, encrypt } }, { - encryptionType: 'csfle' + encryptionType }); }); it(`Then the schema has an encrypted property of type ${name}`, function() { assert.ok(schemaHasEncryptedProperty(schema, 'field')); }); + + encryptionType === 'csfle' && it('then the generated schemaMap is correct', function() { + assert.deepEqual(schema._buildSchemaMap(), schemaMap); + }); + + encryptionType === 'queryableEncryption' && it('then the generated encryptedFieldsMap is correct', function() { + assert.deepEqual(schema._buildEncryptedFields(), encryptedFields); + }); }); } + }); - describe('when a schema is instantiated with a nested encrypted schema', function() { - let schema; - beforeEach(function() { - const encryptedSchema = new Schema({ - encrypted: { - type: String, encrypt: { keyId: KEY_ID, algorithm } - } - }, { encryptionType: 'csfle' }); - schema = new Schema({ - field: encryptedSchema - }, { encryptionType: 'csfle' }); + describe('Tests that fields of valid schema types can be declared as encrypted schemas', function() { + it('mongoose maps with csfle', function() { + const schema = new Schema({ + field: { + type: Schema.Types.Map, + of: String, + encrypt: { keyId: [KEY_ID], algorithm } + } + }, { encryptionType: 'csfle' }); + + assert.ok(schemaHasEncryptedProperty(schema, 'field')); + + assert.deepEqual(schema._buildSchemaMap(), { + bsonType: 'object', + properties: { + field: { encrypt: { + bsonType: 'object', algorithm, keyId: [KEY_ID] + } } + } }); + }); + + it('mongoose maps with queryableEncryption', function() { + const schema = new Schema({ + field: { + type: Schema.Types.Map, + of: String, + encrypt: { keyId: KEY_ID } + } + }, { encryptionType: 'queryableEncryption' }); + assert.ok(schemaHasEncryptedProperty(schema, 'field')); - it('then the schema has a nested property that is encrypted', function() { - assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + assert.deepEqual(schema._buildEncryptedFields(), { + fields: [ + { path: 'field', keyId: KEY_ID, bsonType: 'object' } + ] }); }); - describe('when a schema is instantiated with a nested schema object', function() { - let schema; - beforeEach(function() { - schema = new Schema({ + it('subdocument for csfle', function() { + const encryptedSchema = new Schema({ + encrypted: { + type: String, encrypt: { keyId: KEY_ID, algorithm } + } + }, { encryptionType: 'csfle' }); + const schema = new Schema({ + field: encryptedSchema + }, { encryptionType: 'csfle' }); + + assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + + assert.deepEqual(schema._buildSchemaMap(), { + bsonType: 'object', + properties: { field: { - encrypted: { - type: String, encrypt: { keyId: KEY_ID, algorithm } + bsonType: 'object', + properties: { + encrypted: { encrypt: { bsonType: 'string', algorithm, keyId: KEY_ID } } } } - }, { encryptionType: 'csfle' }); + } }); - - it('then the schema has a nested property that is encrypted', function() { - assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + }); + it('subdocument for queryableEncryption', function() { + const encryptedSchema = new Schema({ + encrypted: { + type: String, encrypt: { keyId: KEY_ID } + } + }, { encryptionType: 'queryableEncryption' }); + const schema = new Schema({ + field: encryptedSchema + }, { encryptionType: 'queryableEncryption' }); + assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + + assert.deepEqual(schema._buildEncryptedFields(), { + fields: [ + { path: 'field.encrypted', keyId: KEY_ID, bsonType: 'string' } + ] }); }); - - describe('when a schema is instantiated as an Array', function() { - let schema; - beforeEach(function() { - schema = new Schema({ + it('nested object for csfle', function() { + const schema = new Schema({ + field: { encrypted: { - type: [Number], - encrypt: { keyId: KEY_ID, algorithm } + type: String, encrypt: { keyId: KEY_ID, algorithm } } - }, { encryptionType: 'csfle' }); + } + }, { encryptionType: 'csfle' }); + assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + assert.deepEqual(schema._buildSchemaMap(), { + bsonType: 'object', + properties: { + field: { + bsonType: 'object', + properties: { + encrypted: { encrypt: { bsonType: 'string', algorithm, keyId: KEY_ID } } + } + } + } }); - - it('then the schema has a nested property that is encrypted', function() { - assert.ok(schemaHasEncryptedProperty(schema, 'encrypted')); + }); + it('nested object for queryableEncryption', function() { + const schema = new Schema({ + field: { + encrypted: { + type: String, encrypt: { keyId: KEY_ID } + } + } + }, { encryptionType: 'queryableEncryption' }); + assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + assert.deepEqual(schema._buildEncryptedFields(), { + fields: [ + { path: 'field.encrypted', keyId: KEY_ID, bsonType: 'string' } + ] }); }); + it('schema with encrypted array for csfle', function() { + const schema = new Schema({ + encrypted: { + type: [Number], + encrypt: { keyId: KEY_ID, algorithm } + } + }, { encryptionType: 'csfle' }); + assert.ok(schemaHasEncryptedProperty(schema, ['encrypted'])); + assert.deepEqual(schema._buildSchemaMap(), { + bsonType: 'object', + properties: { + encrypted: { + encrypt: { + bsonType: 'array', + keyId: KEY_ID, + algorithm + } + } + } + }); + }); + it('schema with encrypted array for queryableEncryption', function() { + const schema = new Schema({ + encrypted: { + type: [Number], + encrypt: { keyId: KEY_ID } + } + }, { encryptionType: 'queryableEncryption' }); + assert.ok(schemaHasEncryptedProperty(schema, ['encrypted'])); + assert.deepEqual(schema._buildEncryptedFields(), { + fields: [ + { path: 'encrypted', keyId: KEY_ID, bsonType: 'array' } + ] + }); + }); }); describe('invalid schema types for encrypted schemas', function() { @@ -121,7 +224,7 @@ describe('encrypted schema declaration', function() { type: Number, encrypt: { keyId: KEY_ID, algorithm } } }, { encryptionType: 'csfle' }); - }, /Invalid BSON type/); + }, /Invalid BSON type for FLE field: 'field'/); }); }); @@ -133,7 +236,7 @@ describe('encrypted schema declaration', function() { type: Schema.Types.Mixed, encrypt: { keyId: KEY_ID, algorithm } } }, { encryptionType: 'csfle' }); - }, /Invalid BSON type/); + }, /Invalid BSON type for FLE field: 'field'/); }); }); @@ -159,7 +262,7 @@ describe('encrypted schema declaration', function() { type: Int8, encrypt: { keyId: KEY_ID, algorithm } } }, { encryptionType: 'csfle' }); - }, /Invalid BSON type/); + }, /Invalid BSON type for FLE field: 'field'/); }); }); @@ -536,3 +639,476 @@ describe('encrypted schema declaration', function() { }); }); }); + +function primitiveSchemaMapTests() { + return [ + { + name: 'string', + type: String, + encryptionType: 'csfle', + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'string' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'string', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'string', + type: String, + encryptionType: 'queryableEncryption', + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'string' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'string', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'boolean', + type: Schema.Types.Boolean, + encryptionType: 'csfle', + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'bool' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'bool', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'boolean', + encryptionType: 'queryableEncryption', + type: Schema.Types.Boolean, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'bool' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'bool', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'buffer', + encryptionType: 'csfle', + type: Schema.Types.Buffer, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'binData' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'binData', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'buffer', + encryptionType: 'queryableEncryption', + type: Schema.Types.Buffer, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'binData' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'binData', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'date', + encryptionType: 'csfle', + type: Date, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'date' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'date', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'date', + encryptionType: 'queryableEncryption', + type: Date, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'date' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'date', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'objectid', + encryptionType: 'csfle', + type: ObjectId, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'objectId' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'objectId', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'objectid', + encryptionType: 'queryableEncryption', + type: ObjectId, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'objectId' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'objectId', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'bigint', + encryptionType: 'csfle', + type: BigInt, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'long' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'long', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'bigint', + encryptionType: 'queryableEncryption', + type: BigInt, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'long' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'long', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'Decimal128', + encryptionType: 'csfle', + type: Decimal128, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'decimal' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'decimal', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'Decimal128', + encryptionType: 'queryableEncryption', + type: Decimal128, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'decimal' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'decimal', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'int32', + encryptionType: 'csfle', + type: Int32, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'int' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'int', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'int32', + encryptionType: 'queryableEncryption', + type: Int32, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'int' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'int', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + }, + { + name: 'double', + encryptionType: 'csfle', + type: Double, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', + bsonType: 'double' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'double', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + ] + } + }, + { + name: 'double', + encryptionType: 'queryableEncryption', + type: Double, + schemaMap: { + bsonType: 'object', + properties: { + field: { + encrypt: { + keyId: '9fbdace3-4e48-412d-88df-3807e8009522', + bsonType: 'double' + } + } + } + }, + encryptedFields: { + fields: [ + { + path: 'field', + bsonType: 'double', + keyId: '9fbdace3-4e48-412d-88df-3807e8009522' + } + ] + } + } + ]; +} diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index 11369408b24..ee44c0f7ebf 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -1,26 +1,40 @@ 'use strict'; const assert = require('assert'); -const mongodb = require('mongodb'); -const fs = require('fs'); +const mdb = require('mongodb'); const isBsonType = require('../../lib/helpers/isBsonType'); +const { Schema, createConnection } = require('../../lib'); +const { ObjectId, Double, Int32, Decimal128 } = require('bson'); +const fs = require('fs'); +const mongoose = require('../../lib'); +const { Map } = require('../../lib/types'); const LOCAL_KEY = Buffer.from('Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk', 'base64'); -describe('ci', () => { +/** + * @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', () => { const cachedUri = process.env.MONGOOSE_TEST_URI; const cachedLib = process.env.CRYPT_SHARED_LIB_PATH; before(function() { const cwd = process.cwd(); const file = fs.readFileSync(cwd + '/data/mo-expansion.yml', { encoding: 'utf-8' }).trim().split('\n'); + + // matches `key="value"` and extracts key and value. const regex = /^(?<key>.*): "(?<value>.*)"$/; - const variables = file.map((line) => regex.exec(line.trim()).groups).reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {}); - console.log('File contents', file); - console.log('Variables', variables); - process.env.CRYPT_SHARED_LIB_PATH = variables.CRYPT_SHARED_LIB_PATH; - process.env.MONGOOSE_TEST_URI = variables.MONGODB_URI; + const variables = Object.fromEntries(file.map((line) => regex.exec(line.trim()).groups).map(({ key, value }) => [key, value])); + + process.env.CRYPT_SHARED_LIB_PATH ??= variables.CRYPT_SHARED_LIB_PATH; + process.env.MONGOOSE_TEST_URI ??= variables.MONGODB_URI; }); after(function() { @@ -28,92 +42,1269 @@ describe('ci', () => { process.env.MONGOOSE_TEST_URI = cachedUri; }); - describe('environmental variables', () => { + describe('meta: environmental variables are correctly set up', () => { it('MONGOOSE_TEST_URI is set', async function() { const uri = process.env.MONGOOSE_TEST_URI; - console.log('MONGOOSE_TEST_URI=', uri); assert.ok(uri); }); + it('MONGOOSE_TEST_URI points to running cluster', async function() { + try { + const connection = await mongoose.connect(process.env.MONGOOSE_TEST_URI); + await connection.disconnect(); + } catch (error) { + throw new Error('Unable to connect to running cluster', { cause: error }); + } + }); + it('CRYPT_SHARED_LIB_PATH is set', async function() { const shared_library_path = process.env.CRYPT_SHARED_LIB_PATH; - console.log('CRYPT_SHARED_LIB_PATH=', shared_library_path); assert.ok(shared_library_path); }); }); - describe('basic integration', () => { - let keyVaultClient; - let dataKey; - let encryptedClient; - let unencryptedClient; + const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'; + + + let keyId, keyId2, keyId3; + let utilClient; + + beforeEach(async function() { + const keyVaultClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI); + 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(process.env.MONGOOSE_TEST_URI); + }); + + 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: 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(); + }); - beforeEach(async function() { - keyVaultClient = new mongodb.MongoClient(process.env.MONGOOSE_TEST_URI); - await keyVaultClient.connect(); - await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new mongodb.ClientEncryption(keyVaultClient, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } } + for (const { type, name, input, expected } of basicSchemaTypes) { + // eslint-disable-next-line no-inner-declarations + async function test() { + const [{ _id }] = await model.insertMany([{ field: input }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.field, 'Binary')); + assert.ok(encryptedDoc.field.sub_type === 6); + + const doc = await model.findOne({ _id }); + if (Buffer.isBuffer(input)) { + // mongoose's Buffer does not support deep equality - instead use the Buffer.equals method. + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + }); + + it(`${name} encrypts and decrypts`, test); }); - dataKey = await clientEncryption.createDataKey('local'); - encryptedClient = new mongodb.MongoClient( - process.env.MONGOOSE_TEST_URI, - { - autoEncryption: { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } }, - schemaMap: { - 'db.coll': { - bsonType: 'object', - encryptMetadata: { - keyId: [dataKey] - }, - properties: { - a: { - encrypt: { - bsonType: 'int', - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random', - keyId: [dataKey] - } + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + }); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: { + name: 'bailey' + } }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: { + name: 'bailey' + } }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: { b: { c: 'hello' } } }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a.b.c, 'Binary')); + assert.ok(encryptedDoc.a.b.c.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: [new Int32(3)] }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: [{ name: 'bailey' }] }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: [new Int32(3)] }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: [{ name: 'bailey' }] }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: 'hello', b: 1n, c: { d: 'world' } }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + assert.ok(typeof encryptedDoc.b === 'number'); + assert.ok(isBsonType(encryptedDoc.c.d, 'Binary')); + assert.ok(encryptedDoc.c.d.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + { + const [{ _id }] = await model1.insertMany([{ a: 'hello' }]); + const encryptedDoc = await utilClient.db('db').collection('model1').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + 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 }); + + assert.ok(isBsonType(encryptedDoc.b, 'Binary')); + assert.ok(encryptedDoc.b.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, extraOptions: { cryptdSharedLibRequired: true, cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH } } + }); + + { + const [{ _id }] = await model1.insertMany([{ a: 'hello' }]); + const encryptedDoc = await utilClient.db('db').collection('model1').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + const doc = await model1.findOne({ _id }); + assert.deepEqual(doc.a, 'hello'); } - ); - unencryptedClient = new mongodb.MongoClient(process.env.MONGOOSE_TEST_URI); + { + const [{ _id }] = await model2.insertMany([{ b: 'world' }]); + const encryptedDoc = await utilClient.db('db').collection('model2').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.b, 'Binary')); + assert.ok(encryptedDoc.b.sub_type === 6); + + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + }); + 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + }); + 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('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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }).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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }).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 } } } // Different type, no encryption, stored as same field in MDB + }, { 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 } // Different type, no encryption, stored as same field in MDB + })); + }, /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' }) // Different type, no encryption, stored as same field in MDB + }, { 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' }) // Different type, no encryption, stored as same field in MDB + }, { 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(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); + + const [{ _id }] = await model.insertMany([{ a: 2 }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); + + const doc = await model.findOne({ _id }); + assert.deepEqual(doc.a, 2); + }); + }); + + describe('Encryption can be configured on the default mongoose connection', function() { afterEach(async function() { - await keyVaultClient.close(); - await encryptedClient.close(); - await unencryptedClient.close(); + 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 } - it('ci set-up should support basic mongodb auto-encryption integration', async() => { - await encryptedClient.connect(); - const { insertedId } = await encryptedClient.db('db').collection('coll').insertOne({ a: 1 }); + } + }, { + encryptionType: 'csfle' + }); + + const model = mongoose.model('Schema', schema); + await mongoose.connect(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } + }); - // client not configured with autoEncryption, returns a encrypted binary type, meaning that encryption succeeded - const encryptedResult = await unencryptedClient.db('db').collection('coll').findOne({ _id: insertedId }); + const [{ _id }] = await model.insertMany([{ a: 2 }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); - assert.ok(encryptedResult); - assert.ok(encryptedResult.a); - assert.ok(isBsonType(encryptedResult.a, 'Binary')); - assert.ok(encryptedResult.a.sub_type === 6); + assert.ok(isBsonType(encryptedDoc.a, 'Binary')); + assert.ok(encryptedDoc.a.sub_type === 6); - // when the encryptedClient runs a find, the original unencrypted value is returned - const unencryptedResult = await encryptedClient.db('db').collection('coll').findOne({ _id: insertedId }); - assert.ok(unencryptedResult); - assert.ok(unencryptedResult.a === 1); + const doc = await model.findOne({ _id }); + assert.deepEqual(doc.a, 2); }); }); });