From 1593236967950b6ccdc19ff7d0780f418644c4d4 Mon Sep 17 00:00:00 2001 From: bailey Date: Fri, 25 Jul 2025 13:41:27 -0600 Subject: [PATCH 01/11] sync tests, update build script to use my fork of libmongocrypt to test changes --- .../install-mongodb-client-encryption.sh | 2 +- .../client_side_encryption.spec.test.ts | 2 +- ...-Text-cleanupStructuredEncryptionData.json | 217 +++++++ ...E-Text-cleanupStructuredEncryptionData.yml | 128 ++++ ...-Text-compactStructuredEncryptionData.json | 259 +++++++++ ...E-Text-compactStructuredEncryptionData.yml | 137 +++++ .../tests/unified/QE-Text-prefixPreview.json | 336 +++++++++++ .../tests/unified/QE-Text-prefixPreview.yml | 225 +++++++ .../unified/QE-Text-substringPreview.json | 549 ++++++++++++++++++ .../unified/QE-Text-substringPreview.yml | 472 +++++++++++++++ .../tests/unified/QE-Text-suffixPreview.json | 336 +++++++++++ .../tests/unified/QE-Text-suffixPreview.yml | 221 +++++++ 12 files changed, 2882 insertions(+), 2 deletions(-) create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml diff --git a/.evergreen/install-mongodb-client-encryption.sh b/.evergreen/install-mongodb-client-encryption.sh index 1bea94950b8..66881a9de9b 100644 --- a/.evergreen/install-mongodb-client-encryption.sh +++ b/.evergreen/install-mongodb-client-encryption.sh @@ -9,7 +9,7 @@ if [ -z ${PROJECT_DIRECTORY+omitted} ]; then echo "PROJECT_DIRECTORY is unset" & source $DRIVERS_TOOLS/.evergreen/init-node-and-npm-env.sh rm -rf mongodb-client-encryption -git clone https://github.com/mongodb-js/mongodb-client-encryption.git +git clone https://github.com/baileympearson/mongodb-client-encryption.git -b NODE-7059 pushd mongodb-client-encryption node --version diff --git a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts index 96afcecf941..570c3725f4d 100644 --- a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts @@ -62,7 +62,7 @@ describe('Client Side Encryption (Legacy)', function () { }); }); -describe('Client Side Encryption (Unified)', function () { +describe.only('Client Side Encryption (Unified)', function () { runUnifiedSuite( loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')), ({ description }, configuration) => { diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json new file mode 100644 index 00000000000..a6c70c50864 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json @@ -0,0 +1,217 @@ +{ + "description": "QE-Text-cleanupStructuredEncryptionData", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "8.2.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "QE Text cleanupStructuredEncryptionData works", + "operations": [ + { + "name": "runCommand", + "object": "db", + "arguments": { + "command": { + "cleanupStructuredEncryptionData": "coll" + }, + "commandName": "cleanupStructuredEncryptionData" + }, + "expectResult": { + "ok": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "cleanupStructuredEncryptionData": "coll", + "cleanupTokens": { + "encryptedText": { + "ecoc": { + "$binary": { + "base64": "SWO8WEoZ2r2Kx/muQKb7+COizy85nIIUFiHh4K9kcvA=", + "subType": "00" + } + }, + "anchorPaddingToken": { + "$binary": { + "base64": "YAiF7Iwhqq1UyfxPvm70xfQJtrIRPrjfD2yRLG1+saQ=", + "subType": "00" + } + } + } + } + }, + "commandName": "cleanupStructuredEncryptionData" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml new file mode 100644 index 00000000000..c15aa3bcdf0 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml @@ -0,0 +1,128 @@ +description: QE-Text-cleanupStructuredEncryptionData +schemaVersion: "1.23" +runOnRequirements: + # Requires libmongocrypt 1.15.0 for SPM-4158. + - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: true +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "QE Text cleanupStructuredEncryptionData works" + operations: + - name: runCommand + object: *db + arguments: + command: + cleanupStructuredEncryptionData: *coll + commandName: cleanupStructuredEncryptionData + expectResult: { ok: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + { + "cleanupStructuredEncryptionData": *coll, + "cleanupTokens": { + "encryptedText": { + "ecoc": { + "$binary": { + "base64": "SWO8WEoZ2r2Kx/muQKb7+COizy85nIIUFiHh4K9kcvA=", + "subType": "00" + } + }, + "anchorPaddingToken": { + "$binary": { + "base64": "YAiF7Iwhqq1UyfxPvm70xfQJtrIRPrjfD2yRLG1+saQ=", + "subType": "00" + } + } + } + } + } + commandName: cleanupStructuredEncryptionData diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json new file mode 100644 index 00000000000..9e0045bb2db --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json @@ -0,0 +1,259 @@ +{ + "description": "QE-Text-compactStructuredEncryptionData", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "8.2.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "QE Text compactStructuredEncryptionData works", + "operations": [ + { + "name": "runCommand", + "object": "db", + "arguments": { + "command": { + "compactStructuredEncryptionData": "coll" + }, + "commandName": "compactStructuredEncryptionData" + }, + "expectResult": { + "ok": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "compactStructuredEncryptionData": "coll", + "encryptionInformation": { + "type": { + "$numberInt": "1" + }, + "schema": { + "db.coll": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ], + "strEncodeVersion": { + "$numberInt": "1" + }, + "escCollection": "enxcol_.coll.esc", + "ecocCollection": "enxcol_.coll.ecoc" + } + } + }, + "compactionTokens": { + "encryptedText": { + "ecoc": { + "$binary": { + "base64": "SWO8WEoZ2r2Kx/muQKb7+COizy85nIIUFiHh4K9kcvA=", + "subType": "00" + } + }, + "anchorPaddingToken": { + "$binary": { + "base64": "YAiF7Iwhqq1UyfxPvm70xfQJtrIRPrjfD2yRLG1+saQ=", + "subType": "00" + } + } + } + } + }, + "commandName": "compactStructuredEncryptionData" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml new file mode 100644 index 00000000000..49b5d83453d --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml @@ -0,0 +1,137 @@ +description: QE-Text-compactStructuredEncryptionData +schemaVersion: "1.23" +runOnRequirements: + # Requires libmongocrypt 1.15.0 for SPM-4158. + - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: true +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: &encryptedFields + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "QE Text compactStructuredEncryptionData works" + operations: + - name: runCommand + object: *db + arguments: + command: + compactStructuredEncryptionData: *coll + commandName: compactStructuredEncryptionData + expectResult: { ok: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + compactStructuredEncryptionData: *coll + encryptionInformation: + type: { "$numberInt": "1" } + schema: + db.coll: + <<: *encryptedFields + # libmongocrypt applies strEncodeVersion, escCollection, and ecocCollection: + strEncodeVersion: { "$numberInt": "1" } + escCollection: "enxcol_.coll.esc" + ecocCollection: "enxcol_.coll.ecoc" + compactionTokens: + encryptedText: + ecoc: + { + "$binary": + { + "base64": "SWO8WEoZ2r2Kx/muQKb7+COizy85nIIUFiHh4K9kcvA=", + "subType": "00", + }, + } + anchorPaddingToken: + { + "$binary": + { + "base64": "YAiF7Iwhqq1UyfxPvm70xfQJtrIRPrjfD2yRLG1+saQ=", + "subType": "00", + }, + } + commandName: compactStructuredEncryptionData diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json new file mode 100644 index 00000000000..8eccf32c776 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json @@ -0,0 +1,336 @@ +{ + "description": "QE-Text-prefixPreview", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "8.2.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "prefixPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE prefixPreview", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 1, + "encryptedText": { + "$$type": "binData" + } + } + ], + "ordered": true + }, + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Query with matching $encStrStartsWith", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrStartsWith": { + "input": "$encryptedText", + "prefix": "foo" + } + } + } + }, + "object": "coll", + "expectResult": [ + { + "_id": { + "$numberInt": "1" + }, + "encryptedText": "foobar", + "__safeContent__": [ + { + "$binary": { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "fmUMXTMV/XRiN0IL3VXxSEn6SQG9E6Po30kJKB8JJlQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "vZIDMiFDgjmLNYVrrbnq1zT4hg7sGpe/PMtighSsnRc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "26Z5G+sHTzV3D7F8Y0m08389USZ2afinyFV3ez9UEBQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "q/JEq8of7bE0QE5Id0XuOsNQ4qVpANYymcPQDUL2Ywk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "Uvvv46LkfbgLoPqZ6xTBzpgoYRTM6FUgRdqZ9eaVojI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "nMxdq2lladuBJA3lv3JC2MumIUtRJBNJVLp3PVE6nQk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "hS3V0qq5CF/SkTl3ZWWWgXcAJ8G5yGtkY2RwcHNc5Oc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "McgwYUxfKj5+4D0vskZymy4KA82s71MR25iV/Enutww=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "Ciqdk1b+t+Vrr6oIlFFk0Zdym5BPmwN3glQ0/VcsVdM=", + "subType": "00" + } + } + ] + } + ] + } + ] + }, + { + "description": "Query with non-matching $encStrStartsWith", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrStartsWith": { + "input": "$encryptedText", + "prefix": "bar" + } + } + } + }, + "object": "coll", + "expectResult": [] + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml new file mode 100644 index 00000000000..ad616354234 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml @@ -0,0 +1,225 @@ +description: QE-Text-prefixPreview +schemaVersion: "1.23" +runOnRequirements: + # Requires libmongocrypt 1.15.0 for SPM-4158. + - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: true +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + # Use zero contention for deterministic __safeContent__: + { + "queryType": "prefixPreview", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE prefixPreview" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + expectEvents: + - client: "client" + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + insert: *coll + documents: + - { "_id": 1, "encryptedText": { $$type: "binData" } } # Sends encrypted payload + ordered: true + commandName: insert + - description: "Query with matching $encStrStartsWith" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrStartsWith: { input: "$encryptedText", prefix: "foo" }, + }, + } + object: *coll + expectResult: + [ + { + "_id": { "$numberInt": "1" }, + "encryptedText": "foobar", + "__safeContent__": + [ + { + "$binary": + { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "fmUMXTMV/XRiN0IL3VXxSEn6SQG9E6Po30kJKB8JJlQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "vZIDMiFDgjmLNYVrrbnq1zT4hg7sGpe/PMtighSsnRc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "26Z5G+sHTzV3D7F8Y0m08389USZ2afinyFV3ez9UEBQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "q/JEq8of7bE0QE5Id0XuOsNQ4qVpANYymcPQDUL2Ywk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "Uvvv46LkfbgLoPqZ6xTBzpgoYRTM6FUgRdqZ9eaVojI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "nMxdq2lladuBJA3lv3JC2MumIUtRJBNJVLp3PVE6nQk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "hS3V0qq5CF/SkTl3ZWWWgXcAJ8G5yGtkY2RwcHNc5Oc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "McgwYUxfKj5+4D0vskZymy4KA82s71MR25iV/Enutww=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "Ciqdk1b+t+Vrr6oIlFFk0Zdym5BPmwN3glQ0/VcsVdM=", + "subType": "00", + }, + }, + ], + }, + ] + + - description: "Query with non-matching $encStrStartsWith" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrStartsWith: { input: "$encryptedText", prefix: "bar" }, + }, + } + object: *coll + expectResult: [] diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json new file mode 100644 index 00000000000..ace41fd3e5f --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json @@ -0,0 +1,549 @@ +{ + "description": "QE-Text-substringPreview", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "8.2.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "substringPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "10" + }, + "strMaxLength": { + "$numberLong": "20" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE suffixPreview", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 1, + "encryptedText": { + "$$type": "binData" + } + } + ], + "ordered": true + }, + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Query with matching $encStrContains", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrContains": { + "input": "$encryptedText", + "substring": "oba" + } + } + } + }, + "object": "coll", + "expectResult": [ + { + "_id": { + "$numberInt": "1" + }, + "encryptedText": "foobar", + "__safeContent__": [ + { + "$binary": { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "IpY3x/jjm8j/74jAdUhgxdM5hk68zR0zv/lTKm/72Vg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "G+ky260C6QiOfIxKz14FmaMbAxvui1BKJO/TnLOHlGk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "7dv3gAKe9vwJMZmpB40pRCwRTmc7ds9UkGhxH8j084E=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "o0V+Efn6x8XQdE80F1tztNaT3qxHjcsd9DOQ47BtmQk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "sJvrCjyVot7PIZFsdRehWFANKAj6fmBaj3FLbz/dZLE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "e98auxFmu02h5MfBIARk29MI7hSmvN3F9DaQ0xjqoEM=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "US83krGNov/ezL6IhsY5eEOCxv1xUPDIEL/nmY0IKi0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "P2Aq5+OHZPG0CWIdmZvWq9c/18ZKVYW3vbxd+WU/TXU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "8AdPRPnSzcd5uhq4TZfNvNeF0XjLNVwAsJJMTtktw84=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "9O6u/G51I4ZHFLhL4ZLuudbr0s202A2QnPfThmOXPhI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "N7AjYVyVlv6+lVSTM+cIxRL3SMgs3G5LgxSs+jrgDkI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "RbGF7dQbPGYQFd9DDO1hPz1UlLOJ77FAC6NsjGwJeos=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "m7srHMgKm6kZwsNx8rc45pmw0/9Qro6xuQ8lZS3+RYk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "K75CNU3JyKFqZWPiIsVi4+n7DhYmcPl/nEhQ3d88mVI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "c7bwGpUZc/7JzEnMS7qQ/TPuXZyrmMihFaAV6zIqbZc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "rDvEdUgEk8u4Srt3ETokWs2FXcnyJaRGQ+NbkFwi2rQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "VcdZj9zfveRBRlpCR2OYWau2+GokOFb73TE3gpElNiU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "eOa9o2xfA6OgkbYUxd6wQJicaeN6guhy2V66W3ALsaA=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "1xGkJh+um70XiRd8lKLDtyHgDqrf7/59Mg7X0+KZh8k=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "OSvllqHxycbcZN4phR6NDujY3ttA59o7nQJ6V9eJpX0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "ZTX1pyk8Vdw0BSbJx7GeJNcQf3tGKxbrrNSTqBqUWkg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "cn7V05zb5iXwYrePGMHztC+GRq+Tj8IMpRDraauPhSE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "E9bV9KyrZxHJSUmMg0HrDK4gGN+75ruelAnrM6hXQgY=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "WrssTNmdgXoTGpbaF0JLRCGH6cDQuz1XEFNTy98nrb0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "jZmyOJP35dsxQ/OY5U4ISpVRIYr8iedNfcwZiKt29Qc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "d2mocORMbX9MX+/itAW8r1kxVw2/uii4vzXtc+2CIRQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "JBnJy58eRPhDo3DuZvsHbvQDiHXxdtAx1Eif66k5SfA=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "OjbDulC8s62v0pgweBSsQqtJjJBwH5JinfJpj7nVr+A=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "85i7KT2GP9nSda3Gsil5LKubhq0LDtc22pxBxHpR+nE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "u9Fvsclwrs9lwIcMPV/fMZD7L3d5anSfJQVjQb9mgLg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "LZ32ttmLJGOIw9oFaUCn3Sx5uHPTYJPSFpeGRWNqlUc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "mMsZvGEePTqtl0FJAL/jAdyWNQIlpwN61YIlZsSIZ6s=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "XZcu1a/ZGsIzAl3j4MXQlLo4v2p7kvIqRHtIQYFmL6k=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "Zse27LinlYCEnX6iTmJceI33mEJxFb0LdPxp0RiMOaQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "vOv2Hgb2/sBpnX9XwFbIN6yDxhjchwlmczUf82W2tp4=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "oQxZ9A6j3x5j6x1Jqw/N9tpP4rfWMjcV3y+a3PkrL7c=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "/D7ew3EijyUnmT22awVFspcuyo3JChJcDeCPwpljzVM=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "BEmmwqyamt9X3bcWDld61P01zquy8fBHAXq3SHAPP0M=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "wygD9/kAo1KsRvtr1v+9/lvqoWdKwgh6gDHvAQfXPPk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "pRTKgF/uksrF1c1AcfSTY6ZhqBKVud1vIztQ4/36SLs=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "C4iUo8oNJsjJ37BqnBgIgSQpf99X2Bb4W5MZEAmakHU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "icoE53jIq6Fu/YGKUiSUTYyZ8xdiTQY9jJiGxVJObpw=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "oubCwk0V6G2RFWtcOnYDU4uUBoXBrhBRi4nZgrYj9JY=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "IyqhQ9nGhzEi5YW2W6v1kGU5DY2u2qSqbM/qXdLdWVU=", + "subType": "00" + } + } + ] + } + ] + } + ] + }, + { + "description": "Query with non-matching $encStrContains", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrContains": { + "input": "$encryptedText", + "substring": "blah" + } + } + } + }, + "object": "coll", + "expectResult": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml new file mode 100644 index 00000000000..f47c9cc5ae1 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml @@ -0,0 +1,472 @@ +description: QE-Text-suffixPreview +schemaVersion: "1.23" +runOnRequirements: + # Requires libmongocrypt 1.15.0 for SPM-4158. + - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: true +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + # Use zero contention for deterministic __safeContent__: + { + "queryType": "substringPreview", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "10" }, + "strMaxLength": { "$numberLong": "20" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE suffixPreview" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + expectEvents: + - client: "client" + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + insert: *coll + documents: + - { "_id": 1, "encryptedText": { $$type: "binData" } } # Sends encrypted payload + ordered: true + commandName: insert + - description: "Query with matching $encStrContains" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrContains: + { input: "$encryptedText", substring: "oba" }, + }, + } + object: *coll + expectResult: + [ + { + "_id": { "$numberInt": "1" }, + "encryptedText": "foobar", + "__safeContent__": + [ + { + "$binary": + { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "IpY3x/jjm8j/74jAdUhgxdM5hk68zR0zv/lTKm/72Vg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "G+ky260C6QiOfIxKz14FmaMbAxvui1BKJO/TnLOHlGk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "7dv3gAKe9vwJMZmpB40pRCwRTmc7ds9UkGhxH8j084E=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "o0V+Efn6x8XQdE80F1tztNaT3qxHjcsd9DOQ47BtmQk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "sJvrCjyVot7PIZFsdRehWFANKAj6fmBaj3FLbz/dZLE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "e98auxFmu02h5MfBIARk29MI7hSmvN3F9DaQ0xjqoEM=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "US83krGNov/ezL6IhsY5eEOCxv1xUPDIEL/nmY0IKi0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "P2Aq5+OHZPG0CWIdmZvWq9c/18ZKVYW3vbxd+WU/TXU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "8AdPRPnSzcd5uhq4TZfNvNeF0XjLNVwAsJJMTtktw84=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "9O6u/G51I4ZHFLhL4ZLuudbr0s202A2QnPfThmOXPhI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "N7AjYVyVlv6+lVSTM+cIxRL3SMgs3G5LgxSs+jrgDkI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "RbGF7dQbPGYQFd9DDO1hPz1UlLOJ77FAC6NsjGwJeos=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "m7srHMgKm6kZwsNx8rc45pmw0/9Qro6xuQ8lZS3+RYk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "K75CNU3JyKFqZWPiIsVi4+n7DhYmcPl/nEhQ3d88mVI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "c7bwGpUZc/7JzEnMS7qQ/TPuXZyrmMihFaAV6zIqbZc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "rDvEdUgEk8u4Srt3ETokWs2FXcnyJaRGQ+NbkFwi2rQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "VcdZj9zfveRBRlpCR2OYWau2+GokOFb73TE3gpElNiU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "eOa9o2xfA6OgkbYUxd6wQJicaeN6guhy2V66W3ALsaA=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "1xGkJh+um70XiRd8lKLDtyHgDqrf7/59Mg7X0+KZh8k=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "OSvllqHxycbcZN4phR6NDujY3ttA59o7nQJ6V9eJpX0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "ZTX1pyk8Vdw0BSbJx7GeJNcQf3tGKxbrrNSTqBqUWkg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "cn7V05zb5iXwYrePGMHztC+GRq+Tj8IMpRDraauPhSE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "E9bV9KyrZxHJSUmMg0HrDK4gGN+75ruelAnrM6hXQgY=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "WrssTNmdgXoTGpbaF0JLRCGH6cDQuz1XEFNTy98nrb0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "jZmyOJP35dsxQ/OY5U4ISpVRIYr8iedNfcwZiKt29Qc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "d2mocORMbX9MX+/itAW8r1kxVw2/uii4vzXtc+2CIRQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "JBnJy58eRPhDo3DuZvsHbvQDiHXxdtAx1Eif66k5SfA=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "OjbDulC8s62v0pgweBSsQqtJjJBwH5JinfJpj7nVr+A=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "85i7KT2GP9nSda3Gsil5LKubhq0LDtc22pxBxHpR+nE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "u9Fvsclwrs9lwIcMPV/fMZD7L3d5anSfJQVjQb9mgLg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "LZ32ttmLJGOIw9oFaUCn3Sx5uHPTYJPSFpeGRWNqlUc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "mMsZvGEePTqtl0FJAL/jAdyWNQIlpwN61YIlZsSIZ6s=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "XZcu1a/ZGsIzAl3j4MXQlLo4v2p7kvIqRHtIQYFmL6k=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "Zse27LinlYCEnX6iTmJceI33mEJxFb0LdPxp0RiMOaQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "vOv2Hgb2/sBpnX9XwFbIN6yDxhjchwlmczUf82W2tp4=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "oQxZ9A6j3x5j6x1Jqw/N9tpP4rfWMjcV3y+a3PkrL7c=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "/D7ew3EijyUnmT22awVFspcuyo3JChJcDeCPwpljzVM=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "BEmmwqyamt9X3bcWDld61P01zquy8fBHAXq3SHAPP0M=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "wygD9/kAo1KsRvtr1v+9/lvqoWdKwgh6gDHvAQfXPPk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "pRTKgF/uksrF1c1AcfSTY6ZhqBKVud1vIztQ4/36SLs=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "C4iUo8oNJsjJ37BqnBgIgSQpf99X2Bb4W5MZEAmakHU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "icoE53jIq6Fu/YGKUiSUTYyZ8xdiTQY9jJiGxVJObpw=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "oubCwk0V6G2RFWtcOnYDU4uUBoXBrhBRi4nZgrYj9JY=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "IyqhQ9nGhzEi5YW2W6v1kGU5DY2u2qSqbM/qXdLdWVU=", + "subType": "00", + }, + }, + ], + }, + ] + + - description: "Query with non-matching $encStrContains" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrContains: { input: "$encryptedText", substring: "blah" }, + }, + } + object: *coll + expectResult: [] diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json new file mode 100644 index 00000000000..efa95edae27 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json @@ -0,0 +1,336 @@ +{ + "description": "QE-Text-suffixPreview", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "8.2.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "suffixPreview", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE suffixPreview", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 1, + "encryptedText": { + "$$type": "binData" + } + } + ], + "ordered": true + }, + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Query with matching $encStrStartsWith", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrEndsWith": { + "input": "$encryptedText", + "suffix": "bar" + } + } + } + }, + "object": "coll", + "expectResult": [ + { + "_id": { + "$numberInt": "1" + }, + "encryptedText": "foobar", + "__safeContent__": [ + { + "$binary": { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "uDCWsucUsJemUP7pmeb+Kd8B9qupVzI8wnLFqX1rkiU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "W3E1x4bHZ8SEHFz4zwXM0G5Z5WSwBhnxE8x5/qdP6JM=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "6g/TXVDDf6z+ntResIvTKWdmIy4ajQ1rhwdNZIiEG7A=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "hU+u/T3D6dHDpT3d/v5AlgtRoAufCXCAyO2jQlgsnCw=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "vrPnq0AtBIURNgNGA6HJL+5/p5SBWe+qz8505TRo/dE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "W5pylBxdv2soY2NcBfPiHDVLTS6tx+0ULkI8gysBeFY=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "oWO3xX3x0bYUJGK2S1aPAmlU3Xtfsgb9lTZ6flGAlsg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "SjZGucTEUbdpd86O8yj1pyMyBOOKxvAQ9C8ngZ9C5UE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "CEaMZkxVDVbnXr+To0DOyvsva04UQkIYP3KtgYVVwf8=", + "subType": "00" + } + } + ] + } + ] + } + ] + }, + { + "description": "Query with non-matching $encStrEndsWith", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrEndsWith": { + "input": "$encryptedText", + "suffix": "foo" + } + } + } + }, + "object": "coll", + "expectResult": [] + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml new file mode 100644 index 00000000000..18ac4c65534 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml @@ -0,0 +1,221 @@ +description: QE-Text-suffixPreview +schemaVersion: "1.23" +runOnRequirements: + # Requires libmongocrypt 1.15.0 for SPM-4158. + - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: true +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + # Use zero contention for deterministic __safeContent__: + { + "queryType": "suffixPreview", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE suffixPreview" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + expectEvents: + - client: "client" + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + insert: *coll + documents: + - { "_id": 1, "encryptedText": { $$type: "binData" } } # Sends encrypted payload + ordered: true + commandName: insert + - description: "Query with matching $encStrStartsWith" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { $encStrEndsWith: { input: "$encryptedText", suffix: "bar" } }, + } + object: *coll + expectResult: + [ + { + "_id": { "$numberInt": "1" }, + "encryptedText": "foobar", + "__safeContent__": + [ + { + "$binary": + { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "uDCWsucUsJemUP7pmeb+Kd8B9qupVzI8wnLFqX1rkiU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "W3E1x4bHZ8SEHFz4zwXM0G5Z5WSwBhnxE8x5/qdP6JM=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "6g/TXVDDf6z+ntResIvTKWdmIy4ajQ1rhwdNZIiEG7A=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "hU+u/T3D6dHDpT3d/v5AlgtRoAufCXCAyO2jQlgsnCw=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "vrPnq0AtBIURNgNGA6HJL+5/p5SBWe+qz8505TRo/dE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "W5pylBxdv2soY2NcBfPiHDVLTS6tx+0ULkI8gysBeFY=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "oWO3xX3x0bYUJGK2S1aPAmlU3Xtfsgb9lTZ6flGAlsg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "SjZGucTEUbdpd86O8yj1pyMyBOOKxvAQ9C8ngZ9C5UE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "CEaMZkxVDVbnXr+To0DOyvsva04UQkIYP3KtgYVVwf8=", + "subType": "00", + }, + }, + ], + }, + ] + + - description: "Query with non-matching $encStrEndsWith" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { $encStrEndsWith: { input: "$encryptedText", suffix: "foo" } }, + } + object: *coll + expectResult: [] From d92fb6a5308849cbf29d79709902133a522b6efb Mon Sep 17 00:00:00 2001 From: bailey Date: Fri, 25 Jul 2025 14:04:00 -0600 Subject: [PATCH 02/11] revert test changes --- .../client-side-encryption/client_side_encryption.spec.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts index 570c3725f4d..96afcecf941 100644 --- a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts @@ -62,7 +62,7 @@ describe('Client Side Encryption (Legacy)', function () { }); }); -describe.only('Client Side Encryption (Unified)', function () { +describe('Client Side Encryption (Unified)', function () { runUnifiedSuite( loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')), ({ description }, configuration) => { From b78e3b7abfe65a021b0837bbb4fb9a11fb2f7f59 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 31 Jul 2025 12:01:51 -0600 Subject: [PATCH 03/11] adopt changes to unified test format 1.25 --- ...-Text-cleanupStructuredEncryptionData.json | 6 ++-- ...E-Text-cleanupStructuredEncryptionData.yml | 6 ++-- ...-Text-compactStructuredEncryptionData.json | 6 ++-- ...E-Text-compactStructuredEncryptionData.yml | 6 ++-- .../tests/unified/QE-Text-prefixPreview.json | 6 ++-- .../tests/unified/QE-Text-prefixPreview.yml | 6 ++-- .../unified/QE-Text-substringPreview.json | 8 +++-- .../unified/QE-Text-substringPreview.yml | 8 ++--- .../tests/unified/QE-Text-suffixPreview.json | 6 ++-- .../tests/unified/QE-Text-suffixPreview.yml | 6 ++-- test/tools/unified-spec-runner/schema.ts | 6 +++- .../unified-spec-runner/unified-utils.ts | 34 ++++++++++++------- 12 files changed, 64 insertions(+), 40 deletions(-) diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json index a6c70c50864..24f33ab3ecb 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json @@ -1,6 +1,6 @@ { "description": "QE-Text-cleanupStructuredEncryptionData", - "schemaVersion": "1.23", + "schemaVersion": "1.25", "runOnRequirements": [ { "minServerVersion": "8.2.0", @@ -9,7 +9,9 @@ "sharded", "load-balanced" ], - "csfle": true + "csfle": { + "minLibmongocryptVersion": "1.15.0" + } } ], "createEntities": [ diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml index c15aa3bcdf0..a326cca63db 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.yml @@ -1,10 +1,10 @@ description: QE-Text-cleanupStructuredEncryptionData -schemaVersion: "1.23" +schemaVersion: "1.25" runOnRequirements: - # Requires libmongocrypt 1.15.0 for SPM-4158. - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. - csfle: true + csfle: + minLibmongocryptVersion: 1.15.0 # For SPM-4158. createEntities: - client: id: &client "client" diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json index 9e0045bb2db..c7abfe2d4bc 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json @@ -1,6 +1,6 @@ { "description": "QE-Text-compactStructuredEncryptionData", - "schemaVersion": "1.23", + "schemaVersion": "1.25", "runOnRequirements": [ { "minServerVersion": "8.2.0", @@ -9,7 +9,9 @@ "sharded", "load-balanced" ], - "csfle": true + "csfle": { + "minLibmongocryptVersion": "1.15.0" + } } ], "createEntities": [ diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml index 49b5d83453d..994e6209eab 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.yml @@ -1,10 +1,10 @@ description: QE-Text-compactStructuredEncryptionData -schemaVersion: "1.23" +schemaVersion: "1.25" runOnRequirements: - # Requires libmongocrypt 1.15.0 for SPM-4158. - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. - csfle: true + csfle: + minLibmongocryptVersion: 1.15.0 # For SPM-4158. createEntities: - client: id: &client "client" diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json index 8eccf32c776..7279385743f 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json @@ -1,6 +1,6 @@ { "description": "QE-Text-prefixPreview", - "schemaVersion": "1.23", + "schemaVersion": "1.25", "runOnRequirements": [ { "minServerVersion": "8.2.0", @@ -9,7 +9,9 @@ "sharded", "load-balanced" ], - "csfle": true + "csfle": { + "minLibmongocryptVersion": "1.15.0" + } } ], "createEntities": [ diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml index ad616354234..6f228e2d708 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefixPreview.yml @@ -1,10 +1,10 @@ description: QE-Text-prefixPreview -schemaVersion: "1.23" +schemaVersion: "1.25" runOnRequirements: - # Requires libmongocrypt 1.15.0 for SPM-4158. - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. - csfle: true + csfle: + minLibmongocryptVersion: 1.15.0 # For SPM-4158. createEntities: - client: id: &client "client" diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json index ace41fd3e5f..6a8f133eac5 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json @@ -1,6 +1,6 @@ { "description": "QE-Text-substringPreview", - "schemaVersion": "1.23", + "schemaVersion": "1.25", "runOnRequirements": [ { "minServerVersion": "8.2.0", @@ -9,7 +9,9 @@ "sharded", "load-balanced" ], - "csfle": true + "csfle": { + "minLibmongocryptVersion": "1.15.0" + } } ], "createEntities": [ @@ -546,4 +548,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml index f47c9cc5ae1..cee6a9f7ca4 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml @@ -1,10 +1,10 @@ -description: QE-Text-suffixPreview -schemaVersion: "1.23" +description: QE-Text-substringPreview +schemaVersion: "1.25" runOnRequirements: - # Requires libmongocrypt 1.15.0 for SPM-4158. - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. - csfle: true + csfle: + minLibmongocryptVersion: 1.15.0 # For SPM-4158. createEntities: - client: id: &client "client" diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json index efa95edae27..deec5e63b07 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json @@ -1,6 +1,6 @@ { "description": "QE-Text-suffixPreview", - "schemaVersion": "1.23", + "schemaVersion": "1.25", "runOnRequirements": [ { "minServerVersion": "8.2.0", @@ -9,7 +9,9 @@ "sharded", "load-balanced" ], - "csfle": true + "csfle": { + "minLibmongocryptVersion": "1.15.0" + } } ], "createEntities": [ diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml index 18ac4c65534..9a6925a2a1c 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffixPreview.yml @@ -1,10 +1,10 @@ description: QE-Text-suffixPreview -schemaVersion: "1.23" +schemaVersion: "1.25" runOnRequirements: - # Requires libmongocrypt 1.15.0 for SPM-4158. - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. - csfle: true + csfle: + minLibmongocryptVersion: 1.15.0 # For SPM-4158. createEntities: - client: id: &client "client" diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index 3fc60407a71..8ec850979fa 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -113,7 +113,11 @@ export interface RunOnRequirement { minServerVersion?: string; topologies?: TopologyName[]; serverParameters?: Document; - csfle?: boolean; + csfle?: + | boolean + | { + minLibmongocryptVersion?: string; + }; } export type ObservableCommandEventId = | 'commandStartedEvent' diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index f966f10d378..79ba247f8e0 100644 --- a/test/tools/unified-spec-runner/unified-utils.ts +++ b/test/tools/unified-spec-runner/unified-utils.ts @@ -1,6 +1,6 @@ import { AssertionError, expect } from 'chai'; import ConnectionString from 'mongodb-connection-string-url'; -import { gte as semverGte, lte as semverLte } from 'semver'; +import { coerce, gte as semverGte, lte as semverLte } from 'semver'; import { isDeepStrictEqual } from 'util'; /* eslint-disable @typescript-eslint/no-restricted-imports */ @@ -115,23 +115,33 @@ export async function topologySatisfies( } } - if (typeof r.csfle === 'boolean') { - const versionSupportsCSFLE = semverGte(config.version, '4.2.0'); + if (r.csfle != null) { const csfleEnabled = config.clientSideEncryption.enabled; - if (r.csfle) { - ok &&= versionSupportsCSFLE && csfleEnabled; + if (r.csfle === false) { + ok &&= !csfleEnabled; if (!ok && skipReason == null) { - skipReason = versionSupportsCSFLE - ? `requires csfle to run but CSFLE is not set for this environment` - : 'requires mongodb >= 4.2 to run csfle tests'; + skipReason = `forbids csfle to run but CSFLE is set for this environment`; } } else { - ok &&= !(csfleEnabled && versionSupportsCSFLE); + // first, confirm that CSFLE is enabled + ok &&= csfleEnabled; if (!ok && skipReason == null) { - skipReason = versionSupportsCSFLE - ? `forbids csfle to run but CSFLE is set for this environment` - : 'forbids mongodb >= 4.2 to run csfle tests'; + skipReason = `requires csfle to run but CSFLE is not set for this environment`; + } + + // then, confirm that libmongocrypt satisfies the version constraint (if it exists) + const minLibmongocryptVersion: string | undefined = + typeof r.csfle === 'object' && r.csfle.minLibmongocryptVersion; + + if (typeof minLibmongocryptVersion === 'string') { + ok &&= semverGte( + coerce(config.clientSideEncryption.libmongocrypt), + coerce(minLibmongocryptVersion) + ); + if (!ok && skipReason == null) { + skipReason = `requires libmongocrypt>=${minLibmongocryptVersion} but ${config.clientSideEncryption.libmongocrypt} is being used.`; + } } } } From a9ca0a47fccb070e7da2e3720a0baf08be828b43 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 7 Aug 2025 13:52:37 -0600 Subject: [PATCH 04/11] explicit encryption support --- .../client_encryption.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 82a804c9356..fde63b5b5dd 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -749,7 +749,8 @@ export class ClientEncryption { expressionMode: boolean, options: ClientEncryptionEncryptOptions ): Promise { - const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions } = options; + const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions, textOptions } = + options; const contextOptions: ExplicitEncryptionContextOptions = { expressionMode, algorithm @@ -782,6 +783,11 @@ export class ClientEncryption { contextOptions.rangeOptions = serialize(rangeOptions); } + if (typeof textOptions === 'object') { + // @ts-expect-error errors until mongodb-client-encryption release + contextOptions.textOptions = serialize(textOptions); + } + const valueBuffer = serialize({ v: value }); const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, @@ -812,7 +818,8 @@ export interface ClientEncryptionEncryptOptions { | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' | 'Indexed' | 'Unindexed' - | 'Range'; + | 'Range' + | 'TextPreview'; /** * The id of the Binary dataKey to use for encryption @@ -830,10 +837,33 @@ export interface ClientEncryptionEncryptOptions { /** * The query type. */ - queryType?: 'equality' | 'range'; + queryType?: 'equality' | 'range' | 'prefixPreview' | 'suffixPreview' | 'substringPreview'; /** The index options for a Queryable Encryption field supporting "range" queries.*/ rangeOptions?: RangeOptions; + + textOptions?: TextQueryOptions; +} + +interface TextQueryOptions { + caseSensitive: boolean; + diacraticSensitive: boolean; + + prefix?: { + strMaxQueryLength: Int32 | number; + strMinQueryLength: Int32 | number; + }; + + suffix?: { + strMaxQueryLength: Int32 | number; + strMinQueryLength: Int32 | number; + }; + + substring?: { + strMaxLength: Int32 | number; + strMaxQueryLength: Int32 | number; + strMinQueryLength: Int32 | number; + }; } /** From 210e80aea8f951b7a89b26edd4a30aaf7dfd4be6 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 7 Aug 2025 15:18:32 -0600 Subject: [PATCH 05/11] changes --- .../client_encryption.ts | 50 ++--- ...e_encryption.prose.27.text_queries.test.ts | 180 ++++++++++++++++++ .../data/encryptedFields-prefix-suffix.json | 38 ++++ .../etc/data/encryptedFields-substring.json | 30 +++ 4 files changed, 273 insertions(+), 25 deletions(-) create mode 100644 test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts create mode 100644 test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json create mode 100644 test/spec/client-side-encryption/etc/data/encryptedFields-substring.json diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index fde63b5b5dd..7a8cb172b3f 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -591,14 +591,14 @@ export class ClientEncryption { field == null || typeof field !== 'object' || field.keyId != null ? field : { - ...field, - keyId: await this.createDataKey(provider, { - masterKey, - // clone the timeoutContext - // in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations - timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined - }) - } + ...field, + keyId: await this.createDataKey(provider, { + masterKey, + // clone the timeoutContext + // in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations + timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined + }) + } ); const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises); @@ -814,12 +814,12 @@ export interface ClientEncryptionEncryptOptions { * The algorithm to use for encryption. */ algorithm: - | 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' - | 'Indexed' - | 'Unindexed' - | 'Range' - | 'TextPreview'; + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' + | 'Indexed' + | 'Unindexed' + | 'Range' + | 'TextPreview'; /** * The id of the Binary dataKey to use for encryption @@ -847,7 +847,7 @@ export interface ClientEncryptionEncryptOptions { interface TextQueryOptions { caseSensitive: boolean; - diacraticSensitive: boolean; + diacriticSensitive: boolean; prefix?: { strMaxQueryLength: Int32 | number; @@ -873,11 +873,11 @@ interface TextQueryOptions { export interface ClientEncryptionRewrapManyDataKeyProviderOptions { provider: ClientEncryptionDataKeyProvider; masterKey?: - | AWSEncryptionKeyOptions - | AzureEncryptionKeyOptions - | GCPEncryptionKeyOptions - | KMIPEncryptionKeyOptions - | undefined; + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | KMIPEncryptionKeyOptions + | undefined; } /** @@ -1066,11 +1066,11 @@ export interface ClientEncryptionCreateDataKeyProviderOptions { * Identifies a new KMS-specific key used to encrypt the new data key */ masterKey?: - | AWSEncryptionKeyOptions - | AzureEncryptionKeyOptions - | GCPEncryptionKeyOptions - | KMIPEncryptionKeyOptions - | undefined; + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | KMIPEncryptionKeyOptions + | undefined; /** * An optional list of string alternate names used to reference a key. diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts new file mode 100644 index 00000000000..811aace8e05 --- /dev/null +++ b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts @@ -0,0 +1,180 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { type Binary, type Document, EJSON } from 'bson'; +import { expect } from 'chai'; + +import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; +import { + ClientEncryption, + type ClientEncryptionEncryptOptions, + type MongoClient +} from '../../mongodb'; + +const metadata: MongoDBMetadataUI = { + requires: { + clientSideEncryption: '>=6.4.0', + mongodb: '>=8.2.0', + topology: '!single' + } +}; + +const loadFLEDataFile = (filename: string) => + readFile(join(__dirname, '../../spec/client-side-encryption/etc/data', filename), { + encoding: 'utf-8' + }); + +describe.only('27. Text Explicit Encryption', function () { + let encryptedFields: Document; + let keyDocument1: Document; + let keyId1: Binary; + let client: MongoClient; + let keyVaultClient: MongoClient; + let clientEncryption: ClientEncryption; + let encryptedClient: MongoClient; + let encryptOpts: ClientEncryptionEncryptOptions; + + beforeEach(async function () { + encryptedFields = EJSON.parse(await loadFLEDataFile('encryptedFields-prefix-suffix.json'), { + relaxed: false + }); + keyDocument1 = EJSON.parse(await loadFLEDataFile('keys/key1-document.json'), { + relaxed: false + }); + + keyId1 = keyDocument1._id; + client = this.configuration.newClient(); + + await client + .db('db') + .dropCollection('explicit_encryption', { writeConcern: { w: 'majority' }, encryptedFields }); + await client.db('db').createCollection('explicit_encryption', { + writeConcern: { w: 'majority' }, + encryptedFields + }); + + // Drop and create the collection `keyvault.datakeys`. + // Insert `key1Document` in `keyvault.datakeys` with majority write concern. + await client.db('keyvault').dropCollection('datakeys', { writeConcern: { w: 'majority' } }); + await client.db('keyvault').createCollection('datakeys', { writeConcern: { w: 'majority' } }); + await client + .db('keyvault') + .collection('datakeys') + .insertOne(keyDocument1, { writeConcern: { w: 'majority' } }); + + // Create a MongoClient named `keyVaultClient`. + keyVaultClient = this.configuration.newClient(); + + // Create a ClientEncryption object named `clientEncryption` with these options: + // class ClientEncryptionOpts { + // keyVaultClient: , + // keyVaultNamespace: "keyvault.datakeys", + // kmsProviders: { "local": { "key": } }, + // } + clientEncryption = new ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { + local: getCSFLEKMSProviders().local + } + }); + + // Create a MongoClient named `encryptedClient` with these `AutoEncryptionOpts`: + // class AutoEncryptionOpts { + // keyVaultNamespace: "keyvault.datakeys", + // kmsProviders: { "local": { "key": } }, + // bypassQueryAnalysis: true, + // } + encryptedClient = this.configuration.newClient( + {}, + { + autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { + local: getCSFLEKMSProviders().local + }, + bypassQueryAnalysis: true + } + } + ); + + encryptOpts = { + keyId: keyId1, + contentionFactor: 0, + algorithm: 'TextPreview', + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + }, + suffix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + }, + substring: { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }; + + const encryptedText = await clientEncryption.encrypt('foobarbaz', { + keyId: keyId1, + algorithm: 'TextPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + }, + suffix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }); + + await encryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('explicit_encryption') + .insertOne({ + _id: 0, + encryptedText + }); + }); + + afterEach(async function () { + await Promise.allSettled([client.close(), encryptedClient.close(), keyVaultClient.close()]); + }); + + it('works', async function () { + const encryptedFoo = await clientEncryption.encrypt('foo', { + keyId: keyId1, + algorithm: 'TextPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }); + + const filter = { + $expr: { $encStrStartsWith: { input: 'encryptedText', prefix: encryptedFoo } } + }; + + expect( + await encryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('explicit_encryption') + .findOne(filter) + ).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); +}); diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json new file mode 100644 index 00000000000..e8b9f653b72 --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json @@ -0,0 +1,38 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "prefixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + }, + { + "queryType": "suffixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} \ No newline at end of file diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json new file mode 100644 index 00000000000..cb97a167175 --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json @@ -0,0 +1,30 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encrypted-textPreview", + "bsonType": "string", + "queries": [ + { + "queryType": "substringPreview", + "strMaxLength": { + "$numberInt": "10" + }, + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} From 86ec38653c85c619a9ec46580c29d7d1fa8d0246 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 12:47:05 -0600 Subject: [PATCH 06/11] add doc comments to PR --- .../client_encryption.ts | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 7a8cb172b3f..aeaeeba5eb0 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -591,14 +591,14 @@ export class ClientEncryption { field == null || typeof field !== 'object' || field.keyId != null ? field : { - ...field, - keyId: await this.createDataKey(provider, { - masterKey, - // clone the timeoutContext - // in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations - timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined - }) - } + ...field, + keyId: await this.createDataKey(provider, { + masterKey, + // clone the timeoutContext + // in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations + timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined + }) + } ); const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises); @@ -784,7 +784,6 @@ export class ClientEncryption { } if (typeof textOptions === 'object') { - // @ts-expect-error errors until mongodb-client-encryption release contextOptions.textOptions = serialize(textOptions); } @@ -814,12 +813,12 @@ export interface ClientEncryptionEncryptOptions { * The algorithm to use for encryption. */ algorithm: - | 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' - | 'Indexed' - | 'Unindexed' - | 'Range' - | 'TextPreview'; + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' + | 'Indexed' + | 'Unindexed' + | 'Range' + | 'TextPreview'; /** * The id of the Binary dataKey to use for encryption @@ -842,26 +841,45 @@ export interface ClientEncryptionEncryptOptions { /** The index options for a Queryable Encryption field supporting "range" queries.*/ rangeOptions?: RangeOptions; + /** + * Options for a Queryable Encryption field supporting text queries. Only valid when `algorithm` is `TextPreview`. + * + * @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time. + */ textOptions?: TextQueryOptions; } +/** + * Options for a Queryable Encryption field supporting text queries. + * + * @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time. + */ interface TextQueryOptions { + /** Indicates that text indexes for this field are case sensitive */ caseSensitive: boolean; + /** Indicates that text indexes for this field are diacritic sensitive. */ diacriticSensitive: boolean; prefix?: { + /** The maximum allowed query length. */ strMaxQueryLength: Int32 | number; + /** The minimum allowed query length. */ strMinQueryLength: Int32 | number; }; suffix?: { + /** The maximum allowed query length. */ strMaxQueryLength: Int32 | number; + /** The minimum allowed query length. */ strMinQueryLength: Int32 | number; }; substring?: { + /** The maximum allowed length to insert. */ strMaxLength: Int32 | number; + /** The maximum allowed query length. */ strMaxQueryLength: Int32 | number; + /** The minimum allowed query length. */ strMinQueryLength: Int32 | number; }; } @@ -873,11 +891,11 @@ interface TextQueryOptions { export interface ClientEncryptionRewrapManyDataKeyProviderOptions { provider: ClientEncryptionDataKeyProvider; masterKey?: - | AWSEncryptionKeyOptions - | AzureEncryptionKeyOptions - | GCPEncryptionKeyOptions - | KMIPEncryptionKeyOptions - | undefined; + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | KMIPEncryptionKeyOptions + | undefined; } /** @@ -1066,11 +1084,11 @@ export interface ClientEncryptionCreateDataKeyProviderOptions { * Identifies a new KMS-specific key used to encrypt the new data key */ masterKey?: - | AWSEncryptionKeyOptions - | AzureEncryptionKeyOptions - | GCPEncryptionKeyOptions - | KMIPEncryptionKeyOptions - | undefined; + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | KMIPEncryptionKeyOptions + | undefined; /** * An optional list of string alternate names used to reference a key. From 4d417575db048ea7060520686457821a9103b3e3 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 13 Aug 2025 09:47:55 -0600 Subject: [PATCH 07/11] all --- global.d.ts | 3 +- ...e_encryption.prose.27.text_queries.test.ts | 430 +++++++++++++++--- .../client_side_encryption.spec.test.ts | 2 +- .../etc/data/encryptedFields-substring.json | 10 +- .../filters/client_encryption_filter.ts | 2 +- .../filters/libmongocrypt_version_filter.ts | 37 ++ test/tools/runner/hooks/configuration.ts | 2 + 7 files changed, 419 insertions(+), 67 deletions(-) create mode 100644 test/tools/runner/filters/libmongocrypt_version_filter.ts diff --git a/global.d.ts b/global.d.ts index 5ba7f1d6865..13cd171c476 100644 --- a/global.d.ts +++ b/global.d.ts @@ -20,7 +20,8 @@ declare global { idmsMockServer?: true; nodejs?: string; predicate?: (test?: Mocha.Test) => true | string; - crypt_shared?: 'enabled' | 'disabled' + crypt_shared?: 'enabled' | 'disabled', + libmongocrypt?: string; }; sessions?: { diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts index 811aace8e05..7e1b13da0c8 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts @@ -5,59 +5,70 @@ import { type Binary, type Document, EJSON } from 'bson'; import { expect } from 'chai'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; -import { - ClientEncryption, - type ClientEncryptionEncryptOptions, - type MongoClient -} from '../../mongodb'; +import { ClientEncryption, type MongoClient, MongoDBCollectionNamespace } from '../../mongodb'; const metadata: MongoDBMetadataUI = { requires: { clientSideEncryption: '>=6.4.0', mongodb: '>=8.2.0', - topology: '!single' + topology: '!single', + libmongocrypt: '>=1.15.1' } }; -const loadFLEDataFile = (filename: string) => - readFile(join(__dirname, '../../spec/client-side-encryption/etc/data', filename), { - encoding: 'utf-8' - }); +const loadFLEDataFile = async (filename: string) => + EJSON.parse( + await readFile(join(__dirname, '../../spec/client-side-encryption/etc/data', filename), { + encoding: 'utf-8' + }), + { relaxed: false } + ); describe.only('27. Text Explicit Encryption', function () { - let encryptedFields: Document; let keyDocument1: Document; let keyId1: Binary; - let client: MongoClient; + let utilClient: MongoClient; let keyVaultClient: MongoClient; let clientEncryption: ClientEncryption; let encryptedClient: MongoClient; - let encryptOpts: ClientEncryptionEncryptOptions; beforeEach(async function () { - encryptedFields = EJSON.parse(await loadFLEDataFile('encryptedFields-prefix-suffix.json'), { - relaxed: false - }); - keyDocument1 = EJSON.parse(await loadFLEDataFile('keys/key1-document.json'), { - relaxed: false - }); + utilClient = this.configuration.newClient(); + + // Using QE CreateCollection() and Collection.Drop(), drop and create the following collections: + // - db.prefix-suffix using the encryptedFields option set to the contents of encryptedFields-prefix-suffix.json + // - db.substring using the encryptedFields option set to the contents of encryptedFields-substring.json + async function dropAndCreateCollection(ns: string, encryptedFields?: Document) { + const { db, collection } = MongoDBCollectionNamespace.fromString(ns); + await utilClient.db(db).dropCollection(collection, { + writeConcern: { w: 'majority' }, + encryptedFields + }); + await utilClient.db(db).createCollection(collection, { + writeConcern: { w: 'majority' }, + encryptedFields + }); + } + await dropAndCreateCollection( + 'db.prefix-suffix', + await loadFLEDataFile('encryptedFields-prefix-suffix.json') + ); + await dropAndCreateCollection( + 'db.substring', + await loadFLEDataFile('encryptedFields-substring.json') + ); + // Load the file key1-document.json as key1Document. + keyDocument1 = await loadFLEDataFile('keys/key1-document.json'); + + // Read the "_id" field of key1Document as key1ID. keyId1 = keyDocument1._id; - client = this.configuration.newClient(); - await client - .db('db') - .dropCollection('explicit_encryption', { writeConcern: { w: 'majority' }, encryptedFields }); - await client.db('db').createCollection('explicit_encryption', { - writeConcern: { w: 'majority' }, - encryptedFields - }); + // Drop and create the collection keyvault.datakeys. + await dropAndCreateCollection('keyvault.datakeys'); - // Drop and create the collection `keyvault.datakeys`. // Insert `key1Document` in `keyvault.datakeys` with majority write concern. - await client.db('keyvault').dropCollection('datakeys', { writeConcern: { w: 'majority' } }); - await client.db('keyvault').createCollection('datakeys', { writeConcern: { w: 'majority' } }); - await client + await utilClient .db('keyvault') .collection('datakeys') .insertOne(keyDocument1, { writeConcern: { w: 'majority' } }); @@ -97,32 +108,187 @@ describe.only('27. Text Explicit Encryption', function () { } ); - encryptOpts = { + { + const encryptedText = await clientEncryption.encrypt('foobarbaz', { + keyId: keyId1, + algorithm: 'TextPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + }, + suffix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }); + + await encryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('prefix-suffix') + .insertOne({ + _id: 0, + encryptedText + }); + } + + { + const encryptedText = await clientEncryption.encrypt('foobarbaz', { + keyId: keyId1, + algorithm: 'TextPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }); + + await encryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('substring') + .insertOne({ + _id: 0, + encryptedText + }); + } + }); + + afterEach(async function () { + await Promise.allSettled([utilClient.close(), encryptedClient.close(), keyVaultClient.close()]); + }); + + it('Case 1: can find a document by prefix', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "prefixPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // prefix: PrefixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, - contentionFactor: 0, algorithm: 'TextPreview', + queryType: 'prefixPreview', + contentionFactor: 0, textOptions: { caseSensitive: true, diacriticSensitive: true, prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 - }, + } + } + }); + + // Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter: + // { $expr: { $encStrStartsWith: {input: '$encryptedText', prefix: } } } + + const filter = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedFoo } } + }; + + const { __safeContent__, ...result } = await encryptedClient + .db('db') + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: any; + }>('prefix-suffix') + .findOne(filter); + + // Assert the following document is returned: + // { "_id": 0, "encryptedText": "foobarbaz" } + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); + + it('Case 2: can find a document by suffix', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "suffixPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // suffix: SuffixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedBaz = await clientEncryption.encrypt('baz', { + keyId: keyId1, + algorithm: 'TextPreview', + queryType: 'suffixPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 - }, - substring: { - strMaxLength: 10, - strMaxQueryLength: 10, - strMinQueryLength: 2 } } + }); + + // Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter: + // { $expr: { $encStrEndsWith: {input: '$encryptedText', suffix: } } } + const filter = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedBaz } } }; - const encryptedText = await clientEncryption.encrypt('foobarbaz', { + const { __safeContent__, ...result } = await encryptedClient + .db('db') + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: any; + }>('prefix-suffix') + .findOne(filter); + + // Assert the following document is returned: + // { "_id": 0, "encryptedText": "foobarbaz" } + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); + + it('Case 3: assert no document found by prefix', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "prefixPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // prefix: PrefixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, algorithm: 'TextPreview', + queryType: 'prefixPreview', contentionFactor: 0, textOptions: { caseSensitive: true, @@ -130,7 +296,43 @@ describe.only('27. Text Explicit Encryption', function () { prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 - }, + } + } + }); + + // Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter: + // { $expr: { $encStrStartsWith: {input: '$encryptedText', prefix: } } } + // Assert that no documents are returned. + const filter = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedBaz } } + }; + expect(await encryptedClient.db('db').collection('prefix-suffix').findOne(filter)).to.be.null; + }); + + it('Case 4: assert no document found by suffix', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "suffixPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // suffix: SuffixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedFoo = await clientEncryption.encrypt('foo', { + keyId: keyId1, + algorithm: 'TextPreview', + queryType: 'suffixPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 @@ -138,43 +340,153 @@ describe.only('27. Text Explicit Encryption', function () { } }); - await encryptedClient + const filter = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedFoo } } + }; + + const result = await encryptedClient .db('db') - .collection<{ _id: number; encryptedText: Binary }>('explicit_encryption') - .insertOne({ - _id: 0, - encryptedText - }); + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: any; + }>('prefix-suffix') + .findOne(filter); + expect(result).to.be.null; }); - afterEach(async function () { - await Promise.allSettled([client.close(), encryptedClient.close(), keyVaultClient.close()]); + it('Case 5: can find a document by substring', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "bar" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "substringPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // substring: SubstringOpts { + // strMaxLength: 10, + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedFoo = await clientEncryption.encrypt('bar', { + keyId: keyId1, + algorithm: 'TextPreview', + queryType: 'substringPreview', + contentionFactor: 0, + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }); + + // Use encryptedClient to run a "find" operation on the db.substring collection with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedFoo } } + }; + + const { __safeContent__, ...result } = await encryptedClient + .db('db') + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: any; + }>('substring') + .findOne(filter); + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); }); - it('works', async function () { - const encryptedFoo = await clientEncryption.encrypt('foo', { + it('Case 6: assert no document found by substring', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "bar" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "substringPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // substring: SubstringOpts { + // strMaxLength: 10, + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + const encryptedQux = await clientEncryption.encrypt('qux', { keyId: keyId1, algorithm: 'TextPreview', + queryType: 'substringPreview', contentionFactor: 0, textOptions: { caseSensitive: true, diacriticSensitive: true, - prefix: { + substring: { + strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } } }); + // Use encryptedClient to run a "find" operation on the db.substring collection with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } const filter = { - $expr: { $encStrStartsWith: { input: 'encryptedText', prefix: encryptedFoo } } + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedQux } } }; - expect( - await encryptedClient - .db('db') - .collection<{ _id: number; encryptedText: Binary }>('explicit_encryption') - .findOne(filter) - ).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + const result = await encryptedClient + .db('db') + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: any; + }>('substring') + .findOne(filter); + expect(result).to.be.null; + }); + + it('Case 7: assert contentionFactor is required', metadata, async function () { + // Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // queryType: "prefixPreview", + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // prefix: PrefixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } + // Expect an error from libmongocrypt with a message containing the string: "contention factor is required for textPreview algorithm". + const error = await clientEncryption + .encrypt('foo', { + keyId: keyId1, + algorithm: 'TextPreview', + queryType: 'prefixPreview', + textOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { + strMaxQueryLength: 10, + strMinQueryLength: 2 + } + } + }) + .catch(e => e); + + expect(error).to.match(/contention factor is required for textPreview algorithm/); }); }); diff --git a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts index 96afcecf941..570c3725f4d 100644 --- a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts @@ -62,7 +62,7 @@ describe('Client Side Encryption (Legacy)', function () { }); }); -describe('Client Side Encryption (Unified)', function () { +describe.only('Client Side Encryption (Unified)', function () { runUnifiedSuite( loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')), ({ description }, configuration) => { diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json index cb97a167175..8951dd10948 100644 --- a/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json @@ -1,13 +1,13 @@ { - "fields": [ - { + "fields": [ + { "keyId": { "$binary": { "base64": "EjRWeBI0mHYSNBI0VniQEg==", "subType": "04" } }, - "path": "encrypted-textPreview", + "path": "encryptedText", "bsonType": "string", "queries": [ { @@ -26,5 +26,5 @@ } ] } - ] -} + ] +} \ No newline at end of file diff --git a/test/tools/runner/filters/client_encryption_filter.ts b/test/tools/runner/filters/client_encryption_filter.ts index 17c56190154..b9f87350413 100644 --- a/test/tools/runner/filters/client_encryption_filter.ts +++ b/test/tools/runner/filters/client_encryption_filter.ts @@ -59,7 +59,7 @@ export class ClientSideEncryptionFilter extends Filter { }; } - filter(test: { metadata?: MongoDBMetadataUI }) { + filter(test: { metadata?: MongoDBMetadataUI }): boolean | string { const clientSideEncryption = test.metadata && test.metadata.requires && test.metadata.requires.clientSideEncryption; diff --git a/test/tools/runner/filters/libmongocrypt_version_filter.ts b/test/tools/runner/filters/libmongocrypt_version_filter.ts new file mode 100644 index 00000000000..3ad20406421 --- /dev/null +++ b/test/tools/runner/filters/libmongocrypt_version_filter.ts @@ -0,0 +1,37 @@ +import { satisfies } from 'semver'; + +import { ClientSideEncryptionFilter } from './client_encryption_filter'; + +/** + * Filter for whether or not a test requires a specific libmongocrypt version. + * + * @example + * ```js + * metadata: { + * requires: { + * libmongocrypt_version: '>=1.8.0 <2.0.0' + * } + * } + * ``` + * + * - If `libmongocrypt_version` is specified, the test will only run if the detected libmongocrypt version satisfies the semver range. + * - If not specified, the test will always run. + */ +export class LibmongocryptVersionFilter extends ClientSideEncryptionFilter { + override filter(test: { metadata?: MongoDBMetadataUI }): boolean | string { + const requiredVersion = test.metadata?.requires?.libmongocrypt; + + if (requiredVersion == null) { + return true; + } + + if (!this.libmongocrypt) { + return 'Test requires libmongocrypt to be installed.'; + } + + if (satisfies(this.libmongocrypt, requiredVersion, { includePrerelease: true })) { + return true; + } + return `requires libmongocrypt version ${requiredVersion}, detected ${this.libmongocrypt}`; + } +} diff --git a/test/tools/runner/hooks/configuration.ts b/test/tools/runner/hooks/configuration.ts index a380e4240ba..1e20f558cad 100644 --- a/test/tools/runner/hooks/configuration.ts +++ b/test/tools/runner/hooks/configuration.ts @@ -24,6 +24,7 @@ import { type Filter } from '../filters/filter'; import { type Context } from 'mocha'; import { flakyTests } from '../flaky'; import { CryptSharedFilter } from '../filters/crypt_shared_filter'; +import { LibmongocryptVersionFilter } from '../filters/libmongocrypt_version_filter'; // Default our tests to have auth enabled // A better solution will be tackled in NODE-3714 @@ -61,6 +62,7 @@ async function initializeFilters(client): Promise> { new CryptSharedFilter(), new GenericPredicateFilter(), new IDMSMockServerFilter(), + new LibmongocryptVersionFilter(), new MongoDBTopologyFilter(), new MongoDBVersionFilter(), new NodeVersionFilter(), From 84fd3d348902d635f9e091894594f09b752d2c42 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 14 Aug 2025 09:47:04 -0600 Subject: [PATCH 08/11] adopt mongodb-client-encrption@2.5.0 --- .evergreen/install-mongodb-client-encryption.sh | 2 +- package-lock.json | 8 ++++---- package.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.evergreen/install-mongodb-client-encryption.sh b/.evergreen/install-mongodb-client-encryption.sh index 66881a9de9b..1bea94950b8 100644 --- a/.evergreen/install-mongodb-client-encryption.sh +++ b/.evergreen/install-mongodb-client-encryption.sh @@ -9,7 +9,7 @@ if [ -z ${PROJECT_DIRECTORY+omitted} ]; then echo "PROJECT_DIRECTORY is unset" & source $DRIVERS_TOOLS/.evergreen/init-node-and-npm-env.sh rm -rf mongodb-client-encryption -git clone https://github.com/baileympearson/mongodb-client-encryption.git -b NODE-7059 +git clone https://github.com/mongodb-js/mongodb-client-encryption.git pushd mongodb-client-encryption node --version diff --git a/package-lock.json b/package-lock.json index 3d81c90ad34..a41cf9d49bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "js-yaml": "^4.1.0", "mocha": "^11.7.1", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.4.0", + "mongodb-client-encryption": "^6.5.0", "mongodb-legacy": "^6.1.3", "nyc": "^15.1.0", "prettier": "^3.5.3", @@ -6761,9 +6761,9 @@ } }, "node_modules/mongodb-client-encryption": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.4.0.tgz", - "integrity": "sha512-Un1W/5P4KjcUBPeJeSKFNaWH0/8PVsoSatDqyWM2bMK0Vu2Jjxy7ZTgDj1g+uChuqroB09s8LvppdsHpwxSTVA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.5.0.tgz", + "integrity": "sha512-Gj8EeyYKsssdko0NKhWRBGDif6uVFBbv+e+Nyn7E316UmRzApc4IP+p2NLm+av+fU+dFHVT5WqfzaQVDTh8i9w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", diff --git a/package.json b/package.json index bfa701425f9..a13f4a1ff9c 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "js-yaml": "^4.1.0", "mocha": "^11.7.1", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.4.0", + "mongodb-client-encryption": "^6.5.0", "mongodb-legacy": "^6.1.3", "nyc": "^15.1.0", "prettier": "^3.5.3", @@ -175,4 +175,4 @@ "moduleResolution": "node" } } -} \ No newline at end of file +} From a864fbaeb8643757b2134504486514d310852fda Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 14 Aug 2025 09:48:52 -0600 Subject: [PATCH 09/11] fix lint --- src/client-side-encryption/client_encryption.ts | 3 ++- src/index.ts | 3 ++- .../client_side_encryption.prose.27.text_queries.test.ts | 2 +- .../client-side-encryption/client_side_encryption.spec.test.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index aeaeeba5eb0..0fda0a32334 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -852,9 +852,10 @@ export interface ClientEncryptionEncryptOptions { /** * Options for a Queryable Encryption field supporting text queries. * + * @public * @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time. */ -interface TextQueryOptions { +export interface TextQueryOptions { /** Indicates that text indexes for this field are case sensitive */ caseSensitive: boolean; /** Indicates that text indexes for this field are diacritic sensitive. */ diff --git a/src/index.ts b/src/index.ts index 33945827aea..8106b6cdc19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -242,7 +242,8 @@ export type { DataKey, GCPEncryptionKeyOptions, KMIPEncryptionKeyOptions, - RangeOptions + RangeOptions, + TextQueryOptions } from './client-side-encryption/client_encryption'; export { MongoCryptAzureKMSRequestError, diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts index 7e1b13da0c8..14f9604700f 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts @@ -24,7 +24,7 @@ const loadFLEDataFile = async (filename: string) => { relaxed: false } ); -describe.only('27. Text Explicit Encryption', function () { +describe('27. Text Explicit Encryption', function () { let keyDocument1: Document; let keyId1: Binary; let utilClient: MongoClient; diff --git a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts index 570c3725f4d..96afcecf941 100644 --- a/test/integration/client-side-encryption/client_side_encryption.spec.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.spec.test.ts @@ -62,7 +62,7 @@ describe('Client Side Encryption (Legacy)', function () { }); }); -describe.only('Client Side Encryption (Unified)', function () { +describe('Client Side Encryption (Unified)', function () { runUnifiedSuite( loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')), ({ description }, configuration) => { From 7b2560bb635949bf72c14edd9622e5838e6217b6 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 14 Aug 2025 10:19:16 -0600 Subject: [PATCH 10/11] add rest of prose test comments --- ...e_encryption.prose.27.text_queries.test.ts | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts index 14f9604700f..672c37fafd4 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts @@ -35,7 +35,7 @@ describe('27. Text Explicit Encryption', function () { beforeEach(async function () { utilClient = this.configuration.newClient(); - // Using QE CreateCollection() and Collection.Drop(), drop and create the following collections: + // Using QE CreateCollection() and Collection.Drop(), drop and create the following collections with majority write concern: // - db.prefix-suffix using the encryptedFields option set to the contents of encryptedFields-prefix-suffix.json // - db.substring using the encryptedFields option set to the contents of encryptedFields-substring.json async function dropAndCreateCollection(ns: string, encryptedFields?: Document) { @@ -64,10 +64,10 @@ describe('27. Text Explicit Encryption', function () { // Read the "_id" field of key1Document as key1ID. keyId1 = keyDocument1._id; - // Drop and create the collection keyvault.datakeys. + // Drop and create the collection keyvault.datakeys with majority write concern. await dropAndCreateCollection('keyvault.datakeys'); - // Insert `key1Document` in `keyvault.datakeys` with majority write concern. + // Insert `key1Document` in `keyvault.datakeys` with majority write concern with majority write concern. await utilClient .db('keyvault') .collection('datakeys') @@ -109,6 +109,24 @@ describe('27. Text Explicit Encryption', function () { ); { + // Use `clientEncryption` to encrypt the string `"foobarbaz"` with the following `EncryptOpts`: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // prefix: PrefixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // }, + // suffix: SuffixOpts { + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // }, + // }, + // } const encryptedText = await clientEncryption.encrypt('foobarbaz', { keyId: keyId1, algorithm: 'TextPreview', @@ -127,16 +145,36 @@ describe('27. Text Explicit Encryption', function () { } }); + // Use `encryptedClient` to insert the following document into `db.prefix-suffix` with majority write concern: + // { "_id": 0, "encryptedText": } await encryptedClient .db('db') .collection<{ _id: number; encryptedText: Binary }>('prefix-suffix') - .insertOne({ - _id: 0, - encryptedText - }); + .insertOne( + { + _id: 0, + encryptedText + }, + { writeConcern: { w: 'majority' } } + ); } { + // Use `clientEncryption` to encrypt the string `"foobarbaz"` with the following `EncryptOpts`: + // class EncryptOpts { + // keyId : , + // algorithm: "TextPreview", + // contentionFactor: 0, + // textOpts: TextOpts { + // caseSensitive: true, + // diacriticSensitive: true, + // substring: SubstringOpts { + // strMaxLength: 10, + // strMaxQueryLength: 10, + // strMinQueryLength: 2, + // } + // }, + // } const encryptedText = await clientEncryption.encrypt('foobarbaz', { keyId: keyId1, algorithm: 'TextPreview', @@ -152,13 +190,18 @@ describe('27. Text Explicit Encryption', function () { } }); + // Use `encryptedClient` to insert the following document into `db.substring` with majority write concern: + // { "_id": 0, "encryptedText": } await encryptedClient .db('db') .collection<{ _id: number; encryptedText: Binary }>('substring') - .insertOne({ - _id: 0, - encryptedText - }); + .insertOne( + { + _id: 0, + encryptedText + }, + { writeConcern: { w: 'majority' } } + ); } }); From fff2f4148c0c14309746e9f9f10b5eb7ef100e58 Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 14 Aug 2025 10:22:05 -0600 Subject: [PATCH 11/11] sync run on requirement tests --- ...t-csfle-minLibmongocryptVersion-pattern.json | 17 +++++++++++++++++ ...nt-csfle-minLibmongocryptVersion-pattern.yml | 11 +++++++++++ ...ment-csfle-minLibmongocryptVersion-type.json | 17 +++++++++++++++++ ...ement-csfle-minLibmongocryptVersion-type.yml | 11 +++++++++++ 4 files changed, 56 insertions(+) create mode 100644 test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.json create mode 100644 test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.yml create mode 100644 test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.json create mode 100644 test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.yml diff --git a/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.json b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.json new file mode 100644 index 00000000000..1db023bf682 --- /dev/null +++ b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.json @@ -0,0 +1,17 @@ +{ + "description": "runOnRequirement-csfle-minLibmongocryptVersion-pattern", + "schemaVersion": "1.25", + "runOnRequirements": [ + { + "csfle": { + "minLibmongocryptVersion": "1.2.3.4" + } + } + ], + "tests": [ + { + "description": "foo", + "operations": [] + } + ] +} diff --git a/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.yml b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.yml new file mode 100644 index 00000000000..a95e2c6ee71 --- /dev/null +++ b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-pattern.yml @@ -0,0 +1,11 @@ +description: "runOnRequirement-csfle-minLibmongocryptVersion-pattern" + +schemaVersion: "1.25" + +runOnRequirements: + - csfle: + minLibmongocryptVersion: "1.2.3.4" + +tests: + - description: "foo" + operations: [] diff --git a/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.json b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.json new file mode 100644 index 00000000000..8de7b293f1a --- /dev/null +++ b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.json @@ -0,0 +1,17 @@ +{ + "description": "runOnRequirement-csfle-minLibmongocryptVersion-type", + "schemaVersion": "1.25", + "runOnRequirements": [ + { + "csfle": { + "minLibmongocryptVersion": 0 + } + } + ], + "tests": [ + { + "description": "foo", + "operations": [] + } + ] +} diff --git a/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.yml b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.yml new file mode 100644 index 00000000000..0230982ebeb --- /dev/null +++ b/test/spec/unified-test-format/invalid/runOnRequirement-csfle-minLibmongocryptVersion-type.yml @@ -0,0 +1,11 @@ +description: "runOnRequirement-csfle-minLibmongocryptVersion-type" + +schemaVersion: "1.25" + +runOnRequirements: + - csfle: + minLibmongocryptVersion: 0 + +tests: + - description: "foo" + operations: []