Skip to content

Commit 387c957

Browse files
authored
Merge pull request #15407 from mongodb-js/csfle-review-comments-2
chore: comments on CSFLE feature branch PR#2
2 parents df21dee + 02f2521 commit 387c957

File tree

3 files changed

+162
-107
lines changed

3 files changed

+162
-107
lines changed

docs/field-level-encryption.md

Lines changed: 103 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -16,104 +16,6 @@ The resulting document will look similar to the following to a client that doesn
1616

1717
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/).
1818

19-
Note that Mongoose does **not** currently have any Mongoose-specific APIs for CSFLE.
20-
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.
21-
Mongoose schemas currently don't support CSFLE configuration.
22-
23-
## Setting Up Field Level Encryption with Mongoose
24-
25-
First, you need to install the [mongodb-client-encryption npm package](https://www.npmjs.com/package/mongodb-client-encryption).
26-
This is MongoDB's official package for setting up encryption keys.
27-
28-
```sh
29-
npm install mongodb-client-encryption
30-
```
31-
32-
You also need to make sure you've installed [mongocryptd](https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/mongocryptd/).
33-
mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption.
34-
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.
35-
[You can read more about mongocryptd here](https://www.mongodb.com/docs/v5.0/reference/security-client-side-encryption-appendix/#mongocryptd).
36-
37-
Once you've set up and run mongocryptd, first you need to create a new encryption key as follows.
38-
Keep in mind that the following example is a simple example to help you get started.
39-
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/).
40-
41-
```javascript
42-
const { ClientEncryption } = require('mongodb');
43-
const mongoose = require('mongoose');
44-
45-
run().catch(err => console.log(err));
46-
47-
async function run() {
48-
/* Step 1: Connect to MongoDB and insert a key */
49-
50-
// Create a very basic key. You're responsible for making
51-
// your key secure, don't use this in prod :)
52-
const arr = [];
53-
for (let i = 0; i < 96; ++i) {
54-
arr.push(i);
55-
}
56-
const key = Buffer.from(arr);
57-
58-
const keyVaultNamespace = 'client.encryption';
59-
const kmsProviders = { local: { key } };
60-
61-
const uri = 'mongodb://127.0.0.1:27017/mongoose_test';
62-
const conn = await mongoose.createConnection(uri, {
63-
autoEncryption: {
64-
keyVaultNamespace,
65-
kmsProviders
66-
}
67-
}).asPromise();
68-
const encryption = new ClientEncryption(conn.getClient(), {
69-
keyVaultNamespace,
70-
kmsProviders,
71-
});
72-
73-
const _key = await encryption.createDataKey('local', {
74-
keyAltNames: ['exampleKeyName'],
75-
});
76-
}
77-
```
78-
79-
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.
80-
81-
```javascript
82-
/* Step 2: connect using schema map and new key */
83-
await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
84-
// Configure auto encryption
85-
autoEncryption: {
86-
keyVaultNamespace,
87-
kmsProviders,
88-
schemaMap: {
89-
'mongoose_test.tests': {
90-
bsonType: 'object',
91-
encryptMetadata: {
92-
keyId: [_key]
93-
},
94-
properties: {
95-
name: {
96-
encrypt: {
97-
bsonType: 'string',
98-
algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
99-
}
100-
}
101-
}
102-
}
103-
}
104-
}
105-
});
106-
```
107-
108-
With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their `name` property encrypted.
109-
110-
```javascript
111-
// 'super secret' will be stored as 'BinData' in the database,
112-
// if you query using the `mongo` shell.
113-
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
114-
await Model.create({ name: 'super secret' });
115-
```
116-
11719
## Automatic FLE in Mongoose
11820

11921
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:
15052
1. Annotate the field with encryption metadata in the schema definition
15153
2. Choose an encryption type for the schema and configure the schema for the encryption type
15254

153-
Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation.
55+
Not all schematypes are supported for CSFLE and QE. For an overview of supported BSON types, refer to MongoDB's documentation.
15456

15557
### Registering Models
15658

157-
Encrypted schemas must be registered on a connection, not the Mongoose global:
59+
Encrypted schemas can be registered on the global mongoose object or on a specific connection, so long as models are registered before the connection
60+
is established:
15861

15962
```javascript
63+
// specific connection
64+
const GlobalUserModel = mongoose.model('User', encryptedUserSchema);
16065

66+
// specific connection
16167
const connection = mongoose.createConnection();
16268
const UserModel = connection.model('User', encryptedUserSchema);
16369
```
16470

16571
### Connecting and configuring encryption options
16672

167-
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.
73+
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.
16874

169-
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.
75+
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.
17076

17177
```javascript
17278
const keyVaultNamespace = 'client.encryption';
@@ -215,7 +121,7 @@ const ModelWithBirthday = model.discriminator('ModelWithBirthday', new Schema({
215121
}));
216122
```
217123

218-
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.
124+
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.
219125

220126
## Managing Data Keys
221127

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

244150
const clientEncryption = Model.clientEncryption();
245151
```
152+
153+
## Manual FLE in Mongoose
154+
155+
First, you need to install the [mongodb-client-encryption npm package](https://www.npmjs.com/package/mongodb-client-encryption).
156+
This is MongoDB's official package for setting up encryption keys.
157+
158+
```sh
159+
npm install mongodb-client-encryption
160+
```
161+
162+
You also need to make sure you've installed [mongocryptd](https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/mongocryptd/).
163+
mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption.
164+
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.
165+
[You can read more about mongocryptd here](https://www.mongodb.com/docs/v5.0/reference/security-client-side-encryption-appendix/#mongocryptd).
166+
167+
Once you've set up and run mongocryptd, first you need to create a new encryption key as follows.
168+
Keep in mind that the following example is a simple example to help you get started.
169+
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/).
170+
171+
```javascript
172+
const { ClientEncryption } = require('mongodb');
173+
const mongoose = require('mongoose');
174+
175+
run().catch(err => console.log(err));
176+
177+
async function run() {
178+
/* Step 1: Connect to MongoDB and insert a key */
179+
180+
// Create a very basic key. You're responsible for making
181+
// your key secure, don't use this in prod :)
182+
const arr = [];
183+
for (let i = 0; i < 96; ++i) {
184+
arr.push(i);
185+
}
186+
const key = Buffer.from(arr);
187+
188+
const keyVaultNamespace = 'client.encryption';
189+
const kmsProviders = { local: { key } };
190+
191+
const uri = 'mongodb://127.0.0.1:27017/mongoose_test';
192+
const conn = await mongoose.createConnection(uri, {
193+
autoEncryption: {
194+
keyVaultNamespace,
195+
kmsProviders
196+
}
197+
}).asPromise();
198+
const encryption = new ClientEncryption(conn.getClient(), {
199+
keyVaultNamespace,
200+
kmsProviders,
201+
});
202+
203+
const _key = await encryption.createDataKey('local', {
204+
keyAltNames: ['exampleKeyName'],
205+
});
206+
}
207+
```
208+
209+
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.
210+
211+
```javascript
212+
/* Step 2: connect using schema map and new key */
213+
await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
214+
// Configure auto encryption
215+
autoEncryption: {
216+
keyVaultNamespace,
217+
kmsProviders,
218+
schemaMap: {
219+
'mongoose_test.tests': {
220+
bsonType: 'object',
221+
encryptMetadata: {
222+
keyId: [_key]
223+
},
224+
properties: {
225+
name: {
226+
encrypt: {
227+
bsonType: 'string',
228+
algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
229+
}
230+
}
231+
}
232+
}
233+
}
234+
}
235+
});
236+
```
237+
238+
With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their `name` property encrypted.
239+
240+
```javascript
241+
// 'super secret' will be stored as 'BinData' in the database,
242+
// if you query using the `mongo` shell.
243+
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
244+
await Model.create({ name: 'super secret' });
245+
```

test/encryption/encryption.test.js

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ const LOCAL_KEY = Buffer.from('Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0c
1616
const { UUID } = require('mongodb/lib/bson');
1717

1818
/**
19-
* @param { string } path
19+
* @param {string} path
2020
*
21-
* @returns { boolean }
21+
* @returns {boolean}
2222
*/
2323
function exists(path) {
2424
try {
@@ -852,6 +852,55 @@ describe('encryption integration tests', () => {
852852
});
853853
});
854854

855+
describe('cloned parent schema before declaring discriminator', function() {
856+
beforeEach(async function() {
857+
connection = createConnection();
858+
});
859+
describe('csfle', function() {
860+
it('throws on duplicate keys declared on different discriminators', async function() {
861+
const schema = new Schema({
862+
name: {
863+
type: String, encrypt: { keyId: [keyId], algorithm }
864+
}
865+
}, {
866+
encryptionType: 'csfle'
867+
});
868+
model = connection.model('Schema', schema);
869+
870+
assert.throws(() => {
871+
const clonedSchema = schema.clone().add({
872+
age: {
873+
type: Int32, encrypt: { keyId: [keyId], algorithm }
874+
}
875+
});
876+
model.discriminator('Test', clonedSchema);
877+
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator/);
878+
});
879+
});
880+
881+
describe('queryable encryption', function() {
882+
it('throws on duplicate keys declared on different discriminators', async function() {
883+
const schema = new Schema({
884+
name: {
885+
type: String, encrypt: { keyId }
886+
}
887+
}, {
888+
encryptionType: 'queryableEncryption'
889+
});
890+
model = connection.model('Schema', schema);
891+
892+
assert.throws(() => {
893+
const clonedSchema = schema.clone().add({
894+
age: {
895+
type: Int32, encrypt: { keyId: [keyId], algorithm }
896+
}
897+
});
898+
model.discriminator('Test', clonedSchema);
899+
}, /encrypted fields cannot be declared on both the base schema and the child schema in a discriminator/);
900+
});
901+
});
902+
});
903+
855904
describe('duplicate keys in discriminators', function() {
856905
beforeEach(async function() {
857906
connection = createConnection();
@@ -1130,7 +1179,7 @@ describe('encryption integration tests', () => {
11301179
let model;
11311180

11321181
afterEach(async function() {
1133-
await connection.close();
1182+
await connection?.close();
11341183
});
11351184

11361185
describe('No FLE configured', function() {
@@ -1319,7 +1368,7 @@ describe('encryption integration tests', () => {
13191368

13201369
collections.sort((a, b) => {
13211370
// depending on what letter name starts with, `name` might come before the two queryable encryption collections or after them.
1322-
// this method always puts the `name` collection first, and the two QE collections after it.
1371+
// this sort function always puts the `name` collection first, and the two QE collections after it.
13231372
if (!a.includes('enxcol_')) return -1;
13241373

13251374
return a.localeCompare(b);

types/models.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,5 +919,11 @@ declare module 'mongoose' {
919919
'find',
920920
TInstanceMethods & TVirtuals
921921
>;
922+
923+
/**
924+
* If auto encryption is enabled, returns a ClientEncryption instance that is configured with the same settings that
925+
* Mongoose's underlying MongoClient is using. If the client has not yet been configured, returns null.
926+
*/
927+
clientEncryption(): mongodb.ClientEncryption | null;
922928
}
923929
}

0 commit comments

Comments
 (0)