diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index 91f722e8e1..f509f8b3ea 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.vault; +import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Reason; import com.mongodb.lang.Nullable; import org.bson.BsonBinary; @@ -31,6 +33,7 @@ public class EncryptOptions { private Long contentionFactor; private String queryType; private RangeOptions rangeOptions; + private TextOptions textOptions; /** * Construct an instance with the given algorithm. @@ -51,8 +54,13 @@ public EncryptOptions(final String algorithm) { *
  • Indexed
  • *
  • Unindexed
  • *
  • Range
  • + *
  • TextPreview
  • * * + *

    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.

    + * * @return the encryption algorithm */ public String getAlgorithm() { @@ -141,8 +149,8 @@ public Long getContentionFactor() { /** * The QueryType. * - *

    Currently, we support only "equality" or "range" queryType.

    - *

    It is an error to set queryType when the algorithm is not "Indexed" or "Range".

    + *

    Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.

    + *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".

    * @param queryType the query type * @return this * @since 4.7 @@ -194,6 +202,36 @@ public RangeOptions getRangeOptions() { return rangeOptions; } + /** + * The TextOptions + * + *

    It is an error to set TextOptions when the algorithm is not "TextPreview". + * @param textOptions the text options + * @return this + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { + this.textOptions = textOptions; + return this; + } + + /** + * Gets the TextOptions + * @see #textOptions(TextOptions) + * @return the text options or null if not set + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + @Nullable + public TextOptions getTextOptions() { + return textOptions; + } + @Override public String toString() { return "EncryptOptions{" diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java new file mode 100644 index 0000000000..34dcd0d806 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -0,0 +1,187 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.vault; + +import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Reason; +import com.mongodb.lang.Nullable; +import org.bson.BsonDocument; + +/** + * Text options for a Queryable Encryption field that supports text queries. + * + *

    Note: TextOptions is in Alpha and subject to backwards breaking changes. + * + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ +@Alpha(Reason.SERVER) +public class TextOptions { + private Boolean caseSensitive; + private Boolean diacriticSensitive; + @Nullable + private BsonDocument prefixOptions; + @Nullable + private BsonDocument suffixOptions; + @Nullable + private BsonDocument substringOptions; + + /** + * Construct a new instance + */ + public TextOptions() { + } + + /** + * @return true if text indexes for this field are case sensitive. + */ + public boolean getCaseSensitive() { + return caseSensitive; + } + + /** + * Set case sensitivity + * + * @param caseSensitive true if text indexes are case sensitive + * @return this + */ + public TextOptions caseSensitive(final boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + /** + * @return true if text indexes are diacritic sensitive + */ + public boolean getDiacriticSensitive() { + return diacriticSensitive; + } + + /** + * Set diacritic sensitivity + * + * @param diacriticSensitive true if text indexes are diacritic sensitive + * @return this + */ + public TextOptions diacriticSensitive(final boolean diacriticSensitive) { + this.diacriticSensitive = diacriticSensitive; + return this; + } + + /** + * Set the prefix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param prefixOptions the prefix options or null + * @return this + */ + public TextOptions prefixOptions(@Nullable final BsonDocument prefixOptions) { + this.prefixOptions = prefixOptions; + return this; + } + + /** + * @see #prefixOptions(BsonDocument) + * @return the prefix options document or null + */ + @Nullable + public BsonDocument getPrefixOptions() { + return prefixOptions; + } + + /** + * Set the suffix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param suffixOptions the suffix options or null + * @return this + */ + public TextOptions suffixOptions(@Nullable final BsonDocument suffixOptions) { + this.suffixOptions = suffixOptions; + return this; + } + + /** + * @see #suffixOptions(BsonDocument) + * @return the suffix options document or null + */ + @Nullable + public BsonDocument getSuffixOptions() { + return suffixOptions; + } + + /** + * Set the substring options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
    +     *    strMaxLength: BsonInt32,
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param substringOptions the substring options or null + * @return this + */ + public TextOptions substringOptions(@Nullable final BsonDocument substringOptions) { + this.substringOptions = substringOptions; + return this; + } + + /** + * @see #substringOptions(BsonDocument) + * @return the substring options document or null + */ + @Nullable + public BsonDocument getSubstringOptions() { + return substringOptions; + } + +} diff --git a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java index 640707d94d..2b472668d9 100644 --- a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java +++ b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java @@ -17,7 +17,9 @@ import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RangeOptions; +import com.mongodb.client.model.vault.TextOptions; import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonInt64; @@ -70,6 +72,30 @@ public static MongoExplicitEncryptOptions asMongoExplicitEncryptOptions(final En } encryptOptionsBuilder.rangeOptions(rangeOptionsBsonDocument); } + + TextOptions textOptions = options.getTextOptions(); + if (textOptions != null) { + BsonDocument textOptionsDocument = new BsonDocument(); + textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive())); + textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive())); + + BsonDocument substringOptions = textOptions.getSubstringOptions(); + if (substringOptions != null) { + textOptionsDocument.put("substring", substringOptions); + } + + BsonDocument prefixOptions = textOptions.getPrefixOptions(); + if (prefixOptions != null) { + textOptionsDocument.put("prefix", prefixOptions); + } + + BsonDocument suffixOptions = textOptions.getSuffixOptions(); + if (suffixOptions != null) { + textOptionsDocument.put("suffix", suffixOptions); + } + encryptOptionsBuilder.textOptions(textOptionsDocument); + } + return encryptOptionsBuilder.build(); } private EncryptOptionsHelper() { diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java index 7ed6ad3c36..30a7f274bb 100644 --- a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java +++ b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java @@ -64,6 +64,7 @@ import com.mongodb.internal.connection.StreamFactoryFactory; import com.mongodb.internal.connection.TlsChannelStreamFactoryFactory; import com.mongodb.internal.connection.netty.NettyStreamFactoryFactory; +import com.mongodb.internal.crypt.capi.CAPI; import com.mongodb.internal.operation.BatchCursor; import com.mongodb.internal.operation.CommandReadOperation; import com.mongodb.internal.operation.DropDatabaseOperation; @@ -148,6 +149,7 @@ public final class ClusterFixture { private static final Map BINDING_MAP = new HashMap<>(); private static final Map ASYNC_BINDING_MAP = new HashMap<>(); + private static ServerVersion mongoCryptVersion; private static ServerVersion serverVersion; private static BsonDocument serverParameters; @@ -181,6 +183,13 @@ public static ClusterDescription getClusterDescription(final Cluster cluster) { } } + public static ServerVersion getMongoCryptVersion() { + if (mongoCryptVersion == null) { + mongoCryptVersion = new ServerVersion(getVersionList(CAPI.mongocrypt_version(null).toString())); + } + return mongoCryptVersion; + } + public static ServerVersion getServerVersion() { if (serverVersion == null) { serverVersion = getVersion(new CommandReadOperation<>("admin", diff --git a/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java b/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java index 0d8968eea9..d5abfdd6e3 100644 --- a/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java +++ b/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java @@ -74,6 +74,7 @@ import static com.mongodb.ClusterFixture.executeAsync; import static com.mongodb.ClusterFixture.getBinding; +import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -154,6 +155,16 @@ public void drop(final WriteConcern writeConcern) { drop(namespace, writeConcern); } + public void dropAndCreate(final BsonDocument createOptions) { + // Drop the collection and any encryption collections: enxcol_..esc and enxcol_..ecoc + drop(namespace, WriteConcern.MAJORITY); + drop(new MongoNamespace(namespace.getDatabaseName(), format("enxcol_.%s.esc", namespace.getCollectionName())), + WriteConcern.MAJORITY); + drop(new MongoNamespace(namespace.getDatabaseName(), format("enxcol_.%s.ecoc", namespace.getCollectionName())), + WriteConcern.MAJORITY); + create(WriteConcern.MAJORITY, createOptions); + } + public void create() { create(namespace.getCollectionName(), new CreateCollectionOptions(), WriteConcern.ACKNOWLEDGED); } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java new file mode 100644 index 0000000000..849a6eb045 --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.reactivestreams.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.AbstractClientEncryptionTextExplicitEncryptionTest; +import com.mongodb.client.MongoClient; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; +import com.mongodb.reactivestreams.client.vault.ClientEncryptions; + +public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return new SyncMongoClient(MongoClients.create(settings)); + } + + @Override + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return new SyncClientEncryption(ClientEncryptions.create(settings)); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java new file mode 100644 index 0000000000..68bcd76469 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java @@ -0,0 +1,324 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoException; +import com.mongodb.MongoNamespace; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DropCollectionOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.TextOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.connection.ServerVersion; +import com.mongodb.fixture.EncryptionFixture; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.mongodb.ClusterFixture.getDefaultDatabaseName; +import static com.mongodb.ClusterFixture.getMongoCryptVersion; +import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; +import static com.mongodb.ClusterFixture.isStandalone; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getDefaultDatabase; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; +import static java.util.Arrays.asList; +import static org.junit.Assume.assumeTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static util.JsonPoweredTestHelper.getTestDocument; + +public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { + + private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 15, 1)); + private MongoClient encryptedClient; + private MongoDatabase encryptedDatabase; + private ClientEncryption clientEncryption; + private BsonBinary key1Id; + + protected abstract MongoClient createMongoClient(MongoClientSettings settings); + protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); + + + @BeforeEach + public void setUp() { + assumeTrue("Text explicit encryption tests disabled", hasEncryptionTestsEnabled()); + assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0); + assumeTrue(serverVersionAtLeast(8, 2)); + assumeFalse(isStandalone()); + + MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); + BsonDocument encryptedFieldsPrefixSuffix = bsonDocumentFromPath("encryptedFields-prefix-suffix.json"); + BsonDocument encryptedFieldsSubstring = bsonDocumentFromPath("encryptedFields-substring.json"); + BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); + + MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); + database.getCollection("prefix-suffix") + .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); + database.createCollection("prefix-suffix", + new CreateCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); + + database.getCollection("substring") + .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsSubstring)); + database.createCollection("substring", + new CreateCollectionOptions().encryptedFields(encryptedFieldsSubstring)); + + MongoCollection dataKeysCollection = getMongoClient() + .getDatabase(dataKeysNamespace.getDatabaseName()) + .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) + .withWriteConcern(WriteConcern.MAJORITY); + + dataKeysCollection.drop(); + dataKeysCollection.insertOne(key1Document); + key1Id = key1Document.getBinary("_id"); + + Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); + + clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()); + + encryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings( + AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassQueryAnalysis(true) + .build()) + .build()); + + encryptedDatabase = encryptedClient.getDatabase(getDefaultDatabaseName()).withWriteConcern(WriteConcern.MAJORITY); + + EncryptOptions prefixSuffixEncryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), prefixSuffixEncryptOptions); + + encryptedDatabase + .getCollection("prefix-suffix") + .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); + + EncryptOptions substringEncryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), substringEncryptOptions); + + encryptedDatabase + .getCollection("substring") + .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); + } + + @Test + @DisplayName("Case 1: can find a document by prefix") + public void test1CanFindADocumentByPrefix() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("prefixPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); + Document result = encryptedDatabase.getCollection("prefix-suffix") + .find(new Document("$expr", + new Document("$encStrStartsWith", + new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); + + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 2: can find a document by suffix") + public void test2CanFindADocumentBySuffix() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("suffixPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); + Document result = encryptedDatabase.getCollection("prefix-suffix") + .find(new Document("$expr", + new Document("$encStrEndsWith", + new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); + + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 3: assert no document found by prefix") + public void test3AssertNoDocumentFoundByPrefix() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("prefixPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); + Document result = encryptedDatabase.getCollection("prefix-suffix") + .find(new Document("$expr", + new Document("$encStrStartsWith", + new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); + + assertNull(result); + } + + @Test + @DisplayName("Case 4: assert no document found by suffix") + public void test4AssertNoDocumentFoundByPrefix() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("suffixPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); + Document result = encryptedDatabase.getCollection("prefix-suffix") + .find(new Document("$expr", + new Document("$encStrEndsWith", + new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); + + assertNull(result); + } + + @Test + @DisplayName("Case 5: can find a document by substring") + public void test5CanFindADocumentBySubstring() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("substringPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("bar"), encryptOptions); + Document result = encryptedDatabase.getCollection("substring") + .find(new Document("$expr", + new Document("$encStrContains", + new Document("input", "$encryptedText").append("substring", encrypted)))).first(); + + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 6: assert no document found by substring") + public void test6AssertNoDocumentFoundBySubstring() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("substringPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + + BsonBinary encrypted = clientEncryption.encrypt(new BsonString("qux"), encryptOptions); + Document result = encryptedDatabase.getCollection("substring") + .find(new Document("$expr", + new Document("$encStrContains", + new Document("input", "$encryptedText").append("substring", encrypted)))).first(); + + assertNull(result); + } + + @Test + @DisplayName("Case 7: assert `contentionFactor` is required") + public void test7AssertContentionFactorIsRequired() { + EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + .keyId(key1Id) + .queryType("prefixPreview") + .textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + ); + MongoException exception = assertThrows(MongoException.class, () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); + assertTrue(exception.getMessage().contains("contention factor is required for textPreview algorithm")); + } + + + @AfterEach + @SuppressWarnings("try") + public void cleanUp() { + //noinspection EmptyTryBlock + getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); + try (ClientEncryption ignored = this.clientEncryption; + MongoClient ignored1 = this.encryptedClient + ) { + // just using try-with-resources to ensure they all get closed, even in the case of exceptions + } + } + + private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { + actualDocument.remove("__safeContent__"); + assertEquals(expectedDocument, actualDocument); + } + + private static BsonDocument bsonDocumentFromPath(final String path) { + return getTestDocument("client-side-encryption/etc/data/" + path); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java new file mode 100644 index 0000000000..23bd9ec135 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return MongoClients.create(settings); + } + + @Override + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return ClientEncryptions.create(settings); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 35f1e93f77..6f6e5bb66c 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -69,6 +69,7 @@ import java.util.stream.Collectors; import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; +import static com.mongodb.ClusterFixture.getEnv; import static com.mongodb.ClusterFixture.getMultiMongosConnectionString; import static com.mongodb.ClusterFixture.isLoadBalanced; import static com.mongodb.ClusterFixture.isSharded; @@ -502,7 +503,15 @@ private void initClient(final BsonDocument entity, final String id, } if (entity.containsKey("autoEncryptOpts")) { AutoEncryptionSettings.Builder builder = AutoEncryptionSettings.builder(); - for (Map.Entry entry : entity.getDocument("autoEncryptOpts").entrySet()) { + BsonDocument autoEncryptOpts = entity.getDocument("autoEncryptOpts"); + + String cryptSharedLibPath = getEnv("CRYPT_SHARED_LIB_PATH", ""); + if (!cryptSharedLibPath.isEmpty()) { + BsonDocument extraOptions = autoEncryptOpts.getDocument("extraOptions", new BsonDocument()); + autoEncryptOpts.put("extraOptions", extraOptions.append("cryptSharedLibPath", new BsonString(cryptSharedLibPath))); + } + + for (Map.Entry entry : autoEncryptOpts.entrySet()) { switch (entry.getKey()) { case "bypassAutoEncryption": builder.bypassAutoEncryption(entry.getValue().asBoolean().getValue()); @@ -531,6 +540,9 @@ private void initClient(final BsonDocument entity, final String id, case "mongocryptdBypassSpawn": extraOptions.put(extraOptionsEntry.getKey(), extraOptionsEntry.getValue().asBoolean().getValue()); break; + case "cryptSharedLibPath": + extraOptions.put(extraOptionsEntry.getKey(), extraOptionsEntry.getValue().asString().getValue()); + break; default: throw new UnsupportedOperationException("Unsupported extra encryption option: " + extraOptionsEntry.getKey()); } @@ -740,6 +752,7 @@ private TransactionOptions getTransactionOptions(final BsonDocument options) { public void close() { cursors.values().forEach(MongoCursor::close); sessions.values().forEach(ClientSession::close); + clientEncryptions.values().forEach(ClientEncryption::close); clients.values().forEach(MongoClient::close); clientLoggingInterceptors.values().forEach(TestLoggingInterceptor::close); threads.values().forEach(ExecutorService::shutdownNow); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java b/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java index 2883e911cd..a60901e8db 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java @@ -20,11 +20,13 @@ import com.mongodb.connection.ServerVersion; import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonString; import org.bson.BsonValue; import java.util.Map; import java.util.Objects; +import static com.mongodb.ClusterFixture.getMongoCryptVersion; import static com.mongodb.ClusterFixture.getServerParameters; import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; import static com.mongodb.JsonTestServerVersionChecker.getMaxServerVersionForField; @@ -99,6 +101,16 @@ public static boolean runOnRequirementsMet(final BsonArray runOnRequirements, fi requirementMet = false; break requirementLoop; } + if (curRequirement.getValue().isDocument()) { + BsonDocument csfleRequirements = curRequirement.getValue().asDocument(); + ServerVersion mongoCryptSharedLibVersion = getMongoCryptVersion(); + ServerVersion minLibmongocryptVersion = getMinServerVersion(csfleRequirements + .getString("minLibmongocryptVersion", new BsonString("0.0.0")).getValue()); + if (mongoCryptSharedLibVersion.compareTo(minLibmongocryptVersion) < 0) { + requirementMet = false; + break requirementLoop; + } + } break; default: throw new UnsupportedOperationException("Unsupported runOnRequirement: " + curRequirement.getKey()); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedClientEncryptionHelper.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedClientEncryptionHelper.java index c9c0d380c5..dbc919cdc5 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedClientEncryptionHelper.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedClientEncryptionHelper.java @@ -65,8 +65,15 @@ static Map> createKmsProvidersMap(final BsonDocument switch (kmsProviderKey) { case "aws": case "aws:name1": - setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "accessKeyId", "AWS_ACCESS_KEY_ID"); - setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "secretAccessKey", "AWS_SECRET_ACCESS_KEY"); + // awsTemporary uses `aws` and includes a `sessionToken`. + if (kmsProviderOptions.containsKey("sessionToken")) { + setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "accessKeyId", "AWS_TEMP_ACCESS_KEY_ID"); + setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "secretAccessKey", "AWS_TEMP_SECRET_ACCESS_KEY"); + setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "sessionToken", "AWS_TEMP_SESSION_TOKEN"); + } else { + setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "accessKeyId", "AWS_ACCESS_KEY_ID"); + setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "secretAccessKey", "AWS_SECRET_ACCESS_KEY"); + } break; case "aws:name2": setKmsProviderProperty(kmsProviderMap, kmsProviderOptions, "accessKeyId", "AWS_ACCESS_KEY_ID_AWS_KMS_NAMED"); @@ -160,7 +167,7 @@ private static void setKmsProviderProperty(final Map kmsProvider } BsonValue kmsValue = kmsProviderOptions.get(key); - if (kmsValue.isString()) { + if (kmsValue.isString() && !key.equals("sessionToken")) { kmsProviderMap.put(key, decodeKmsProviderString(kmsValue.asString().getValue())); } else { kmsProviderMap.put(key, kmsValue); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index fa47de0117..656698e69b 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -110,7 +110,7 @@ public abstract class UnifiedTest { private static final Set PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton( "wait queue timeout errors include details about checked out connections"); - private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.23"; + private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.25"; private static final List MAX_SUPPORTED_SCHEMA_VERSION_COMPONENTS = Arrays.stream(MAX_SUPPORTED_SCHEMA_VERSION.split("\\.")) .map(Integer::parseInt) .collect(Collectors.toList()); @@ -518,16 +518,25 @@ private static void assertOperationResult(final UnifiedTestContext context, fina context.getAssertionContext().push(ContextElement.ofCompletedOperation(operation, result, operationIndex)); if (!operation.getBoolean("ignoreResultAndError", BsonBoolean.FALSE).getValue()) { + Exception operationException = result.getException(); if (operation.containsKey("expectResult")) { - assertNull(result.getException(), - context.getAssertionContext().getMessage("The operation expects a result but an exception occurred")); - context.getValueMatcher().assertValuesMatch(operation.get("expectResult"), result.getResult()); + BsonValue expectedResult = operation.get("expectResult"); + if (expectedResult.isDocument() && expectedResult.asDocument().containsKey("isTimeoutError")) { + assertNotNull(operationException, + context.getAssertionContext().getMessage("The operation expects a timeout error but no timeout exception was" + + " thrown")); + context.getErrorMatcher().assertErrorsMatch(expectedResult.asDocument(), operationException); + } else { + assertNull(operationException, + context.getAssertionContext().getMessage("The operation expects a result but an exception occurred")); + context.getValueMatcher().assertValuesMatch(expectedResult, result.getResult()); + } } else if (operation.containsKey("expectError")) { - assertNotNull(result.getException(), + assertNotNull(operationException, context.getAssertionContext().getMessage("The operation expects an error but no exception was thrown")); - context.getErrorMatcher().assertErrorsMatch(operation.getDocument("expectError"), result.getException()); + context.getErrorMatcher().assertErrorsMatch(operation.getDocument("expectError"), operationException); } else { - assertNull(result.getException(), + assertNull(operationException, context.getAssertionContext().getMessage("The operation expects no error but an exception occurred")); } } @@ -1086,7 +1095,7 @@ private BsonDocument addInitialDataAndGetClusterTime() { new MongoNamespace(curDataSet.getString("databaseName").getValue(), curDataSet.getString("collectionName").getValue())); - helper.create(WriteConcern.MAJORITY, curDataSet.getDocument("createOptions", new BsonDocument())); + helper.dropAndCreate(curDataSet.getDocument("createOptions", new BsonDocument())); BsonArray documentsArray = curDataSet.getArray("documents", new BsonArray()); if (!documentsArray.isEmpty()) { diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 684a5794d2..005f584296 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -62,6 +62,10 @@ public static void applyCustomizations(final TestDef def) { .test("change-streams", "change-streams-errors", "Change Stream should error when an invalid aggregation stage is passed in") .test("change-streams", "change-streams-errors", "The watch helper must not throw a custom exception when executed against a single server topology, but instead depend on a server error"); + // Client side encryption (QE) + def.skipJira("https://jira.mongodb.org/browse/JAVA-5675 Support QE with Client.bulkWrite") + .file("client-side-encryption/tests/unified", "client bulkWrite with queryable encryption"); + // client-side-operation-timeout (CSOT) def.skipNoncompliantReactive("No good way to fulfill tryNext() requirement with a Publisher") @@ -77,7 +81,6 @@ public static void applyCustomizations(final TestDef def) { "timeoutMS applied to entire download, not individual parts"); def.skipJira("https://jira.mongodb.org/browse/JAVA-5491") - .testContains("client-side-operations-timeout", "dropIndex") .when(() -> !serverVersionLessThan(8, 3)) .test("client-side-operations-timeout", "operations ignore deprecated timeout options if timeoutMS is set", "socketTimeoutMS is ignored if timeoutMS is set - dropIndex on collection") diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6343e1bc1c..812753151d 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -54,7 +54,7 @@ val jnaLibsPath: String = System.getProperty("jnaLibsPath", "${jnaResourcesDir}$ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) // Download jnaLibs that match the git tag or revision to jnaResourcesBuildDir -val downloadRevision = "1.14.1" +val downloadRevision = "1.15.1" val binariesArchiveName = "libmongocrypt-java.tar.gz" /** diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java index 34a102fbde..41cc8ced31 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java @@ -622,6 +622,37 @@ public interface mongocrypt_random_fn extends Callback { public static native boolean mongocrypt_ctx_setopt_algorithm_range (mongocrypt_ctx_t ctx, mongocrypt_binary_t opts); + + /** + * Set options for explicit encryption with the "textPreview" algorithm. "prefix" and "suffix" can both be set. + * NOTE: "textPreview" is experimental only and may be removed in a future non-major release. + * opts is a BSON document of the form: + * + * { + * "caseSensitive": bool, + * "diacriticSensitive": bool, + * "prefix": Optional{ + * "strMaxQueryLength": Int32, + * "strMinQueryLength": Int32, + * }, + * "suffix": Optional{ + * "strMaxQueryLength": Int32, + * "strMinQueryLength": Int32, + * }, + * "substring": Optional{ + * "strMaxLength": Int32, + * "strMaxQueryLength": Int32, + * "strMinQueryLength": Int32, + * }, + * } + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param opts BSON. + * @return A boolean indicating success. If false, an error status is set. + * @since 5.6 + */ + public static native boolean mongocrypt_ctx_setopt_algorithm_text(mongocrypt_ctx_t ctx, mongocrypt_binary_t opts); + /** * Initialize new @ref mongocrypt_t object. * @@ -631,7 +662,6 @@ public interface mongocrypt_random_fn extends Callback { public static native boolean mongocrypt_init(mongocrypt_t crypt); - /** * Get the status associated with a @ref mongocrypt_t object. * diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContext.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContext.java index 573e1cdf88..37269e0f1e 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContext.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContext.java @@ -32,6 +32,11 @@ public interface MongoCryptContext extends Closeable { * The possible states. */ enum State { + /** + * There has been an error + */ + ERROR(CAPI.MONGOCRYPT_CTX_ERROR), + /** * Needs collection information from the cluster encrypting to */ diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContextImpl.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContextImpl.java index 502784fdb7..06f282aa86 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContextImpl.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptContextImpl.java @@ -58,7 +58,11 @@ class MongoCryptContextImpl implements MongoCryptContext { @Override public State getState() { isTrue("open", !closed); - return State.fromIndex(mongocrypt_ctx_state(wrapped)); + State state = State.fromIndex(mongocrypt_ctx_state(wrapped)); + if (state.equals(State.ERROR)) { + throwExceptionFromStatus(); + } + return state; } @Override diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java index 4f131f5d4e..774b9e718c 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java @@ -19,6 +19,7 @@ import com.mongodb.crypt.capi.MongoCryptException; import com.mongodb.internal.crypt.capi.CAPI.cstring; +import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_binary_t; import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_t; import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_log_fn_t; import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_status_t; @@ -32,8 +33,11 @@ import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Supplier; +import static com.mongodb.internal.crypt.capi.CAPI.MONGOCRYPT_CTX_ERROR; import static com.mongodb.internal.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_ERROR; import static com.mongodb.internal.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_FATAL; import static com.mongodb.internal.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_INFO; @@ -41,21 +45,18 @@ import static com.mongodb.internal.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_WARNING; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_crypt_shared_lib_version_string; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_datakey_init; -import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_decrypt_init; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_encrypt_init; -import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_explicit_decrypt_init; -import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_explicit_encrypt_expression_init; -import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_explicit_encrypt_init; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_new; -import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_rewrap_many_datakey_init; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_algorithm; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_algorithm_range; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_algorithm_text; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_contention_factor; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_alt_name; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_encryption_key; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_id; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_material; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_setopt_query_type; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_ctx_state; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_destroy; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_init; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_is_crypto_available; @@ -84,6 +85,22 @@ import static org.bson.assertions.Assertions.isTrue; import static org.bson.assertions.Assertions.notNull; +/** + * MongoCryptImpl is the main implementation of the {@link MongoCrypt} interface. + *

    + * This class is responsible for configuring and managing the native libmongocrypt context, + * handling encryption and decryption operations, and bridging Java cryptographic hooks + * when required. It wraps the native resource and provides context creation methods for + * various cryptographic operations. + *

    + * Key responsibilities: + *

      + *
    • Configures libmongocrypt with KMS providers, schema maps, encrypted fields, and other options.
    • + *
    • Registers Java cryptographic hooks if native crypto is not available.
    • + *
    • Provides context creation for encryption, decryption, key management, and explicit operations.
    • + *
    • Manages native resource lifecycle and error handling.
    • + *
    + */ class MongoCryptImpl implements MongoCrypt { private static final Logger LOGGER = Loggers.getLogger(); private final mongocrypt_t wrapped; @@ -113,6 +130,12 @@ class MongoCryptImpl implements MongoCrypt { private final AtomicBoolean closed; + /** + * Constructs a MongoCryptImpl instance and configures the native libmongocrypt context. + *

    + * Registers log handlers, cryptographic hooks, and sets up KMS providers and other options. + * Throws MongoCryptException if initialization fails. + */ MongoCryptImpl(final MongoCryptOptions options) { closed = new AtomicBoolean(); wrapped = mongocrypt_new(); @@ -161,9 +184,8 @@ class MongoCryptImpl implements MongoCrypt { } if (options.getLocalKmsProviderOptions() != null) { - try (BinaryHolder localMasterKeyBinaryHolder = toBinary(options.getLocalKmsProviderOptions().getLocalMasterKey())) { - configure(() -> mongocrypt_setopt_kms_provider_local(wrapped, localMasterKeyBinaryHolder.getBinary())); - } + withBinaryHolder(options.getLocalKmsProviderOptions().getLocalMasterKey(), + binary -> configure(() -> mongocrypt_setopt_kms_provider_local(wrapped, binary))); } if (options.getAwsKmsProviderOptions() != null) { @@ -177,18 +199,15 @@ class MongoCryptImpl implements MongoCrypt { } if (options.getKmsProviderOptions() != null) { - try (BinaryHolder binaryHolder = toBinary(options.getKmsProviderOptions())) { - configure(() -> mongocrypt_setopt_kms_providers(wrapped, binaryHolder.getBinary())); - } + withBinaryHolder(options.getKmsProviderOptions(), + binary -> configure(() -> mongocrypt_setopt_kms_providers(wrapped, binary))); } if (options.getLocalSchemaMap() != null) { BsonDocument localSchemaMapDocument = new BsonDocument(); localSchemaMapDocument.putAll(options.getLocalSchemaMap()); - try (BinaryHolder localSchemaMapBinaryHolder = toBinary(localSchemaMapDocument)) { - configure(() -> mongocrypt_setopt_schema_map(wrapped, localSchemaMapBinaryHolder.getBinary())); - } + withBinaryHolder(localSchemaMapDocument, binary -> configure(() -> mongocrypt_setopt_schema_map(wrapped, binary))); } if (options.isBypassQueryAnalysis()) { @@ -204,9 +223,8 @@ class MongoCryptImpl implements MongoCrypt { BsonDocument localEncryptedFieldsMap = new BsonDocument(); localEncryptedFieldsMap.putAll(options.getEncryptedFieldsMap()); - try (BinaryHolder localEncryptedFieldsMapHolder = toBinary(localEncryptedFieldsMap)) { - configure(() -> mongocrypt_setopt_encrypted_field_config_map(wrapped, localEncryptedFieldsMapHolder.getBinary())); - } + withBinaryHolder(localEncryptedFieldsMap, + binary -> configure(() -> mongocrypt_setopt_encrypted_field_config_map(wrapped, binary))); } options.getSearchPaths().forEach(p -> mongocrypt_setopt_append_crypt_shared_lib_search_path(wrapped, new cstring(p))); @@ -217,116 +235,93 @@ class MongoCryptImpl implements MongoCrypt { configure(() -> mongocrypt_init(wrapped)); } + /** + * Creates an encryption context for the given database and command document. + */ @Override public MongoCryptContext createEncryptionContext(final String database, final BsonDocument commandDocument) { isTrue("open", !closed.get()); notNull("database", database); notNull("commandDocument", commandDocument); - mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); - if (context == null) { - throwExceptionFromStatus(); - } - - try (BinaryHolder commandDocumentBinaryHolder = toBinary(commandDocument)) { - configure(() -> mongocrypt_ctx_encrypt_init(context, new cstring(database), -1, - commandDocumentBinaryHolder.getBinary()), context); - return new MongoCryptContextImpl(context); - } + return createMongoCryptContext(commandDocument, createNewMongoCryptContext(), + (context, binary) -> mongocrypt_ctx_encrypt_init(context, new cstring(database), -1, binary)); } + /** + * Creates a decryption context for the given document. + */ @Override public MongoCryptContext createDecryptionContext(final BsonDocument document) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); - if (context == null) { - throwExceptionFromStatus(); - } - try (BinaryHolder documentBinaryHolder = toBinary(document)){ - configure(() -> mongocrypt_ctx_decrypt_init(context, documentBinaryHolder.getBinary()), context); - } - return new MongoCryptContextImpl(context); + return createMongoCryptContext(document, createNewMongoCryptContext(), CAPI::mongocrypt_ctx_decrypt_init); } + /** + * Creates a data key context for the specified KMS provider and options. + */ @Override public MongoCryptContext createDataKeyContext(final String kmsProvider, final MongoDataKeyOptions options) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); - if (context == null) { - throwExceptionFromStatus(); - } + mongocrypt_ctx_t context = createNewMongoCryptContext(); BsonDocument keyDocument = new BsonDocument("provider", new BsonString(kmsProvider)); BsonDocument masterKey = options.getMasterKey(); if (masterKey != null) { masterKey.forEach(keyDocument::append); } - try (BinaryHolder masterKeyHolder = toBinary(keyDocument)) { - configure(() -> mongocrypt_ctx_setopt_key_encryption_key(context, masterKeyHolder.getBinary()), context); - } + withBinaryHolder(keyDocument, + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_encryption_key(context, binary))); if (options.getKeyAltNames() != null) { for (String cur : options.getKeyAltNames()) { - try (BinaryHolder keyAltNameBinaryHolder = toBinary(new BsonDocument("keyAltName", new BsonString(cur)))) { - configure(() -> mongocrypt_ctx_setopt_key_alt_name(context, keyAltNameBinaryHolder.getBinary()), context); - } + withBinaryHolder(new BsonDocument("keyAltName", new BsonString(cur)), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_alt_name(context, binary))); } } if (options.getKeyMaterial() != null) { - try (BinaryHolder keyMaterialBinaryHolder = toBinary(new BsonDocument("keyMaterial", new BsonBinary(options.getKeyMaterial())))) { - configure(() -> mongocrypt_ctx_setopt_key_material(context, keyMaterialBinaryHolder.getBinary()), context); - } + withBinaryHolder(new BsonDocument("keyMaterial", new BsonBinary(options.getKeyMaterial())), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_material(context, binary))); } - if (!mongocrypt_ctx_datakey_init(context)) { - MongoCryptContextImpl.throwExceptionFromStatus(context); - } + configureContext(context, () -> mongocrypt_ctx_datakey_init(context)); return new MongoCryptContextImpl(context); } + /** + * Creates an explicit encryption context + */ @Override public MongoCryptContext createExplicitEncryptionContext(final BsonDocument document, final MongoExplicitEncryptOptions options) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = configureExplicitEncryption(options); - - try (BinaryHolder documentBinaryHolder = toBinary(document)) { - configure(() -> mongocrypt_ctx_explicit_encrypt_init(context, documentBinaryHolder.getBinary()), context); - } - - return new MongoCryptContextImpl(context); + return createMongoCryptContext(document, configureExplicitEncryption(options), CAPI::mongocrypt_ctx_explicit_encrypt_init); } + /** + * Creates an explicit encrypt *expression* context + */ @Override public MongoCryptContext createEncryptExpressionContext(final BsonDocument document, final MongoExplicitEncryptOptions options) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = configureExplicitEncryption(options); - - try (BinaryHolder documentBinaryHolder = toBinary(document)) { - configure(() -> mongocrypt_ctx_explicit_encrypt_expression_init(context, documentBinaryHolder.getBinary()), context); - } - return new MongoCryptContextImpl(context); + return createMongoCryptContext(document, configureExplicitEncryption(options), CAPI::mongocrypt_ctx_explicit_encrypt_expression_init); } + /** + * Creates an explicit decryption context + */ @Override public MongoCryptContext createExplicitDecryptionContext(final BsonDocument document) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); - if (context == null) { - throwExceptionFromStatus(); - } - try (BinaryHolder binaryHolder = toBinary(document)) { - configure(() -> mongocrypt_ctx_explicit_decrypt_init(context, binaryHolder.getBinary()), context); - } - return new MongoCryptContextImpl(context); + return createMongoCryptContext(document, createNewMongoCryptContext(), CAPI::mongocrypt_ctx_explicit_decrypt_init); } + /** + * Creates a rewrap many data keys context + */ @Override public MongoCryptContext createRewrapManyDatakeyContext(final BsonDocument filter, final MongoRewrapManyDataKeyOptions options) { isTrue("open", !closed.get()); - mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); - if (context == null) { - throwExceptionFromStatus(); - } + mongocrypt_ctx_t context = createNewMongoCryptContext(); if (options != null && options.getProvider() != null) { BsonDocument keyDocument = new BsonDocument("provider", new BsonString(options.getProvider())); @@ -334,23 +329,27 @@ public MongoCryptContext createRewrapManyDatakeyContext(final BsonDocument filte if (masterKey != null) { masterKey.forEach(keyDocument::append); } - try (BinaryHolder binaryHolder = toBinary(keyDocument)) { - configure(() -> mongocrypt_ctx_setopt_key_encryption_key(context, binaryHolder.getBinary()), context); - } + withBinaryHolder(keyDocument, + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_encryption_key(context, binary))); } - try (BinaryHolder binaryHolder = toBinary(filter)) { - configure(() -> mongocrypt_ctx_rewrap_many_datakey_init(context, binaryHolder.getBinary()), context); - } - return new MongoCryptContextImpl(context); + return createMongoCryptContext(filter, context, CAPI::mongocrypt_ctx_rewrap_many_datakey_init); } + /** + * Returns the version string of the loaded crypt shared library. + */ @Override public String getCryptSharedLibVersionString() { cstring versionString = mongocrypt_crypt_shared_lib_version_string(wrapped, null); return versionString == null ? null : versionString.toString(); } + /** + * Closes the native libmongocrypt resource. + *

    + * This should be called when the instance is no longer needed to release native resources. + */ @Override public void close() { if (!closed.getAndSet(true)) { @@ -358,52 +357,100 @@ public void close() { } } - private mongocrypt_ctx_t configureExplicitEncryption(final MongoExplicitEncryptOptions options) { + /** + * Helper to create a MongoCryptContext from a BSON document and a native context. + *

    + * Applies the given configuration function and checks for errors. + */ + private MongoCryptContext createMongoCryptContext(final BsonDocument document, final mongocrypt_ctx_t context, + final BiFunction configureFunction) { + withBinaryHolder(document, + binary -> { + if (!configureFunction.apply(context, binary)) { + MongoCryptContextImpl.throwExceptionFromStatus(context); + } + }); + if (mongocrypt_ctx_state(context) == MONGOCRYPT_CTX_ERROR) { + MongoCryptContextImpl.throwExceptionFromStatus(context); + } + return new MongoCryptContextImpl(context); + } + + /** + * Helper to create a new native mongocrypt_ctx_t context. + *

    + * Throws if context creation fails. + */ + private mongocrypt_ctx_t createNewMongoCryptContext() { mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); if (context == null) { throwExceptionFromStatus(); } + return context; + } + /** + * Configures explicit encryption options on a new native context. + *

    + * Applies key ID, key alt name, algorithm, query type, contention factor, and other options. + */ + private mongocrypt_ctx_t configureExplicitEncryption(final MongoExplicitEncryptOptions options) { + mongocrypt_ctx_t context = createNewMongoCryptContext(); if (options.getKeyId() != null) { - try (BinaryHolder keyIdBinaryHolder = toBinary(ByteBuffer.wrap(options.getKeyId().getData()))) { - configure(() -> mongocrypt_ctx_setopt_key_id(context, keyIdBinaryHolder.getBinary()), context); - } - } else if (options.getKeyAltName() != null) { - try (BinaryHolder keyAltNameBinaryHolder = toBinary(new BsonDocument("keyAltName", new BsonString(options.getKeyAltName())))) { - configure(() -> mongocrypt_ctx_setopt_key_alt_name(context, keyAltNameBinaryHolder.getBinary()), context); - } + withBinaryHolder(ByteBuffer.wrap(options.getKeyId().getData()), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_id(context, binary))); + } + + if (options.getKeyAltName() != null) { + withBinaryHolder(new BsonDocument("keyAltName", new BsonString(options.getKeyAltName())), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_key_alt_name(context, binary))); } if (options.getAlgorithm() != null) { - configure(() -> mongocrypt_ctx_setopt_algorithm(context, new cstring(options.getAlgorithm()), -1), context); + configureContext(context, () -> mongocrypt_ctx_setopt_algorithm(context, new cstring(options.getAlgorithm()), -1)); } if (options.getQueryType() != null) { - configure(() -> mongocrypt_ctx_setopt_query_type(context, new cstring(options.getQueryType()), -1), context); + configureContext(context, () -> mongocrypt_ctx_setopt_query_type(context, new cstring(options.getQueryType()), -1)); } if (options.getContentionFactor() != null) { - configure(() -> mongocrypt_ctx_setopt_contention_factor(context, options.getContentionFactor()), context); + configureContext(context, () -> mongocrypt_ctx_setopt_contention_factor(context, options.getContentionFactor())); } if (options.getRangeOptions() != null) { - try (BinaryHolder rangeOptionsHolder = toBinary(options.getRangeOptions())) { - configure(() -> mongocrypt_ctx_setopt_algorithm_range(context, rangeOptionsHolder.getBinary()), context); - } + withBinaryHolder(options.getRangeOptions(), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_algorithm_range(context, binary))); + } + if (options.getTextOptions() != null) { + withBinaryHolder(options.getTextOptions(), + binary -> configureContext(context, () -> mongocrypt_ctx_setopt_algorithm_text(context, binary))); } return context; } - + /** + * Configures the main mongocrypt instance with the given supplier that indicates if configuration was successful or not. + *

    + * Throws an exception derived from the mongocrypt status if the configuration fails. + */ private void configure(final Supplier successSupplier) { if (!successSupplier.get()) { throwExceptionFromStatus(); } } - private void configure(final Supplier successSupplier, final mongocrypt_ctx_t context) { + /** + * Configures a mongocrypt_ctx_t context instance with the given supplier that indicates if configuration was successful or not. + *

    + * Throws an exception derived from the contexts mongocrypt status if the configuration fails. + */ + private void configureContext(final mongocrypt_ctx_t context, final Supplier successSupplier) { if (!successSupplier.get()) { MongoCryptContextImpl.throwExceptionFromStatus(context); } } + /** + * Throws a MongoCryptException based on the current status of the native context. + */ private void throwExceptionFromStatus() { mongocrypt_status_t status = mongocrypt_status_new(); mongocrypt_status(wrapped, status); @@ -413,6 +460,29 @@ private void throwExceptionFromStatus() { throw e; } + /** + * Utility method to handle BinaryHolder resource management for ByteBuffer values. + */ + private static void withBinaryHolder(final ByteBuffer value, final Consumer consumer) { + try (BinaryHolder binaryHolder = toBinary(value)) { + consumer.accept(binaryHolder.getBinary()); + } + } + + /** + * Utility method to handle BinaryHolder resource management for BsonDocument values. + */ + private static void withBinaryHolder(final BsonDocument value, final Consumer consumer) { + try (BinaryHolder binaryHolder = toBinary(value)) { + consumer.accept(binaryHolder.getBinary()); + } + } + + /** + * LogCallback bridges native log events to the Java logger. + *

    + * Handles different log levels and forwards messages to the appropriate logger method. + */ static class LogCallback implements mongocrypt_log_fn_t { @Override public void log(final int level, final cstring message, final int messageLength, final Pointer ctx) { diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java index 9080a77374..c08608ca59 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java @@ -20,8 +20,6 @@ import org.bson.BsonBinary; import org.bson.BsonDocument; -import java.util.Objects; - /** * Options for explicit encryption. */ @@ -32,6 +30,7 @@ public final class MongoExplicitEncryptOptions { private final Long contentionFactor; private final String queryType; private final BsonDocument rangeOptions; + private final BsonDocument textOptions; /** * The builder for the options @@ -43,6 +42,7 @@ public static final class Builder { private Long contentionFactor; private String queryType; private BsonDocument rangeOptions; + private BsonDocument textOptions; private Builder() { } @@ -87,7 +87,7 @@ public Builder algorithm(final String algorithm) { /** * The contention factor. * - *

    It is an error to set contentionFactor when algorithm is not "Indexed". + *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    * @param contentionFactor the contention factor * @return this * @since 1.5 @@ -100,7 +100,7 @@ public Builder contentionFactor(final Long contentionFactor) { /** * The QueryType. * - *

    It is an error to set queryType when algorithm is not "Indexed".

    + *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    * * @param queryType the query type * @return this @@ -114,7 +114,7 @@ public Builder queryType(final String queryType) { /** * The Range Options. * - *

    It is an error to set rangeOptions when the algorithm is not "range".

    + *

    Only applies when algorithm is "Range".

    * * @param rangeOptions the range options * @return this @@ -125,6 +125,20 @@ public Builder rangeOptions(final BsonDocument rangeOptions) { return this; } + /** + * The Text Options. + * + *

    Only applies when algorithm is "TextPreview".

    + * + * @param textOptions the text options + * @return this + * @since 5.6 + */ + public Builder textOptions(final BsonDocument textOptions) { + this.textOptions = textOptions; + return this; + } + /** * Build the options. * @@ -195,6 +209,15 @@ public BsonDocument getRangeOptions() { return rangeOptions; } + /** + * Gets the text options + * @return the text options + * @since 5.6 + */ + public BsonDocument getTextOptions() { + return textOptions; + } + private MongoExplicitEncryptOptions(final Builder builder) { this.keyId = builder.keyId; this.keyAltName = builder.keyAltName; @@ -202,15 +225,7 @@ private MongoExplicitEncryptOptions(final Builder builder) { this.contentionFactor = builder.contentionFactor; this.queryType = builder.queryType; this.rangeOptions = builder.rangeOptions; - if (!(Objects.equals(algorithm, "Indexed") || Objects.equals(algorithm, "Range"))) { - if (contentionFactor != null) { - throw new IllegalStateException( - "Invalid configuration, contentionFactor can only be set if algorithm is 'Indexed' or 'Range'"); - } else if (queryType != null) { - throw new IllegalStateException( - "Invalid configuration, queryType can only be set if algorithm is 'Indexed' or 'Range'"); - } - } + this.textOptions = builder.textOptions; } @Override @@ -222,6 +237,7 @@ public String toString() { + ", contentionFactor=" + contentionFactor + ", queryType='" + queryType + '\'' + ", rangeOptions=" + rangeOptions + + ", textOptions=" + textOptions + '}'; } } diff --git a/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java b/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java index 32e87714bb..14bb2a5ccd 100644 --- a/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java +++ b/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java @@ -265,18 +265,17 @@ public void testRangePreviewAlgorithmIsNotSupported() { MongoCrypt mongoCrypt = createMongoCrypt(); assertNotNull(mongoCrypt); + BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json"); BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json"); - IllegalStateException illegalStateException = assertThrows(IllegalStateException.class, () -> MongoExplicitEncryptOptions.builder() + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg=="))) .algorithm("RangePreview") - .queryType("range") - .contentionFactor(4L) .rangeOptions(rangeOptions) - .build()); + .build(); - assertEquals("Invalid configuration, contentionFactor can only be set if algorithm is 'Indexed' or 'Range'", - illegalStateException.getMessage()); + MongoCryptException exp = assertThrows(MongoCryptException.class, () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options)); + assertEquals("Algorithm 'rangePreview' is deprecated, please use 'range'", exp.getMessage()); mongoCrypt.close(); }