diff --git a/docs/field-level-encryption.md b/docs/field-level-encryption.md index 49492cedf1..18226a8d37 100644 --- a/docs/field-level-encryption.md +++ b/docs/field-level-encryption.md @@ -16,104 +16,6 @@ The resulting document will look similar to the following to a client that doesn You can read more about CSFLE on the [MongoDB CSFLE documentation](https://www.mongodb.com/docs/manual/core/csfle/) and [this blog post about CSFLE in Node.js](https://www.mongodb.com/developer/languages/javascript/client-side-field-level-encryption-csfle-mongodb-node/). -Note that Mongoose does **not** currently have any Mongoose-specific APIs for CSFLE. -Mongoose defers all CSFLE-related work to the MongoDB Node.js driver, so the [`autoEncryption` option](https://mongodb.github.io/node-mongodb-native/5.6/interfaces/AutoEncryptionOptions.html) for `mongoose.connect()` and `mongoose.createConnection()` is where you put all CSFLE-related configuration. -Mongoose schemas currently don't support CSFLE configuration. - -## Setting Up Field Level Encryption with Mongoose - -First, you need to install the [mongodb-client-encryption npm package](https://www.npmjs.com/package/mongodb-client-encryption). -This is MongoDB's official package for setting up encryption keys. - -```sh -npm install mongodb-client-encryption -``` - -You also need to make sure you've installed [mongocryptd](https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/mongocryptd/). -mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption. -You can either run mongocryptd yourself, or make sure it is on the system PATH and the MongoDB Node.js driver will run it for you. -[You can read more about mongocryptd here](https://www.mongodb.com/docs/v5.0/reference/security-client-side-encryption-appendix/#mongocryptd). - -Once you've set up and run mongocryptd, first you need to create a new encryption key as follows. -Keep in mind that the following example is a simple example to help you get started. -The encryption key in the following example is insecure; MongoDB recommends using a [KMS](https://www.mongodb.com/docs/v5.0/core/security-client-side-encryption-key-management/). - -```javascript -const { ClientEncryption } = require('mongodb'); -const mongoose = require('mongoose'); - -run().catch(err => console.log(err)); - -async function run() { - /* Step 1: Connect to MongoDB and insert a key */ - - // Create a very basic key. You're responsible for making - // your key secure, don't use this in prod :) - const arr = []; - for (let i = 0; i < 96; ++i) { - arr.push(i); - } - const key = Buffer.from(arr); - - const keyVaultNamespace = 'client.encryption'; - const kmsProviders = { local: { key } }; - - const uri = 'mongodb://127.0.0.1:27017/mongoose_test'; - const conn = await mongoose.createConnection(uri, { - autoEncryption: { - keyVaultNamespace, - kmsProviders - } - }).asPromise(); - const encryption = new ClientEncryption(conn.getClient(), { - keyVaultNamespace, - kmsProviders, - }); - - const _key = await encryption.createDataKey('local', { - keyAltNames: ['exampleKeyName'], - }); -} -``` - -Once you have an encryption key, you can create a separate Mongoose connection with a [`schemaMap`](https://mongodb.github.io/node-mongodb-native/5.6/interfaces/AutoEncryptionOptions.html#schemaMap) that defines which fields are encrypted using JSON schema syntax as follows. - -```javascript -/* Step 2: connect using schema map and new key */ -await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', { - // Configure auto encryption - autoEncryption: { - keyVaultNamespace, - kmsProviders, - schemaMap: { - 'mongoose_test.tests': { - bsonType: 'object', - encryptMetadata: { - keyId: [_key] - }, - properties: { - name: { - encrypt: { - bsonType: 'string', - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - } - } - } - } - } - } -}); -``` - -With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their `name` property encrypted. - -```javascript -// 'super secret' will be stored as 'BinData' in the database, -// if you query using the `mongo` shell. -const Model = mongoose.model('Test', mongoose.Schema({ name: String })); -await Model.create({ name: 'super secret' }); -``` - ## Automatic FLE in Mongoose Mongoose supports the declaration of encrypted schemas - schemas that, when connected to a model, utilize MongoDB's Client Side @@ -150,23 +52,27 @@ To declare a field as encrypted, you must: 1. Annotate the field with encryption metadata in the schema definition 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. +Not all schematypes are supported for CSFLE and QE. For an overview of supported BSON types, refer to MongoDB's documentation. ### Registering Models -Encrypted schemas must be registered on a connection, not the Mongoose global: +Encrypted schemas can be registered on the global mongoose object or on a specific connection, so long as models are registered before the connection +is established: ```javascript +// specific connection +const GlobalUserModel = mongoose.model('User', encryptedUserSchema); +// specific connection 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 when the model's connection is established. +Field level encryption in Mongoose works by generating the encryption schema that the MongoDB driver expects for each encrypted model on the connection. This happens automatically when the model's connection is established. -Queryable encryption and CSFLE requires all the same configuration as outlined in the [MongoDB encryption in-use documentation](https://www.mongodb.com/docs/manual/core/security-in-use-encryption/), except for the schemaMap or encryptedFieldsMap options. +Queryable encryption and CSFLE require all the same configuration as outlined in the [MongoDB encryption in-use documentation](https://www.mongodb.com/docs/manual/core/security-in-use-encryption/), except for the schemaMap or encryptedFieldsMap options. ```javascript const keyVaultNamespace = 'client.encryption'; @@ -215,7 +121,7 @@ const ModelWithBirthday = model.discriminator('ModelWithBirthday', new Schema({ })); ``` -When generating encryption schemas, Mongoose merges all discriminators together for all of the 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. +When generating encryption schemas, Mongoose merges all discriminators together for all of the discriminators declared on the same namespace. As a result, discriminators that declare the same key with different types are not supported. Furthermore, all discriminators for the same namespace must share the same encryption type - it is not possible to configure discriminators on the same model for both CSFLE and Queryable Encryption. ## Managing Data Keys @@ -243,3 +149,97 @@ await connection.openUri(`mongodb://localhost:27017`, { const clientEncryption = Model.clientEncryption(); ``` + +## Manual FLE in Mongoose + +First, you need to install the [mongodb-client-encryption npm package](https://www.npmjs.com/package/mongodb-client-encryption). +This is MongoDB's official package for setting up encryption keys. + +```sh +npm install mongodb-client-encryption +``` + +You also need to make sure you've installed [mongocryptd](https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/mongocryptd/). +mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption. +You can either run mongocryptd yourself, or make sure it is on the system PATH and the MongoDB Node.js driver will run it for you. +[You can read more about mongocryptd here](https://www.mongodb.com/docs/v5.0/reference/security-client-side-encryption-appendix/#mongocryptd). + +Once you've set up and run mongocryptd, first you need to create a new encryption key as follows. +Keep in mind that the following example is a simple example to help you get started. +The encryption key in the following example is insecure; MongoDB recommends using a [KMS](https://www.mongodb.com/docs/v5.0/core/security-client-side-encryption-key-management/). + +```javascript +const { ClientEncryption } = require('mongodb'); +const mongoose = require('mongoose'); + +run().catch(err => console.log(err)); + +async function run() { + /* Step 1: Connect to MongoDB and insert a key */ + + // Create a very basic key. You're responsible for making + // your key secure, don't use this in prod :) + const arr = []; + for (let i = 0; i < 96; ++i) { + arr.push(i); + } + const key = Buffer.from(arr); + + const keyVaultNamespace = 'client.encryption'; + const kmsProviders = { local: { key } }; + + const uri = 'mongodb://127.0.0.1:27017/mongoose_test'; + const conn = await mongoose.createConnection(uri, { + autoEncryption: { + keyVaultNamespace, + kmsProviders + } + }).asPromise(); + const encryption = new ClientEncryption(conn.getClient(), { + keyVaultNamespace, + kmsProviders, + }); + + const _key = await encryption.createDataKey('local', { + keyAltNames: ['exampleKeyName'], + }); +} +``` + +Once you have an encryption key, you can create a separate Mongoose connection with a [`schemaMap`](https://mongodb.github.io/node-mongodb-native/5.6/interfaces/AutoEncryptionOptions.html#schemaMap) that defines which fields are encrypted using JSON schema syntax as follows. + +```javascript +/* Step 2: connect using schema map and new key */ +await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', { + // Configure auto encryption + autoEncryption: { + keyVaultNamespace, + kmsProviders, + schemaMap: { + 'mongoose_test.tests': { + bsonType: 'object', + encryptMetadata: { + keyId: [_key] + }, + properties: { + name: { + encrypt: { + bsonType: 'string', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + } + } + } + } + } + } +}); +``` + +With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their `name` property encrypted. + +```javascript +// 'super secret' will be stored as 'BinData' in the database, +// if you query using the `mongo` shell. +const Model = mongoose.model('Test', mongoose.Schema({ name: String })); +await Model.create({ name: 'super secret' }); +``` diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index 80ad8456c7..b78bc00f75 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -16,9 +16,9 @@ const LOCAL_KEY = Buffer.from('Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0c const { UUID } = require('mongodb/lib/bson'); /** - * @param { string } path + * @param {string} path * - * @returns { boolean } + * @returns {boolean} */ function exists(path) { try { @@ -852,6 +852,55 @@ describe('encryption integration tests', () => { }); }); + 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(); @@ -1130,7 +1179,7 @@ describe('encryption integration tests', () => { let model; afterEach(async function() { - await connection.close(); + await connection?.close(); }); describe('No FLE configured', function() { @@ -1319,7 +1368,7 @@ describe('encryption integration tests', () => { collections.sort((a, b) => { // depending on what letter name starts with, `name` might come before the two queryable encryption collections or after them. - // this method always puts the `name` collection first, and the two QE collections after it. + // this sort function always puts the `name` collection first, and the two QE collections after it. if (!a.includes('enxcol_')) return -1; return a.localeCompare(b); diff --git a/types/models.d.ts b/types/models.d.ts index 4ff5fe83ec..1571719b9b 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -919,5 +919,11 @@ declare module 'mongoose' { 'find', TInstanceMethods & TVirtuals >; + + /** + * If auto encryption is enabled, returns a ClientEncryption instance that is configured with the same settings that + * Mongoose's underlying MongoClient is using. If the client has not yet been configured, returns null. + */ + clientEncryption(): mongodb.ClientEncryption | null; } }