diff --git a/source/client-side-encryption/client-side-encryption.md b/source/client-side-encryption/client-side-encryption.md index 6e62c1f78b..29f7e4eba3 100644 --- a/source/client-side-encryption/client-side-encryption.md +++ b/source/client-side-encryption/client-side-encryption.md @@ -48,6 +48,8 @@ QEv1 and QEv2 are incompatible. MongoDB 8.0 dropped `queryType=rangePreview` and added `queryType=range` ([SPM-3583](https://jira.mongodb.org/browse/SPM-3583)). +MongoDB 8.2 added unstable support for QE text queries ([SPM-2880](https://jira.mongodb.org/browse/SPM-2880)) + ## META The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and @@ -1256,6 +1258,7 @@ class EncryptOpts { contentionFactor: Optional, queryType: Optional rangeOpts: Optional + textOpts: Optional } // RangeOpts specifies index options for a Queryable Encryption field supporting "range" queries. @@ -1273,6 +1276,48 @@ class RangeOpts { // precision determines the number of significant digits after the decimal point. May only be set for double or decimal128. precision: Optional } + +// TextOpts specifies options for a Queryable Encryption field supporting text queries. +// NOTE: TextOpts is currently unstable API and subject to backwards breaking changes. +class TextOpts { + // substring contains further options to support substring queries. + substring: Optional, + // prefix contains further options to support prefix queries. + prefix: Optional, + // suffix contains further options to support suffix queries. + suffix: Optional, + // caseSensitive determines whether text indexes for this field are case sensitive. + caseSensitive: bool, + // diacriticSensitive determines whether text indexes for this field are diacritic sensitive. + diacriticSensitive: bool +} + +// NOTE: SubstringOpts is currently unstable API and subject to backwards breaking changes. +class SubstringOpts { + // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error. + strMaxLength: Int32, + // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error. + strMinQueryLength: Int32, + // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error. + strMaxQueryLength: Int32, +} + +// NOTE: PrefixOpts is currently unstable API and subject to backwards breaking changes. +class PrefixOpts { + // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error. + strMinQueryLength: Int32, + // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error. + strMaxQueryLength: Int32, +} + +// NOTE: SuffixOpts is currently unstable API and subject to backwards breaking changes. +class SuffixOpts { + // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error. + strMinQueryLength: Int32, + // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error. + strMaxQueryLength: Int32, +} + ``` Explicit encryption requires a key and algorithm. Keys are either identified by `_id` or by alternate name. Exactly one @@ -1295,18 +1340,21 @@ One of the strings: - "Indexed" - "Unindexed" - "Range" +- "TextPreview" -The result of explicit encryption with the "Indexed" or "Range" algorithm must be processed by the server to insert or -query. Drivers MUST document the following behavior: +The result of explicit encryption with the "Indexed", "Range", or "TextPreview" algorithm must be processed by the +server to insert or query. Drivers MUST document the following behavior: -> To insert or query with an "Indexed" or "Range" encrypted payload, use a `MongoClient` configured with +> To insert or query with an "Indexed", "Range", or "TextPreview" encrypted payload, use a `MongoClient` configured with > `AutoEncryptionOpts`. `AutoEncryptionOpts.bypassQueryAnalysis` may be true. `AutoEncryptionOpts.bypassAutoEncryption` -> must be false. +> must be false. The "TextPreview" algorithm is in preview and should be used for experimental workloads only. These +> features are unstable and their security is not guaranteed until released as Generally Available (GA). The GA version +> of these features may not be backwards compatible with the preview version. #### contentionFactor -contentionFactor may be used to tune performance. Only applies when algorithm is "Indexed" or "Range". libmongocrypt -returns an error if contentionFactor is set for a non-applicable algorithm. +contentionFactor may be used to tune performance. Only applies when algorithm is "Indexed", "Range", or "TextPreview". +libmongocrypt returns an error if contentionFactor is set for a non-applicable algorithm. #### queryType @@ -1314,15 +1362,23 @@ One of the strings: - "equality" - "range" +- "prefixPreview" +- "suffixPreview" +- "substringPreview" -queryType only applies when algorithm is "Indexed" or "Range". libmongocrypt returns an error if queryType is set for a -non-applicable queryType. +queryType only applies when algorithm is "Indexed", "Range", or "TextPreview". libmongocrypt returns an error if +queryType is set for a non-applicable algorithm. #### rangeOpts rangeOpts only applies when algorithm is "Range". libmongocrypt returns an error if rangeOpts is set for a non-applicable algorithm. +#### textOpts + +textOpts only applies when algorithm is "TextPreview". libmongocrypt returns an error if textOpts is set for a +non-applicable algorithm. + ## User facing API: When Auto Encryption Fails Auto encryption requires parsing the MongoDB query language client side (with the [mongocryptd](#mongocryptd) process or @@ -2463,6 +2519,8 @@ explicit session parameter as described in the [Drivers Sessions Specification]( ## Changelog +- 2025-08-06: Add `TextPreview` algorithm. + - 2024-02-19: Add custom options AWS credential provider. - 2024-10-09: Add retry prose test. diff --git a/source/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json b/source/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json new file mode 100644 index 0000000000..ec4489fa09 --- /dev/null +++ b/source/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 + } + ] + } + ] +} diff --git a/source/client-side-encryption/etc/data/encryptedFields-substring.json b/source/client-side-encryption/etc/data/encryptedFields-substring.json new file mode 100644 index 0000000000..ee22def77b --- /dev/null +++ b/source/client-side-encryption/etc/data/encryptedFields-substring.json @@ -0,0 +1,30 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "substringPreview", + "strMaxLength": { + "$numberInt": "10" + }, + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} diff --git a/source/client-side-encryption/tests/README.md b/source/client-side-encryption/tests/README.md index b56160d62f..e0677bbab7 100644 --- a/source/client-side-encryption/tests/README.md +++ b/source/client-side-encryption/tests/README.md @@ -3764,3 +3764,316 @@ class AutoEncryptionOpts { ``` Assert that an error is thrown. + +### 27. Text Explicit Encryption + +The Text Explicit Encryption tests utilize Queryable Encryption (QE) range protocol V2 and require MongoDB server 8.2.0+ +and libmongocrypt 1.15.1+. The tests must not run against a standalone. + +Before running each of the following test cases, perform the following Test Setup. + +#### Test Setup + +Using [QE CreateCollection() and Collection.Drop()](../client-side-encryption.md#create-collection-helper), 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](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/etc/data/encryptedFields-prefix-suffix.json) +- `db.substring` using the `encryptedFields` option set to the contents of + [encryptedFields-substring.json](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/etc/data/encryptedFields-substring.json) + +Load the file +[key1-document.json](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/etc/data/keys/key1-document.json) +as `key1Document`. + +Read the `"_id"` field of `key1Document` as `key1ID`. + +Drop and create the collection `keyvault.datakeys`. + +Insert `key1Document` in `keyvault.datakeys` with majority write concern. + +Create a MongoClient named `keyVaultClient`. + +Create a ClientEncryption object named `clientEncryption` with these options: + +```typescript +class ClientEncryptionOpts { + keyVaultClient: , + keyVaultNamespace: "keyvault.datakeys", + kmsProviders: { "local": { "key": } }, +} +``` + +Create a MongoClient named `encryptedClient` with these `AutoEncryptionOpts`: + +```typescript +class AutoEncryptionOpts { + keyVaultNamespace: "keyvault.datakeys", + kmsProviders: { "local": { "key": } }, + bypassQueryAnalysis: true, +} +``` + +Use `clientEncryption` to encrypt the string `"foobarbaz"` with the following `EncryptOpts`: + +```typescript +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, + }, + }, +} +``` + +Use `encryptedClient` to insert the following document into `db.prefix-suffix` with majority write concern: + +```javascript +{ "_id": 0, "encryptedText": } +``` + +Use `clientEncryption` to encrypt the string `"foobarbaz"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + substring: SubstringOpts { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to insert the following document into `db.substring` with majority write concern: + +```javascript +{ "_id": 0, "encryptedText": } +``` + +#### Case 1: can find a document by prefix + +Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "prefixPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + prefix: PrefixOpts { + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.prefix-suffix` collection with the following filter: + +```javascript +{ $expr: { $encStrStartsWith: {input: '$encryptedText', prefix: } } } +``` + +Assert the following document is returned: + +```javascript +{ "_id": 0, "encryptedText": "foobarbaz" } +``` + +#### Case 2: can find a document by suffix + +Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "suffixPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + suffix: SuffixOpts { + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.prefix-suffix` collection with the following filter: + +```javascript +{ $expr: { $encStrEndsWith: {input: '$encryptedText', suffix: } } } +``` + +Assert the following document is returned: + +```javascript +{ "_id": 0, "encryptedText": "foobarbaz" } +``` + +#### Case 3: assert no document found by prefix + +Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "prefixPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + prefix: PrefixOpts { + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.prefix-suffix` collection with the following filter: + +```javascript +{ $expr: { $encStrStartsWith: {input: '$encryptedText', prefix: } } } +``` + +Assert that no documents are returned. + +#### Case 4: assert no document found by suffix + +Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "suffixPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + suffix: SuffixOpts { + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.prefix-suffix` collection with the following filter: + +```javascript +{ $expr: { $encStrEndsWith: {input: '$encryptedText', suffix: } } } +``` + +Assert that no documents are returned. + +#### Case 5: can find a document by substring + +Use `clientEncryption.encrypt()` to encrypt the string `"bar"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "substringPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + substring: SubstringOpts { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.substring` collection with the following filter: + +```javascript +{ $expr: { $encStrContains: {input: '$encryptedText', substring: } } } +``` + +Assert the following document is returned: + +```javascript +{ "_id": 0, "encryptedText": "foobarbaz" } +``` + +#### Case 6: assert no document found by substring + +Use `clientEncryption.encrypt()` to encrypt the string `"qux"` with the following `EncryptOpts`: + +```typescript +class EncryptOpts { + keyId : , + algorithm: "TextPreview", + queryType: "substringPreview", + contentionFactor: 0, + textOpts: TextOpts { + caseSensitive: true, + diacriticSensitive: true, + substring: SubstringOpts { + strMaxLength: 10, + strMaxQueryLength: 10, + strMinQueryLength: 2, + } + }, +} +``` + +Use `encryptedClient` to run a "find" operation on the `db.substring` collection with the following filter: + +```javascript +{ $expr: { $encStrContains: {input: '$encryptedText', substring: } } } +``` + +Assert that no documents are returned. + +#### Case 7: assert `contentionFactor` is required + +Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: + +```typescript +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".