Skip to content

Commit 2221bf9

Browse files
all changes
1 parent 39886fb commit 2221bf9

18 files changed

+2240
-106
lines changed

.eslintrc.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ module.exports = {
1414
'**/docs/js/native.js',
1515
'!.*',
1616
'node_modules',
17-
'.git'
17+
'.git',
18+
'data',
19+
'.config'
1820
],
1921
overrides: [
2022
{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Encryption Tests
2+
3+
on:
4+
push:
5+
branches: ['master']
6+
pull_request:
7+
branches: [ 'master' ]
8+
workflow_dispatch: {}
9+
10+
permissions:
11+
contents: write
12+
pull-requests: write
13+
id-token: write
14+
15+
jobs:
16+
run-tests:
17+
permissions:
18+
# required for all workflows
19+
security-events: write
20+
id-token: write
21+
contents: write
22+
runs-on: ubuntu-latest
23+
name: Encryption tests
24+
env:
25+
FORCE_COLOR: true
26+
steps:
27+
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
28+
- name: Setup node
29+
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
30+
with:
31+
node-version: 22
32+
- name: Install Dependencies
33+
run: npm install
34+
- name: Install mongodb-client-encryption
35+
run: npm install mongodb-client-encryption
36+
- name: Run Tests
37+
run: npm run test-encryption

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,6 @@ examples/ecommerce-netlify-functions/.netlify/state.json
6767

6868
notes.md
6969
list.out
70+
71+
data
72+
*.pid

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ If you have a question about Mongoose (not a bug report) please post it to eithe
4646
* execute `npm run test-tsd` to run the typescript tests
4747
* execute `npm run ts-benchmark` to run the typescript benchmark "performance test" for a single time.
4848
* execute `npm run ts-benchmark-watch` to run the typescript benchmark "performance test" while watching changes on types folder. Note: Make sure to commit all changes before executing this command.
49+
* in order to run tests that require an cluster with encryption locally, run `npm run test-encryption`. Alternatively, you can start an encrypted cluster using the `scripts/configure-cluster-with-encryption.sh` file.
4950

5051
## Documentation
5152

docs/field-level-encryption.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,51 @@ With the above connection, if you create a model named 'Test' that uses the 'tes
112112
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
113113
await Model.create({ name: 'super secret' });
114114
```
115+
116+
## Automatic FLE in Mongoose
117+
118+
Mongoose supports the declaration of encrypted schemas - schemas that, when connected to a model, utilize MongoDB's Client Side
119+
Field Level Encryption or Queryable Encryption under the hood. Mongoose automatically generates either an `encryptedFieldsMap` or a
120+
`schemaMap` when instantiating a MongoClient and encrypts fields on write and decrypts fields on reads.
121+
122+
### Encryption types
123+
124+
MongoDB has to different automatic encryption implementations: client side field level encryption (CSFLE) and queryable encryption (QE).
125+
See [choosing an in-use encryption approach](https://www.mongodb.com/docs/v7.3/core/queryable-encryption/about-qe-csfle/#choosing-an-in-use-encryption-approach).
126+
127+
### Declaring Encrypted Schemas
128+
129+
The following schema declares two properties, `name` and `ssn`. `ssn` is encrypted using queryable encryption, and
130+
is configured for equality queries:
131+
132+
```javascript
133+
const encryptedUserSchema = new Schema({
134+
name: String,
135+
ssn: {
136+
type: String,
137+
// 1
138+
encrypt: {
139+
keyId: '<uuid string of key id>',
140+
queries: 'equality'
141+
}
142+
}
143+
// 2
144+
}, { encryptionType: 'queryable encryption' });
145+
```
146+
147+
To declare a field as encrypted, you must:
148+
149+
1. Annotate the field with encryption metadata in the schema definition
150+
2. Choose an encryption type for the schema and configure the schema for the encryption type
151+
152+
Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation.
153+
154+
### Registering Models
155+
156+
Encrypted schemas must be registered on a connection, not the Mongoose global:
157+
158+
```javascript
159+
160+
const connection = mongoose.createConnection();
161+
const UserModel = connection.model('User', encryptedUserSchema);
162+
```

lib/collection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Collection.prototype.onOpen = function() {
8181
* @api private
8282
*/
8383

84-
Collection.prototype.onClose = function() {};
84+
Collection.prototype.onClose = function() { };
8585

8686
/**
8787
* Queues a method for later execution when its

lib/connection.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
607607

608608
Connection.prototype.createCollections = async function createCollections(options = {}) {
609609
const result = {};
610-
const errorsMap = { };
610+
const errorsMap = {};
611611

612612
const { continueOnError } = options;
613613
delete options.continueOnError;
@@ -734,7 +734,7 @@ Connection.prototype.transaction = function transaction(fn, options) {
734734
throw err;
735735
}).
736736
finally(() => {
737-
session.endSession().catch(() => {});
737+
session.endSession().catch(() => { });
738738
});
739739
});
740740
};
@@ -1025,7 +1025,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10251025

10261026
for (const model of Object.values(this.models)) {
10271027
// Errors handled internally, so safe to ignore error
1028-
model.init().catch(function $modelInitNoop() {});
1028+
model.init().catch(function $modelInitNoop() { });
10291029
}
10301030

10311031
// `createConnection()` calls this `openUri()` function without
@@ -1061,7 +1061,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10611061
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10621062
Connection.prototype.on = function on(event, callback) {
10631063
if (event === 'error' && this.$initialConnection) {
1064-
this.$initialConnection.catch(() => {});
1064+
this.$initialConnection.catch(() => { });
10651065
}
10661066
return EventEmitter.prototype.on.call(this, event, callback);
10671067
};
@@ -1083,7 +1083,7 @@ Connection.prototype.on = function on(event, callback) {
10831083
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10841084
Connection.prototype.once = function on(event, callback) {
10851085
if (event === 'error' && this.$initialConnection) {
1086-
this.$initialConnection.catch(() => {});
1086+
this.$initialConnection.catch(() => { });
10871087
}
10881088
return EventEmitter.prototype.once.call(this, event, callback);
10891089
};
@@ -1412,7 +1412,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14121412
}
14131413

14141414
// Errors handled internally, so safe to ignore error
1415-
model.init().catch(function $modelInitNoop() {});
1415+
model.init().catch(function $modelInitNoop() { });
14161416

14171417
return model;
14181418
}
@@ -1439,7 +1439,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14391439
}
14401440

14411441
if (this === model.prototype.db
1442-
&& (!collection || collection === model.collection.name)) {
1442+
&& (!collection || collection === model.collection.name)) {
14431443
// model already uses this connection.
14441444

14451445
// only the first model with this name is cached to allow
@@ -1626,8 +1626,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanis
16261626
*/
16271627
Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
16281628
return (options) &&
1629-
(options.user) &&
1630-
((options.pass) || this.authMechanismDoesNotRequirePassword());
1629+
(options.user) &&
1630+
((options.pass) || this.authMechanismDoesNotRequirePassword());
16311631
};
16321632

16331633
/**
@@ -1689,7 +1689,7 @@ Connection.prototype.createClient = function createClient() {
16891689
*/
16901690
Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
16911691
const result = {};
1692-
const errorsMap = { };
1692+
const errorsMap = {};
16931693

16941694
const { continueOnError } = options;
16951695
delete options.continueOnError;

lib/drivers/node-mongodb-native/connection.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,16 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
304304
};
305305
}
306306

307+
const { schemaMap, encryptedFieldsMap } = this._buildEncryptionSchemas();
308+
309+
if (Object.keys(schemaMap).length > 0) {
310+
options.autoEncryption.schemaMap = schemaMap;
311+
}
312+
313+
if (Object.keys(encryptedFieldsMap).length > 0) {
314+
options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap;
315+
}
316+
307317
this.readyState = STATES.connecting;
308318
this._connectionString = uri;
309319

@@ -327,6 +337,40 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
327337
return this;
328338
};
329339

340+
/**
341+
* Given a connection, which may or may not have encrypted models, build
342+
* a schemaMap and/or an encryptedFieldsMap for the connection, combining all models
343+
* into a single schemaMap and encryptedFields map.
344+
*
345+
* @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption
346+
* options.
347+
*/
348+
NativeConnection.prototype._buildEncryptionSchemas = function() {
349+
const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce(
350+
(schemaMap, model) => {
351+
const { schema, collection: { collectionName } } = model;
352+
const namespace = `${this.$dbName}.${collectionName}`;
353+
schemaMap[namespace] = schema._buildSchemaMap();
354+
return schemaMap;
355+
},
356+
{}
357+
);
358+
359+
const encryptedFieldsMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'qe').reduce(
360+
(encryptedFieldsMap, model) => {
361+
const { schema, collection: { collectionName } } = model;
362+
const namespace = `${this.$dbName}.${collectionName}`;
363+
encryptedFieldsMap[namespace] = schema._buildEncryptedFields();
364+
return encryptedFieldsMap;
365+
},
366+
{}
367+
);
368+
369+
return {
370+
schemaMap, encryptedFieldsMap
371+
};
372+
};
373+
330374
/*!
331375
* ignore
332376
*/
@@ -347,7 +391,7 @@ NativeConnection.prototype.setClient = function setClient(client) {
347391

348392
for (const model of Object.values(this.models)) {
349393
// Errors handled internally, so safe to ignore error
350-
model.init().catch(function $modelInitNoop() {});
394+
model.init().catch(function $modelInitNoop() { });
351395
}
352396

353397
return this;
@@ -390,9 +434,9 @@ function _setClient(conn, client, options, dbName) {
390434
};
391435

392436
const type = client &&
393-
client.topology &&
394-
client.topology.description &&
395-
client.topology.description.type || '';
437+
client.topology &&
438+
client.topology.description &&
439+
client.topology.description.type || '';
396440

397441
if (type === 'Single') {
398442
client.on('serverDescriptionChanged', ev => {

lib/encryption_utils.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const { Array } = require('./schema/index.js');
4+
const SchemaBigInt = require('./schema/bigint');
5+
const SchemaBoolean = require('./schema/boolean');
6+
const SchemaBuffer = require('./schema/buffer');
7+
const SchemaDate = require('./schema/date');
8+
const SchemaDecimal128 = require('./schema/decimal128');
9+
const SchemaDouble = require('./schema/double');
10+
const SchemaInt32 = require('./schema/int32');
11+
const SchemaObjectId = require('./schema/objectId');
12+
const SchemaString = require('./schema/string');
13+
14+
/**
15+
* Given a schema and a path to a field in the schema, this returns the
16+
* BSON type of the field, if it can be determined. This method specifically
17+
* **only** handles BSON types that are used for CSFLE and QE - any other
18+
* BSON types will return `null`. (example: MinKey and MaxKey).
19+
*
20+
* @param {import('.').Schema} schema
21+
* @param {string} path
22+
* @returns
23+
*/
24+
function inferBSONType(schema, path) {
25+
const type = schema.path(path);
26+
27+
if (type instanceof SchemaString) {
28+
return 'string';
29+
}
30+
31+
if (type instanceof SchemaInt32) {
32+
return 'int';
33+
}
34+
35+
if (type instanceof SchemaBigInt) {
36+
return 'long';
37+
}
38+
39+
if (type instanceof SchemaBoolean) {
40+
return 'bool';
41+
}
42+
43+
if (type instanceof SchemaDate) {
44+
return 'date';
45+
}
46+
47+
if (type instanceof SchemaBuffer) {
48+
return 'binData';
49+
}
50+
51+
if (type instanceof SchemaObjectId) {
52+
return 'objectId';
53+
}
54+
55+
if (type instanceof SchemaDecimal128) {
56+
return 'decimal';
57+
}
58+
59+
if (type instanceof SchemaDouble) {
60+
return 'double';
61+
}
62+
63+
if (type instanceof Array) {
64+
return 'array';
65+
}
66+
67+
return null;
68+
}
69+
70+
module.exports = {
71+
inferBSONType
72+
};

0 commit comments

Comments
 (0)