diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 948f27256fb..79d67559e4b 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -746,8 +746,16 @@ export class ClientEncryption { expressionMode: boolean, options: ClientEncryptionEncryptOptions ): Promise { - const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions, textOptions } = - options; + const { + algorithm, + keyId, + keyAltName, + contentionFactor, + queryType, + rangeOptions, + stringOptions, + textOptions + } = options; const contextOptions: ExplicitEncryptionContextOptions = { expressionMode, algorithm @@ -780,8 +788,9 @@ export class ClientEncryption { contextOptions.rangeOptions = serialize(rangeOptions); } - if (typeof textOptions === 'object') { - contextOptions.textOptions = serialize(textOptions); + const resolvedStringOptions = stringOptions ?? textOptions; + if (typeof resolvedStringOptions === 'object') { + contextOptions.textOptions = serialize(resolvedStringOptions); } const valueBuffer = serialize({ v: value }); @@ -815,6 +824,8 @@ export interface ClientEncryptionEncryptOptions { | 'Indexed' | 'Unindexed' | 'Range' + | 'String' + /** @deprecated Use `'String'` instead. */ | 'TextPreview'; /** @@ -833,26 +844,38 @@ export interface ClientEncryptionEncryptOptions { /** * The query type. */ - queryType?: 'equality' | 'range' | 'prefixPreview' | 'suffixPreview' | 'substringPreview'; + queryType?: + | 'equality' + | 'range' + | 'prefix' + | 'suffix' + /** @deprecated Use `'prefix'` instead. */ + | 'prefixPreview' + /** @deprecated Use `'suffix'` instead. */ + | 'suffixPreview' + /** @experimental Public Technical Preview: `substringPreview` is an experimental feature and may break at any time. */ + | 'substringPreview'; /** The index options for a Queryable Encryption field supporting "range" queries.*/ rangeOptions?: RangeOptions; + /** Options for a Queryable Encryption field supporting string queries. Only valid when `algorithm` is `'String'`. */ + stringOptions?: StringQueryOptions; + /** - * Options for a Queryable Encryption field supporting text queries. Only valid when `algorithm` is `TextPreview`. + * Options for a Queryable Encryption field supporting text queries. Only valid when `algorithm` is `'String'`. * - * @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time. + * @deprecated Use `stringOptions` instead. */ - textOptions?: TextQueryOptions; + textOptions?: StringQueryOptions; } /** - * Options for a Queryable Encryption field supporting text queries. + * Options for a Queryable Encryption field supporting string queries. * * @public - * @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time. */ -export interface TextQueryOptions { +export interface StringQueryOptions { /** Indicates that text indexes for this field are case sensitive */ caseSensitive: boolean; /** Indicates that text indexes for this field are diacritic sensitive. */ @@ -872,6 +895,9 @@ export interface TextQueryOptions { strMinQueryLength: Int32 | number; }; + /** + * @experimental Public Technical Preview: `substring` is an experimental feature and may break at any time. + */ substring?: { /** The maximum allowed length to insert. */ strMaxLength: Int32 | number; @@ -882,6 +908,12 @@ export interface TextQueryOptions { }; } +/** + * @public + * @deprecated Use {@link StringQueryOptions} instead. + */ +export type TextQueryOptions = StringQueryOptions; + /** * @public * @experimental diff --git a/src/index.ts b/src/index.ts index 74803dfa2a0..542a0c17406 100644 --- a/src/index.ts +++ b/src/index.ts @@ -241,6 +241,7 @@ export type { GCPEncryptionKeyOptions, KMIPEncryptionKeyOptions, RangeOptions, + StringQueryOptions, TextQueryOptions } from './client-side-encryption/client_encryption'; export { 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 e6dbc8f45c5..81e844b4f5c 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 @@ -7,22 +7,54 @@ import * as semver from 'semver'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { ClientEncryption, type MongoClient, MongoDBCollectionNamespace } from '../../mongodb'; -// # Server 9.0.0-rc0 removes support for "prefixPreview" and "suffixPreview": SERVER-123416 -const metadataWithoutPreview: MongoDBMetadataUI = { + +// Cases 1-4 GA: GA prefix/suffix requires server 9.0+ (SERVER-123416) and libmongocrypt 1.19.0+ (MONGOCRYPT-870). +const metadataGA: MongoDBMetadataUI = { + requires: { + clientSideEncryption: '>=6.4.0', + mongodb: '>=9.0.0', + topology: '!single', + libmongocrypt: '>=1.19.0' + } +}; + +// Cases 1-4 preview: prefixPreview/suffixPreview removed in server 9.0.0 (SERVER-123416). +const metadataPreview: MongoDBMetadataUI = { requires: { clientSideEncryption: '>=6.4.0', mongodb: '>=8.2.0 <9.0.0', topology: '!single', - libmongocrypt: '>=1.15.1' + libmongocrypt: '>=1.19.1' } }; + // TODO(NODE-7623): substringPreview contention validation broken on MongoDB 9.0+ (SERVER-91887). -const metadataWithoutSubstringPreview: MongoDBMetadataUI = { +const metadataSubstring: MongoDBMetadataUI = { + requires: { + clientSideEncryption: '>=6.4.0', + mongodb: '>=8.2.0 <9.0.0', + topology: '!single', + libmongocrypt: '>=1.18.1' + } +}; + +// Cases 8-9: DRIVERS-3470 regression, requires server 9.0+ and libmongocrypt 1.19.0+. +const metadataCiDiGA: MongoDBMetadataUI = { + requires: { + clientSideEncryption: '>=6.4.0', + mongodb: '>=9.0.0', + topology: '!single', + libmongocrypt: '>=1.19.0' + } +}; + +// Cases 10-11: DRIVERS-3470 regression for substring. TODO(NODE-7623): skip 9.0+. +const metadataCiDiSubstring: MongoDBMetadataUI = { requires: { clientSideEncryption: '>=6.4.0', mongodb: '>=8.2.0 <9.0.0', topology: '!single', - libmongocrypt: '>=1.15.1' + libmongocrypt: '>=1.18.1' } }; @@ -34,23 +66,19 @@ const loadFLEDataFile = async (filename: string) => { relaxed: false } ); -describe('27. Text Explicit Encryption', function () { +describe('27. String Explicit Encryption', function () { let keyDocument1: Document; let keyId1: Binary; let utilClient: MongoClient; let keyVaultClient: MongoClient; let clientEncryption: ClientEncryption; - let encryptedClient: MongoClient; + let explicitEncryptedClient: MongoClient; + let autoEncryptedClient: MongoClient; beforeEach(async function () { utilClient = this.configuration.newClient(); const isServer9OrAbove = semver.satisfies(this.configuration.version, '>=9.0.0'); - const shouldRunPrefixSuffixTests = !isServer9OrAbove; - // 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 - // Skip this step if testing server 9.0.0+. - // - 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, { @@ -63,503 +91,540 @@ describe('27. Text Explicit Encryption', function () { }); } - if (shouldRunPrefixSuffixTests) { + if (isServer9OrAbove) { await dropAndCreateCollection( 'db.prefix-suffix', await loadFLEDataFile('encryptedFields-prefix-suffix.json') ); + await dropAndCreateCollection( + 'db.prefix-suffix-ci-di', + await loadFLEDataFile('encryptedFields-prefix-suffix-ci-di.json') + ); + } else { + await dropAndCreateCollection( + 'db.prefix-suffix-preview', + await loadFLEDataFile('encryptedFields-prefix-suffix-preview.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'); + await dropAndCreateCollection( + 'db.substring-ci-di', + await loadFLEDataFile('encryptedFields-substring-ci-di.json') + ); - // Read the "_id" field of key1Document as key1ID. + keyDocument1 = await loadFLEDataFile('keys/key1-document.json'); keyId1 = keyDocument1._id; - // Drop and create the collection keyvault.datakeys with majority write concern. await dropAndCreateCollection('keyvault.datakeys'); - - // Insert `key1Document` in `keyvault.datakeys` with majority write concern with majority write concern. await utilClient .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 - } + 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( + explicitEncryptedClient = this.configuration.newClient( {}, { autoEncryption: { keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { - local: getCSFLEKMSProviders().local - }, + kmsProviders: { local: getCSFLEKMSProviders().local }, bypassQueryAnalysis: true } } ); - { - // 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', - contentionFactor: 0, - textOptions: { - caseSensitive: true, - diacriticSensitive: true, - prefix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - }, - suffix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + autoEncryptedClient = this.configuration.newClient( + {}, + { + autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: getCSFLEKMSProviders().local } } - }); - - if (shouldRunPrefixSuffixTests) { - // 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 - }, - { 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', - contentionFactor: 0, - textOptions: { - caseSensitive: true, - diacriticSensitive: true, - substring: { - strMaxLength: 10, - strMaxQueryLength: 10, - strMinQueryLength: 2 - } - } - }); + const encryptedPrefixSuffix = await clientEncryption.encrypt('foobarbaz', { + keyId: keyId1, + algorithm: 'String', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 }, + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); - // Use `encryptedClient` to insert the following document into `db.substring` with majority write concern: - // { "_id": 0, "encryptedText": } - await encryptedClient + if (isServer9OrAbove) { + await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('prefix-suffix') + .insertOne( + { _id: 0, encryptedText: encryptedPrefixSuffix }, + { writeConcern: { w: 'majority' } } + ); + } else { + await explicitEncryptedClient .db('db') - .collection<{ _id: number; encryptedText: Binary }>('substring') + .collection<{ _id: number; encryptedText: Binary }>('prefix-suffix-preview') .insertOne( - { - _id: 0, - encryptedText - }, + { _id: 0, encryptedText: encryptedPrefixSuffix }, { writeConcern: { w: 'majority' } } ); } + + const encryptedSubstring = await clientEncryption.encrypt('foobarbaz', { + keyId: keyId1, + algorithm: 'String', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('substring') + .insertOne( + { _id: 0, encryptedText: encryptedSubstring }, + { writeConcern: { w: 'majority' } } + ); }); afterEach(async function () { - await Promise.allSettled([utilClient.close(), encryptedClient.close(), keyVaultClient.close()]); + await Promise.allSettled([ + utilClient.close(), + explicitEncryptedClient.close(), + autoEncryptedClient.close(), + keyVaultClient.close() + ]); }); - it('Case 1: can find a document by prefix', metadataWithoutPreview, async function () { - // Skip this test case if testing MongoDB server 9.0.0+. - // 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, - // } - // }, - // } + it('Case 1 (GA): can find a document by prefix', metadataGA, async function () { const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, - algorithm: 'TextPreview', - queryType: 'prefixPreview', + algorithm: 'String', + queryType: 'prefix', contentionFactor: 0, - textOptions: { + stringOptions: { caseSensitive: true, diacriticSensitive: true, - prefix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + 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 explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix') + .findOne(filter); - const { __safeContent__, ...result } = await encryptedClient + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); + + it('Case 1 (preview): can find a document by prefix', metadataPreview, async function () { + const encryptedFoo = await clientEncryption.encrypt('foo', { + keyId: keyId1, + algorithm: 'String', + queryType: 'prefixPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedFoo } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient .db('db') .collection<{ _id: number; encryptedText: Binary; - __safeContent__: any; - }>('prefix-suffix') + __safeContent__: unknown; + }>('prefix-suffix-preview') .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', metadataWithoutPreview, async function () { - // Skip this test case if testing MongoDB server 9.0.0+. - // 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, - // } - // }, - // } + it('Case 2 (GA): can find a document by suffix', metadataGA, async function () { const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, - algorithm: 'TextPreview', - queryType: 'suffixPreview', + algorithm: 'String', + queryType: 'suffix', contentionFactor: 0, - textOptions: { + stringOptions: { caseSensitive: true, diacriticSensitive: true, - suffix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + suffix: { 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 { __safeContent__, ...result } = await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix') + .findOne(filter); + + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); - const { __safeContent__, ...result } = await encryptedClient + it('Case 2 (preview): can find a document by suffix', metadataPreview, async function () { + const encryptedBaz = await clientEncryption.encrypt('baz', { + keyId: keyId1, + algorithm: 'String', + queryType: 'suffixPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedBaz } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient .db('db') .collection<{ _id: number; encryptedText: Binary; - __safeContent__: any; - }>('prefix-suffix') + __safeContent__: unknown; + }>('prefix-suffix-preview') .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', metadataWithoutPreview, async function () { - // Skip this test case if testing MongoDB server 9.0.0+. - // 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, - // } - // }, - // } + it('Case 3 (GA): assert no document found by prefix', metadataGA, async function () { + const encryptedBaz = await clientEncryption.encrypt('baz', { + keyId: keyId1, + algorithm: 'String', + queryType: 'prefix', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedBaz } } + }; + expect(await explicitEncryptedClient.db('db').collection('prefix-suffix').findOne(filter)).to.be + .null; + }); + + it('Case 3 (preview): assert no document found by prefix', metadataPreview, async function () { const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, - algorithm: 'TextPreview', + algorithm: 'String', queryType: 'prefixPreview', contentionFactor: 0, - textOptions: { + stringOptions: { caseSensitive: true, diacriticSensitive: true, - prefix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + 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; + expect( + await explicitEncryptedClient.db('db').collection('prefix-suffix-preview').findOne(filter) + ).to.be.null; + }); + + it('Case 4 (GA): assert no document found by suffix', metadataGA, async function () { + const encryptedFoo = await clientEncryption.encrypt('foo', { + keyId: keyId1, + algorithm: 'String', + queryType: 'suffix', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedFoo } } + }; + expect(await explicitEncryptedClient.db('db').collection('prefix-suffix').findOne(filter)).to.be + .null; }); - it('Case 4: assert no document found by suffix', metadataWithoutPreview, async function () { - // Skip this test case if testing MongoDB server 9.0.0+. - // 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, - // } - // }, - // } + it('Case 4 (preview): assert no document found by suffix', metadataPreview, async function () { const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, - algorithm: 'TextPreview', + algorithm: 'String', queryType: 'suffixPreview', contentionFactor: 0, - textOptions: { + stringOptions: { caseSensitive: true, diacriticSensitive: true, - suffix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } } }); const filter = { $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedFoo } } }; + expect( + await explicitEncryptedClient.db('db').collection('prefix-suffix-preview').findOne(filter) + ).to.be.null; + }); - const result = await encryptedClient + it('Case 5: can find a document by substring', metadataSubstring, async function () { + const encryptedBar = await clientEncryption.encrypt('bar', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substringPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedBar } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient .db('db') - .collection<{ - _id: number; - encryptedText: Binary; - __safeContent__: any; - }>('prefix-suffix') + .collection<{ _id: number; encryptedText: Binary; __safeContent__: unknown }>('substring') .findOne(filter); - expect(result).to.be.null; + + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); + + it('Case 6: assert no document found by substring', metadataSubstring, async function () { + const encryptedQux = await clientEncryption.encrypt('qux', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substringPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedQux } } + }; + expect(await explicitEncryptedClient.db('db').collection('substring').findOne(filter)).to.be + .null; + }); + + it('Case 7: assert contentionFactor is required', metadataGA, async function () { + const error = await clientEncryption + .encrypt('foo', { + keyId: keyId1, + algorithm: 'String', + queryType: 'prefix', + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }) + .catch(e => e); + + expect(error).to.match(/contention factor is required for string algorithm/); }); it( - 'Case 5: can find a document by substring', - metadataWithoutSubstringPreview, + 'Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix', + metadataCiDiGA, 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', { + await autoEncryptedClient + .db('db') + .collection<{ encryptedText: string }>('prefix-suffix-ci-di') + .insertOne({ encryptedText: 'BingQiLin' }, { writeConcern: { w: 'majority' } }); + + const encryptedBing = await clientEncryption.encrypt('bing', { keyId: keyId1, - algorithm: 'TextPreview', - queryType: 'substringPreview', + algorithm: 'String', + queryType: 'prefix', contentionFactor: 0, - textOptions: { - caseSensitive: true, - diacriticSensitive: true, - substring: { - strMaxLength: 10, - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + prefix: { 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 byPrefix = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedBing } } }; + const { __safeContent__: _s1, ...byPrefixResult } = await explicitEncryptedClient + .db('db') + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix-ci-di') + .findOne(byPrefix); + expect(byPrefixResult).to.deep.equal({ encryptedText: 'BingQiLin' }); - const { __safeContent__, ...result } = await encryptedClient + const encryptedLin = await clientEncryption.encrypt('lin', { + keyId: keyId1, + algorithm: 'String', + queryType: 'suffix', + contentionFactor: 0, + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const bySuffix = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedLin } } + }; + const { __safeContent__: _s2, ...bySuffixResult } = await explicitEncryptedClient .db('db') - .collection<{ - _id: number; - encryptedText: Binary; - __safeContent__: any; - }>('substring') - .findOne(filter); - expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix-ci-di') + .findOne(bySuffix); + expect(bySuffixResult).to.deep.equal({ encryptedText: 'BingQiLin' }); + } + ); + + it( + 'Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix', + metadataCiDiGA, + async function () { + await autoEncryptedClient + .db('db') + .collection<{ encryptedText: string }>('prefix-suffix-ci-di') + .insertOne({ encryptedText: 'cafébarbäz' }, { writeConcern: { w: 'majority' } }); + + const encryptedCafe = await clientEncryption.encrypt('cafe', { + keyId: keyId1, + algorithm: 'String', + queryType: 'prefix', + contentionFactor: 0, + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + prefix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const byPrefix = { + $expr: { $encStrStartsWith: { input: '$encryptedText', prefix: encryptedCafe } } + }; + const { __safeContent__: _s1, ...byPrefixResult } = await explicitEncryptedClient + .db('db') + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix-ci-di') + .findOne(byPrefix); + expect(byPrefixResult).to.deep.equal({ encryptedText: 'cafébarbäz' }); + + const encryptedBaz = await clientEncryption.encrypt('baz', { + keyId: keyId1, + algorithm: 'String', + queryType: 'suffix', + contentionFactor: 0, + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + suffix: { strMaxQueryLength: 10, strMinQueryLength: 2 } + } + }); + + const bySuffix = { + $expr: { $encStrEndsWith: { input: '$encryptedText', suffix: encryptedBaz } } + }; + const { __safeContent__: _s2, ...bySuffixResult } = await explicitEncryptedClient + .db('db') + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('prefix-suffix-ci-di') + .findOne(bySuffix); + expect(bySuffixResult).to.deep.equal({ encryptedText: 'cafébarbäz' }); } ); it( - 'Case 6: assert no document found by substring', - metadataWithoutSubstringPreview, + 'Case 10: can find an auto-encrypted case-insensitively indexed document by substring', + metadataCiDiSubstring, 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', { + await autoEncryptedClient + .db('db') + .collection<{ encryptedText: string }>('substring-ci-di') + .insertOne({ encryptedText: 'FooBarBaz' }, { writeConcern: { w: 'majority' } }); + + const encryptedBar = await clientEncryption.encrypt('bar', { keyId: keyId1, - algorithm: 'TextPreview', + algorithm: 'String', queryType: 'substringPreview', contentionFactor: 0, - textOptions: { - caseSensitive: true, - diacriticSensitive: true, - substring: { - strMaxLength: 10, - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + 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: encryptedQux } } + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedBar } } }; - - const result = await encryptedClient + const { __safeContent__, ...result } = await explicitEncryptedClient .db('db') - .collection<{ - _id: number; - encryptedText: Binary; - __safeContent__: any; - }>('substring') + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('substring-ci-di') .findOne(filter); - expect(result).to.be.null; + expect(result).to.deep.equal({ encryptedText: 'FooBarBaz' }); } ); - it('Case 7: assert contentionFactor is required', metadataWithoutPreview, async function () { - // Skip this test case if testing MongoDB server 9.0.0+. - // 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', { + it( + 'Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring', + metadataCiDiSubstring, + async function () { + await autoEncryptedClient + .db('db') + .collection<{ encryptedText: string }>('substring-ci-di') + .insertOne({ encryptedText: 'foocafébaz' }, { writeConcern: { w: 'majority' } }); + + const encryptedCafe = await clientEncryption.encrypt('cafe', { keyId: keyId1, - algorithm: 'TextPreview', - queryType: 'prefixPreview', - textOptions: { - caseSensitive: true, - diacriticSensitive: true, - prefix: { - strMaxQueryLength: 10, - strMinQueryLength: 2 - } + algorithm: 'String', + queryType: 'substringPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: false, + diacriticSensitive: false, + substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } } - }) - .catch(e => e); + }); - expect(error).to.match(/contention factor is required for textPreview algorithm/); - }); + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedCafe } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient + .db('db') + .collection<{ encryptedText: Binary; __safeContent__: unknown }>('substring-ci-di') + .findOne(filter); + expect(result).to.deep.equal({ encryptedText: 'foocafébaz' }); + } + ); }); diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-ci-di.json b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-ci-di.json new file mode 100644 index 00000000000..3002c642b2e --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-ci-di.json @@ -0,0 +1,40 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "prefix", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "contention": 0, + "caseSensitive": false, + "diacriticSensitive": false + }, + { + "queryType": "suffix", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "contention": 0, + "caseSensitive": false, + "diacriticSensitive": false + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-preview.json b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-preview.json new file mode 100644 index 00000000000..047064beb15 --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-prefix-suffix-preview.json @@ -0,0 +1,44 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "prefixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "contention": { + "$numberLong": "0" + }, + "caseSensitive": true, + "diacriticSensitive": true + }, + { + "queryType": "suffixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "contention": { + "$numberLong": "0" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-substring-ci-di.json b/test/spec/client-side-encryption/etc/data/encryptedFields-substring-ci-di.json new file mode 100644 index 00000000000..d6aadfb8777 --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-substring-ci-di.json @@ -0,0 +1,31 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "substringPreview", + "strMaxLength": { + "$numberInt": "10" + }, + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "contention": 0, + "caseSensitive": false, + "diacriticSensitive": false + } + ] + } + ] +} \ No newline at end of file diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-prefix.json b/test/spec/client-side-encryption/tests/unified/QE-Text-prefix.json new file mode 100644 index 00000000000..25475e2c3a0 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefix.json @@ -0,0 +1,338 @@ +{ + "description": "QE-Text-prefix", + "schemaVersion": "1.25", + "runOnRequirements": [ + { + "minServerVersion": "9.0.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": { + "minLibmongocryptVersion": "1.19.0" + } + } + ], + "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": "prefix", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE prefix", + "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-prefix.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-prefix.yml new file mode 100644 index 00000000000..7cdc4e45f73 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-prefix.yml @@ -0,0 +1,225 @@ +description: QE-Text-prefix +schemaVersion: "1.25" +runOnRequirements: + - minServerVersion: "9.0.0" # Server 9.0.0 adds stable support for QE text prefix and suffix queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: + minLibmongocryptVersion: 1.19.0 # For MONGOCRYPT-870. +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": "prefix", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE prefix" + 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-suffix.json b/test/spec/client-side-encryption/tests/unified/QE-Text-suffix.json new file mode 100644 index 00000000000..ad6cdc06c96 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffix.json @@ -0,0 +1,338 @@ +{ + "description": "QE-Text-suffix", + "schemaVersion": "1.25", + "runOnRequirements": [ + { + "minServerVersion": "9.0.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": { + "minLibmongocryptVersion": "1.19.0" + } + } + ], + "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": "suffix", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "30" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE suffix", + "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 $encStrEndsWith", + "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-suffix.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-suffix.yml new file mode 100644 index 00000000000..c98cb5e9dc5 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-suffix.yml @@ -0,0 +1,221 @@ +description: QE-Text-suffix +schemaVersion: "1.25" +runOnRequirements: + - minServerVersion: "9.0.0" # Server 9.0.0 adds stable support for QE text prefix and suffix queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: + minLibmongocryptVersion: 1.19.0 # For MONGOCRYPT-870. +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": "suffix", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "30" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE suffix" + 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 $encStrEndsWith" + 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: []