Skip to content

chore: comments on CSFLE feature branch PR#2 #15407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 103 additions & 103 deletions docs/field-level-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this to the bottom of the page to emphasize the new Mongoose API for FLE.

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
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -243,3 +149,97 @@ await connection.openUri(`mongodb://localhost:27017`, {

const clientEncryption = Model.clientEncryption();
```

## 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' });
```
57 changes: 53 additions & 4 deletions test/encryption/encryption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
const { UUID } = require('mongodb/lib/bson');

/**
* @param { string } path
* @param {string} path
*
* @returns { boolean }
* @returns {boolean}
*/
function exists(path) {
try {
Expand Down Expand Up @@ -852,6 +852,55 @@
});
});

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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an ideal world, we would support this behavior because there are a number of users that rely on this pattern. However, we can consider throwing an error when using cloned schemas for disctiminator() a simplifying assumption, and add support for it later if there is demand.

age: {
type: Int32, encrypt: { keyId: [keyId], algorithm }
}
});
model.discriminator('Test', clonedSchema)

Check failure on line 876 in test/encryption/encryption.test.js

View workflow job for this annotation

GitHub Actions / Lint JS-Files

Missing semicolon
}, /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)

Check failure on line 898 in test/encryption/encryption.test.js

View workflow job for this annotation

GitHub Actions / Lint JS-Files

Missing semicolon
}, /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();
Expand Down Expand Up @@ -1130,7 +1179,7 @@
let model;

afterEach(async function() {
await connection.close();
await connection?.close();
});

describe('No FLE configured', function() {
Expand Down Expand Up @@ -1319,7 +1368,7 @@

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);
Expand Down
6 changes: 6 additions & 0 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading