diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 7de253baac..b0772260ac 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -764,6 +764,16 @@ functions: set +o xtrace MONGODB_URI="${MONGODB_URI}" KMS_TLS_ERROR_TYPE=${KMS_TLS_ERROR_TYPE} .evergreen/run-kms-tls-tests.sh + "run-kms-retry-test": + - command: shell.exec + type: "test" + params: + working_dir: "src" + script: | + ${PREPARE_SHELL} + set +o xtrace + MONGODB_URI="${MONGODB_URI}" .evergreen/run-kms-retry-tests.sh + "run-csfle-aws-from-environment-test": - command: shell.exec type: "test" @@ -1632,6 +1642,17 @@ tasks: AUTH: "noauth" SSL: "nossl" + - name: "test-kms-retry-task" + tags: [ "kms-retry" ] + commands: + - func: "start-mongo-orchestration" + vars: + TOPOLOGY: "server" + AUTH: "noauth" + SSL: "nossl" + - func: "start-csfle-servers" + - func: "run-kms-retry-test" + - name: "test-csfle-aws-from-environment-task" tags: [ "csfle-aws-from-environment" ] commands: @@ -2528,6 +2549,12 @@ buildvariants: tasks: - name: ".kms-tls" + - matrix_name: "kms-retry-test" + matrix_spec: { os: "linux", version: [ "5.0" ], topology: [ "standalone" ] } + display_name: "CSFLE KMS Retry" + tasks: + - name: ".kms-retry" + - matrix_name: "csfle-aws-from-environment-test" matrix_spec: { os: "linux", version: [ "5.0" ], topology: [ "standalone" ] } display_name: "CSFLE AWS From Environment" diff --git a/.evergreen/run-kms-retry-tests.sh b/.evergreen/run-kms-retry-tests.sh new file mode 100755 index 0000000000..5f9e577a49 --- /dev/null +++ b/.evergreen/run-kms-retry-tests.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -o errexit # Exit the script with error if any of the commands fail + +# Supported/used environment variables: +# MONGODB_URI Set the suggested connection MONGODB_URI (including credentials and topology info) + +############################################ +# Main Program # +############################################ +RELATIVE_DIR_PATH="$(dirname "${BASH_SOURCE:-$0}")" +. "${RELATIVE_DIR_PATH}/setup-env.bash" +echo "Running KMS Retry tests" + +cp ${JAVA_HOME}/lib/security/cacerts mongo-truststore +${JAVA_HOME}/bin/keytool -importcert -trustcacerts -file ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -keystore mongo-truststore -storepass changeit -storetype JKS -noprompt + +export GRADLE_EXTRA_VARS="-Pssl.enabled=true -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit" + +./gradlew -version + +# Disable errexit so both suites run and their exit codes can be captured below. +set +o errexit + +./gradlew --stacktrace --info ${GRADLE_EXTRA_VARS} -Dorg.mongodb.test.uri=${MONGODB_URI} \ + -Dorg.mongodb.test.kms.retry.ca.path="${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" \ + driver-sync:cleanTest driver-sync:test --tests ClientSideEncryptionKmsRetryProseTest +first=$? +echo "sync exit code: $first" + +./gradlew --stacktrace --info ${GRADLE_EXTRA_VARS} -Dorg.mongodb.test.uri=${MONGODB_URI} \ + -Dorg.mongodb.test.kms.retry.ca.path="${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" \ + driver-reactive-streams:cleanTest driver-reactive-streams:test --tests ClientSideEncryptionKmsRetryProseTest +second=$? +echo "reactive exit code: $second" + +if [ $first -ne 0 ]; then + exit $first +elif [ $second -ne 0 ]; then + exit $second +else + exit 0 +fi diff --git a/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java b/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java index 240f5051c9..f29e32ee99 100644 --- a/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java +++ b/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java @@ -24,10 +24,12 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoConfigurationException; import com.mongodb.client.model.vault.RewrapManyDataKeyOptions; +import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.authentication.AwsCredentialHelper; import com.mongodb.internal.authentication.AzureCredentialHelper; import com.mongodb.internal.authentication.GcpCredentialHelper; import com.mongodb.internal.crypt.capi.MongoCryptOptions; +import com.mongodb.internal.time.Timeout; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.BsonDocumentWrapper; @@ -52,6 +54,32 @@ */ public final class MongoCryptHelper { + public static final String KMS_TIMEOUT_ERROR_MESSAGE = "KMS key decryption exceeded the timeout limit."; + + /** + * Throws a {@code MongoOperationTimeoutException} if the operation timeout has expired or the + * KMS retry backoff would exceed the remaining operation time. + * + * @param operationTimeout the operation timeout, or null if none + * @param backoffMicros the backoff to sleep before the next KMS attempt, in microseconds + */ + public static void checkKmsRetryBackoff(@Nullable final Timeout operationTimeout, final long backoffMicros) { + if (operationTimeout == null) { + return; + } + operationTimeout.run(TimeUnit.MICROSECONDS, + // infinite timeout: no CSOT budget to enforce; libmongocrypt's retry count is the only limit + () -> { }, + remainingMicros -> { + if (remainingMicros < backoffMicros) { + throw TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE); + } + }, + () -> { + throw TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE); + }); + } + public static MongoCryptOptions createMongoCryptOptions(final ClientEncryptionSettings settings) { return createMongoCryptOptions(settings.getKmsProviders(), false, emptyList(), emptyMap(), null, null, settings.getKeyExpiration(TimeUnit.MILLISECONDS)); diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java index 67ebf421c9..814825edde 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java @@ -48,24 +48,32 @@ import java.io.Closeable; import java.nio.channels.CompletionHandler; import java.nio.channels.InterruptedByTimeoutException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; +import static com.mongodb.internal.capi.MongoCryptHelper.KMS_TIMEOUT_ERROR_MESSAGE; +import static com.mongodb.internal.capi.MongoCryptHelper.checkKmsRetryBackoff; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.bson.assertions.Assertions.assertTrue; class KeyManagementService implements Closeable { private static final Logger LOGGER = Loggers.getLogger("client"); - private static final String TIMEOUT_ERROR_MESSAGE = "KMS key decryption exceeded the timeout limit."; private final Map kmsProviderSslContextMap; private final int timeoutMillis; private final TlsChannelStreamFactoryFactory tlsChannelStreamFactoryFactory; KeyManagementService(final Map kmsProviderSslContextMap, final int timeoutMillis) { + this(kmsProviderSslContextMap, timeoutMillis, new TlsChannelStreamFactoryFactory(new DefaultInetAddressResolver())); + } + + KeyManagementService(final Map kmsProviderSslContextMap, final int timeoutMillis, + final TlsChannelStreamFactoryFactory tlsChannelStreamFactoryFactory) { assertTrue("timeoutMillis > 0", timeoutMillis > 0); this.kmsProviderSslContextMap = kmsProviderSslContextMap; - this.tlsChannelStreamFactoryFactory = new TlsChannelStreamFactoryFactory(new DefaultInetAddressResolver()); + this.tlsChannelStreamFactoryFactory = tlsChannelStreamFactoryFactory; this.timeoutMillis = timeoutMillis; } @@ -74,6 +82,18 @@ public void close() { } Mono decryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Timeout operationTimeout) { + return Mono.defer(() -> { + long sleepMicros = keyDecryptor.sleepMicroseconds(); + if (sleepMicros > 0) { + checkKmsRetryBackoff(operationTimeout, sleepMicros); + return Mono.delay(Duration.of(sleepMicros, ChronoUnit.MICROS)) + .then(attemptDecryptKey(keyDecryptor, operationTimeout)); + } + return attemptDecryptKey(keyDecryptor, operationTimeout); + }).onErrorMap(this::unWrapException); + } + + private Mono attemptDecryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Timeout operationTimeout) { SocketSettings socketSettings = SocketSettings.builder() .connectTimeout(timeoutMillis, MILLISECONDS) .readTimeout(timeoutMillis, MILLISECONDS) @@ -86,88 +106,135 @@ Mono decryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Time LOGGER.info("Connecting to KMS server at " + serverAddress); return Mono.create(sink -> { - Stream stream = streamFactory.create(serverAddress); OperationContext operationContext = createOperationContext(operationTimeout, socketSettings); + Stream stream = streamFactory.create(serverAddress); stream.openAsync(operationContext, new AsyncCompletionHandler() { @Override public void completed(@Nullable final Void ignored) { - streamWrite(stream, keyDecryptor, operationContext, sink); + try { + streamWrite(stream, keyDecryptor, operationContext, operationTimeout, sink); + } catch (Throwable t) { + stream.close(); + sink.error(t); + } } @Override public void failed(final Throwable t) { stream.close(); - handleError(t, operationContext, sink); + failOrHandleError(t, keyDecryptor, operationTimeout, sink); } }); - }).onErrorMap(this::unWrapException); + }); } private void streamWrite(final Stream stream, final MongoKeyDecryptor keyDecryptor, - final OperationContext operationContext, final MonoSink sink) { + final OperationContext operationContext, @Nullable final Timeout operationTimeout, + final MonoSink sink) { List byteBufs = singletonList(new ByteBufNIO(keyDecryptor.getMessage())); stream.writeAsync(byteBufs, operationContext, new AsyncCompletionHandler() { @Override public void completed(@Nullable final Void aVoid) { - streamRead(stream, keyDecryptor, operationContext, sink); + try { + streamRead(stream, keyDecryptor, operationContext, operationTimeout, sink); + } catch (Throwable t) { + stream.close(); + sink.error(t); + } } @Override public void failed(final Throwable t) { stream.close(); - handleError(t, operationContext, sink); + failOrHandleError(t, keyDecryptor, operationTimeout, sink); } }); } private void streamRead(final Stream stream, final MongoKeyDecryptor keyDecryptor, - final OperationContext operationContext, final MonoSink sink) { + final OperationContext operationContext, @Nullable final Timeout operationTimeout, + final MonoSink sink) { int bytesNeeded = keyDecryptor.bytesNeeded(); - if (bytesNeeded > 0) { - AsynchronousChannelStream asyncStream = (AsynchronousChannelStream) stream; - ByteBuf buffer = asyncStream.getBuffer(bytesNeeded); - long readTimeoutMS = operationContext.getTimeoutContext().getReadTimeoutMS(); - asyncStream.getChannel().read(buffer.asNIO(), readTimeoutMS, MILLISECONDS, null, - new CompletionHandler() { - - @Override - public void completed(final Integer integer, final Void aVoid) { - if (integer == -1) { - sink.error(new MongoException( - "Unexpected end of stream from KMS provider " + keyDecryptor.getKmsProvider())); - return; - } - buffer.flip(); - try { - keyDecryptor.feed(buffer.asNIO()); - buffer.release(); - streamRead(stream, keyDecryptor, operationContext, sink); - } catch (Throwable t) { - sink.error(t); - } - } - - @Override - public void failed(final Throwable t, final Void aVoid) { - buffer.release(); - stream.close(); - handleError(t, operationContext, sink); - } - }); - } else { + if (bytesNeeded <= 0) { stream.close(); sink.success(); + return; + } + AsynchronousChannelStream asyncStream = (AsynchronousChannelStream) stream; + ByteBuf buffer = asyncStream.getBuffer(bytesNeeded); + CompletionHandler readHandler = new CompletionHandler() { + + @Override + public void completed(final Integer integer, final Void aVoid) { + try { + if (integer == -1) { + buffer.release(); + stream.close(); + // Treat an unexpected end of stream (the KMS server closed the connection) as a retryable + // transient network error: hand it to failOrHandleError so the context is retried if budget allows. + MongoException eof = new MongoException("Unexpected end of stream from KMS provider " + + keyDecryptor.getKmsProvider()); + failOrHandleError(eof, keyDecryptor, operationTimeout, sink); + return; + } + buffer.flip(); + boolean shouldRetry; + try { + shouldRetry = keyDecryptor.feedWithRetry(buffer.asNIO()); + } finally { + buffer.release(); + } + if (shouldRetry) { + // libmongocrypt marked the context for retry; complete this attempt and let the state machine re-present it + stream.close(); + sink.success(); + } else { + streamRead(stream, keyDecryptor, operationContext, operationTimeout, sink); + } + } catch (Throwable t) { + stream.close(); + sink.error(t); + } + } + + @Override + public void failed(final Throwable t, final Void aVoid) { + buffer.release(); + stream.close(); + failOrHandleError(t, keyDecryptor, operationTimeout, sink); + } + }; + try { + long readTimeoutMS = operationContext.getTimeoutContext().getReadTimeoutMS(); + asyncStream.getChannel().read(buffer.asNIO(), readTimeoutMS, MILLISECONDS, null, readHandler); + } catch (RuntimeException | Error e) { + // the handler was not invoked, so the buffer must be released here + buffer.release(); + throw e; } } - private static void handleError(final Throwable t, final OperationContext operationContext, final MonoSink sink) { - if (isTimeoutException(t) && operationContext.getTimeoutContext().hasTimeoutMS()) { - sink.error(TimeoutContext.createMongoTimeoutException(TIMEOUT_ERROR_MESSAGE, t)); + private static void failOrHandleError(final Throwable t, final MongoKeyDecryptor keyDecryptor, + @Nullable final Timeout operationTimeout, final MonoSink sink) { + if (isTimeoutException(t) && hasExpired(operationTimeout)) { + sink.error(TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE, t)); + return; + } + if (keyDecryptor.fail()) { + LOGGER.debug("Retrying KMS request after transient error", t); + sink.success(); } else { sink.error(t); } } + private static boolean hasExpired(@Nullable final Timeout operationTimeout) { + return operationTimeout != null && operationTimeout.call(MILLISECONDS, + () -> false, + remainingMillis -> false, + () -> true); + } + private OperationContext createOperationContext(@Nullable final Timeout operationTimeout, final SocketSettings socketSettings) { TimeoutSettings timeoutSettings; if (operationTimeout == null) { @@ -179,7 +246,7 @@ private OperationContext createOperationContext(@Nullable final Timeout operatio }, (ms) -> createTimeoutSettings(socketSettings, ms), () -> { - throw new MongoOperationTimeoutException(TIMEOUT_ERROR_MESSAGE); + throw new MongoOperationTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE); }); } return OperationContext.simpleOperationContext(new TimeoutContext(timeoutSettings)); @@ -197,7 +264,13 @@ private static TimeoutSettings createTimeoutSettings(final SocketSettings socket } private Throwable unWrapException(final Throwable t) { - return t instanceof MongoSocketException ? t.getCause() : t; + // Unwrap the IOException the async stream layer wraps in a MongoSocketException, to match the sync path. + // Socket timeout subclasses are meaningful MongoClientExceptions, so preserve them rather than unwrapping. + if (t instanceof MongoSocketReadTimeoutException || t instanceof MongoSocketWriteTimeoutException) { + return t; + } + Throwable cause = t.getCause(); + return t instanceof MongoSocketException && cause != null ? cause : t; } private static boolean isTimeoutException(final Throwable t) { diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionKmsRetryProseTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionKmsRetryProseTest.java new file mode 100644 index 0000000000..1dd4f0f833 --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionKmsRetryProseTest.java @@ -0,0 +1,30 @@ +/* + * 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.client.AbstractClientSideEncryptionKmsRetryProseTest; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; +import com.mongodb.reactivestreams.client.vault.ClientEncryptions; + +public class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest { + @Override + public ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) { + return new SyncClientEncryption(ClientEncryptions.create(settings)); + } +} diff --git a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementServiceKmsRetryTest.java b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementServiceKmsRetryTest.java new file mode 100644 index 0000000000..c35cba2a79 --- /dev/null +++ b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementServiceKmsRetryTest.java @@ -0,0 +1,130 @@ +/* + * 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.internal.crypt; + +import com.mongodb.MongoOperationTimeoutException; +import com.mongodb.MongoSocketReadTimeoutException; +import com.mongodb.ServerAddress; +import com.mongodb.connection.AsyncCompletionHandler; +import com.mongodb.connection.SocketSettings; +import com.mongodb.connection.SslSettings; +import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.connection.Stream; +import com.mongodb.internal.connection.StreamFactory; +import com.mongodb.internal.connection.TlsChannelStreamFactoryFactory; +import com.mongodb.internal.crypt.capi.MongoKeyDecryptor; +import com.mongodb.internal.time.Timeout; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Verifies that a socket read timeout in the reactive KeyManagementService is classified as a + * retryable network error when the CSOT deadline has not yet expired, and as a + * {@link MongoOperationTimeoutException} only when the deadline has actually passed. + */ +class KeyManagementServiceKmsRetryTest { + + private MongoKeyDecryptor keyDecryptor; + private KeyManagementService service; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUp() { + keyDecryptor = mock(MongoKeyDecryptor.class); + when(keyDecryptor.getKmsProvider()).thenReturn("aws"); + when(keyDecryptor.getHostName()).thenReturn("kms.example.com"); + when(keyDecryptor.getMessage()).thenReturn(ByteBuffer.allocate(0)); + when(keyDecryptor.sleepMicroseconds()).thenReturn(0L); + when(keyDecryptor.bytesNeeded()).thenReturn(1); + + Stream stream = mock(Stream.class); + StreamFactory streamFactory = mock(StreamFactory.class); + TlsChannelStreamFactoryFactory factoryFactory = mock(TlsChannelStreamFactoryFactory.class); + + when(streamFactory.create(any(ServerAddress.class))).thenReturn(stream); + when(factoryFactory.create(any(SocketSettings.class), any(SslSettings.class))).thenReturn(streamFactory); + + doAnswer(inv -> { + AsyncCompletionHandler handler = inv.getArgument(1); + handler.failed(new MongoSocketReadTimeoutException( + "read timed out", new ServerAddress("kms.example.com"), new Exception())); + return null; + }).when(stream).openAsync(any(OperationContext.class), any()); + + service = new KeyManagementService(emptyMap(), 1000, factoryFactory); + } + + @Test + void shouldTreatSocketReadTimeoutAsRetryableNetworkErrorWhenBudgetRemains() { + // retries exhausted, so the network error itself surfaces + when(keyDecryptor.fail()).thenReturn(false); + Timeout operationTimeout = Timeout.expiresIn(5, TimeUnit.SECONDS, Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED); + + StepVerifier.create(service.decryptKey(keyDecryptor, operationTimeout)) + .expectErrorSatisfies(error -> { + // the connect/handshake socket timeout is a fixed KMS timeout, not the operation deadline, + // so with budget remaining it must consume retry budget rather than report a CSOT timeout + assertFalse(error instanceof MongoOperationTimeoutException, + "a socket timeout with operation budget remaining must not be classified as an operation timeout"); + // the meaningful MongoSocketReadTimeoutException must be preserved, not stripped to its raw cause + assertTrue(error instanceof MongoSocketReadTimeoutException, + "the socket read timeout type must be preserved"); + }) + .verify(); + + verify(keyDecryptor).fail(); + } + + @Test + void shouldThrowOperationTimeoutWhenDeadlineHasExpired() { + Timeout operationTimeout = Timeout.expiresIn(0, TimeUnit.SECONDS, Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED); + + StepVerifier.create(service.decryptKey(keyDecryptor, operationTimeout)) + .expectError(MongoOperationTimeoutException.class) + .verify(); + + verify(keyDecryptor, never()).fail(); + } + + @Test + void shouldThrowOperationTimeoutWhenRetryBackoffExceedsRemainingBudget() { + // libmongocrypt asks for a retry backoff that cannot fit within the remaining CSOT budget, so + // checkKmsRetryBackoff must fail fast with an operation timeout before any further KMS attempt + when(keyDecryptor.sleepMicroseconds()).thenReturn(TimeUnit.SECONDS.toMicros(10)); + Timeout operationTimeout = Timeout.expiresIn(1, TimeUnit.SECONDS, Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED); + + StepVerifier.create(service.decryptKey(keyDecryptor, operationTimeout)) + .expectError(MongoOperationTimeoutException.class) + .verify(); + + verify(keyDecryptor, never()).fail(); + } +} diff --git a/driver-sync/src/main/com/mongodb/client/internal/Crypt.java b/driver-sync/src/main/com/mongodb/client/internal/Crypt.java index 67fac13770..11049ed220 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/Crypt.java +++ b/driver-sync/src/main/com/mongodb/client/internal/Crypt.java @@ -24,12 +24,15 @@ import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RewrapManyDataKeyOptions; import com.mongodb.crypt.capi.MongoCryptException; +import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.capi.MongoCryptHelper; import com.mongodb.internal.crypt.capi.MongoCrypt; import com.mongodb.internal.crypt.capi.MongoCryptContext; import com.mongodb.internal.crypt.capi.MongoDataKeyOptions; import com.mongodb.internal.crypt.capi.MongoKeyDecryptor; import com.mongodb.internal.crypt.capi.MongoRewrapManyDataKeyOptions; +import com.mongodb.internal.diagnostics.logging.Logger; +import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.time.Timeout; import com.mongodb.lang.Nullable; import org.bson.BsonBinary; @@ -43,10 +46,13 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.notNull; +import static com.mongodb.internal.capi.MongoCryptHelper.KMS_TIMEOUT_ERROR_MESSAGE; +import static com.mongodb.internal.capi.MongoCryptHelper.checkKmsRetryBackoff; import static com.mongodb.internal.client.vault.EncryptOptionsHelper.asMongoExplicitEncryptOptions; import static com.mongodb.internal.crypt.capi.MongoCryptContext.State; import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException; @@ -56,6 +62,7 @@ */ public class Crypt implements Closeable { + private static final Logger LOGGER = Loggers.getLogger("client"); private static final RawBsonDocument EMPTY_RAW_BSON_DOCUMENT = RawBsonDocument.parse("{}"); private final MongoCrypt mongoCrypt; private final Map> kmsProviders; @@ -352,6 +359,7 @@ private void decryptKeys(final MongoCryptContext cryptContext, @Nullable final T MongoKeyDecryptor keyDecryptor = cryptContext.nextKeyDecryptor(); while (keyDecryptor != null) { decryptKey(keyDecryptor, operationTimeout); + // a retry-marked context stays queued and is re-presented by nextKeyDecryptor() keyDecryptor = cryptContext.nextKeyDecryptor(); } cryptContext.completeKeyDecryptors(); @@ -361,20 +369,70 @@ private void decryptKeys(final MongoCryptContext cryptContext, @Nullable final T } } - private void decryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Timeout operationTimeout) throws IOException { - try (InputStream inputStream = keyManagementService.stream(keyDecryptor.getKmsProvider(), keyDecryptor.getHostName(), - keyDecryptor.getMessage(), operationTimeout)) { - int bytesNeeded = keyDecryptor.bytesNeeded(); + private void decryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Timeout operationTimeout) + throws IOException, InterruptedException { + long sleepMicros = keyDecryptor.sleepMicroseconds(); + if (sleepMicros > 0) { + checkKmsRetryBackoff(operationTimeout, sleepMicros); + // An interrupt during backoff propagates out without calling keyDecryptor.fail(): + // an interrupt is not a KMS error and must not consume retry budget. + TimeUnit.MICROSECONDS.sleep(sleepMicros); + } + try { + attemptDecryptKey(keyDecryptor, operationTimeout); + } catch (MongoCryptException e) { + // A libmongocrypt feed failure is not a transient network error; propagate it instead of + // consuming retry budget (as the async path does). + throw e; + } catch (IOException | MongoException e) { + // Under CSOT a read timeout means the deadline was reached (the socket timeout is clamped to the + // remaining operation time); other socket errors or a premature EOF with budget left are retryable. + Timeout.onExistsAndExpired(operationTimeout, () -> { + throw TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE, e); + }); + if (!keyDecryptor.fail()) { + throw e; + } + LOGGER.debug("Retrying KMS request after transient error", e); + } + } + private void attemptDecryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Timeout operationTimeout) + throws IOException { + Timeout.onExistsAndExpired(operationTimeout, () -> { + throw TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE); + }); + InputStream inputStream = keyManagementService.stream(keyDecryptor.getKmsProvider(), keyDecryptor.getHostName(), + keyDecryptor.getMessage(), operationTimeout); + Throwable primary = null; + try { + int bytesNeeded = keyDecryptor.bytesNeeded(); while (bytesNeeded > 0) { byte[] bytes = new byte[bytesNeeded]; int bytesRead = inputStream.read(bytes, 0, bytes.length); if (bytesRead == -1) { + // Match the async path's exception type so both retry an unexpected KMS end of stream identically. throw new MongoException("Unexpected end of stream from KMS provider " + keyDecryptor.getKmsProvider()); } - keyDecryptor.feed(ByteBuffer.wrap(bytes, 0, bytesRead)); + if (keyDecryptor.feedWithRetry(ByteBuffer.wrap(bytes, 0, bytesRead))) { + return; + } bytesNeeded = keyDecryptor.bytesNeeded(); } + } catch (Throwable t) { + primary = t; + throw t; + } finally { + try { + inputStream.close(); + } catch (IOException closeException) { + if (primary != null) { + primary.addSuppressed(closeException); + } else { + // close failure after the data is fully received is non-actionable + LOGGER.debug("Ignoring close() failure after completed KMS attempt", closeException); + } + } } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java new file mode 100644 index 0000000000..2b3360fcc3 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java @@ -0,0 +1,327 @@ +/* + * 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.MongoClientException; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.lang.NonNull; +import com.mongodb.lang.Nullable; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonString; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.mongodb.ClusterFixture.getEnv; +import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * See + * 24. KMS Retry Tests. + * + *

Requires the {@code org.mongodb.test.kms.retry.ca.path} system property pointing to the CA cert for the + * failpoint server. + */ +public abstract class AbstractClientSideEncryptionKmsRetryProseTest { + + private static final String FAILPOINT_SERVER_ADDRESS = "127.0.0.1:9003"; + private static final String FAILPOINT_URL_BASE = "https://" + FAILPOINT_SERVER_ADDRESS; + + @Nullable + private static volatile SSLContext failpointSslContext; + + @NonNull + protected abstract ClientEncryption getClientEncryption(ClientEncryptionSettings settings); + + @BeforeEach + public void setUp() { + assumeTrue(System.getProperty("org.mongodb.test.kms.retry.ca.path") != null, + "org.mongodb.test.kms.retry.ca.path system property is not set"); + assumeTrue(hasEncryptionTestsEnabled()); + assumeTrue(serverVersionAtLeast(4, 2)); + resetFailpoints(); + getMongoClient().getDatabase("keyvault").getCollection("datakeys").drop(); + } + + @AfterEach + public void tearDown() { + // runs even when setUp's assumptions aborted the test, so re-check the environment + if (System.getProperty("org.mongodb.test.kms.retry.ca.path") == null || !hasEncryptionTestsEnabled()) { + return; + } + // leave the shared failpoint server clean for whoever runs next; a test that aborts + // mid-retry (e.g. on operation timeout) leaves unconsumed failure counts armed + resetFailpoints(); + } + + /** + * Case 1: createDataKey and encrypt with TCP retry. + */ + @ParameterizedTest(name = "Case 1: TCP retry with {0}") + @ValueSource(strings = {"aws", "azure", "gcp"}) + public void testCreateDataKeyAndEncryptWithTcpRetry(final String provider) { + try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) { + setFailpoint("network", 1); + BsonBinary keyId = assertDoesNotThrow( + () -> clientEncryption.createDataKey(provider, getDataKeyOptions(provider))); + + setFailpoint("network", 1); + assertDoesNotThrow( + () -> clientEncryption.encrypt(new BsonInt32(123), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(keyId))); + } + } + + /** + * Case 2: createDataKey and encrypt with HTTP retry. + */ + @ParameterizedTest(name = "Case 2: HTTP retry with {0}") + @ValueSource(strings = {"aws", "azure", "gcp"}) + public void testCreateDataKeyAndEncryptWithHttpRetry(final String provider) { + try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) { + setFailpoint("http", 1); + BsonBinary keyId = assertDoesNotThrow( + () -> clientEncryption.createDataKey(provider, getDataKeyOptions(provider))); + + setFailpoint("http", 1); + assertDoesNotThrow( + () -> clientEncryption.encrypt(new BsonInt32(123), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(keyId))); + } + } + + /** + * Case 3: createDataKey fails after too many retries. + */ + @ParameterizedTest(name = "Case 3: Exhausted retries with {0}") + @ValueSource(strings = {"aws", "azure", "gcp"}) + public void testCreateDataKeyFailsAfterTooManyRetries(final String provider) { + try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) { + setFailpoint("network", 4); + assertThrows(MongoClientException.class, + () -> clientEncryption.createDataKey(provider, getDataKeyOptions(provider))); + } + } + + /** + * Prose test: createDataKey fails when the operation timeout expires mid-retry. Configures a 100ms + * operation timeout and a failpoint that triggers repeated network errors, so the cumulative retry + * backoff normally pushes the operation past its deadline. + */ + @Test + public void testCreateDataKeyTimesOutDuringRetry() { + try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest(100L)) { + setFailpoint("network", 4); + // The 100ms deadline races libmongocrypt's jittered retry backoff: usually the deadline + // expires mid-retry and MongoOperationTimeoutException (a MongoClientException subclass) is + // thrown, but if the random backoffs are small enough the retry budget is exhausted first and + // the network error surfaces as a plain MongoClientException. Both are correct CSOT outcomes, + // so only the common supertype is asserted to keep the test deterministic. + assertThrows(MongoClientException.class, + () -> clientEncryption.createDataKey("aws", getDataKeyOptions("aws"))); + } + } + + private ClientEncryption createClientEncryptionForRetryTest() { + return createClientEncryptionForRetryTest(null); + } + + private ClientEncryption createClientEncryptionForRetryTest(@Nullable final Long timeoutMS) { + Map> kmsProviders = getKmsProvidersForRetryTest(); + SSLContext failpointSslContext = createFailpointSslContext(); + Map kmsProviderSslContextMap = new HashMap<>(); + kmsProviderSslContextMap.put("aws", failpointSslContext); + kmsProviderSslContextMap.put("azure", failpointSslContext); + kmsProviderSslContextMap.put("gcp", failpointSslContext); + + ClientEncryptionSettings.Builder builder = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace("keyvault.datakeys") + .kmsProviders(kmsProviders) + .kmsProviderSslContextMap(kmsProviderSslContextMap); + if (timeoutMS != null) { + builder.timeout(timeoutMS, TimeUnit.MILLISECONDS); + } + + return getClientEncryption(builder.build()); + } + + private static Map> getKmsProvidersForRetryTest() { + Map awsCredentials = new HashMap<>(); + awsCredentials.put("accessKeyId", getEnv("AWS_ACCESS_KEY_ID")); + awsCredentials.put("secretAccessKey", getEnv("AWS_SECRET_ACCESS_KEY")); + + Map azureCredentials = new HashMap<>(); + azureCredentials.put("tenantId", getEnv("AZURE_TENANT_ID")); + azureCredentials.put("clientId", getEnv("AZURE_CLIENT_ID")); + azureCredentials.put("clientSecret", getEnv("AZURE_CLIENT_SECRET")); + azureCredentials.put("identityPlatformEndpoint", FAILPOINT_SERVER_ADDRESS); + + Map gcpCredentials = new HashMap<>(); + gcpCredentials.put("email", getEnv("GCP_EMAIL")); + gcpCredentials.put("privateKey", getEnv("GCP_PRIVATE_KEY")); + gcpCredentials.put("endpoint", FAILPOINT_SERVER_ADDRESS); + + Map> kmsProviders = new HashMap<>(); + kmsProviders.put("aws", awsCredentials); + kmsProviders.put("azure", azureCredentials); + kmsProviders.put("gcp", gcpCredentials); + return kmsProviders; + } + + private static DataKeyOptions getDataKeyOptions(final String provider) { + BsonDocument masterKey; + switch (provider) { + case "aws": + masterKey = new BsonDocument() + .append("region", new BsonString("foo")) + .append("key", new BsonString("bar")) + .append("endpoint", new BsonString(FAILPOINT_SERVER_ADDRESS)); + break; + case "azure": + masterKey = new BsonDocument() + .append("keyVaultEndpoint", new BsonString(FAILPOINT_SERVER_ADDRESS)) + .append("keyName", new BsonString("foo")); + break; + case "gcp": + masterKey = new BsonDocument() + .append("projectId", new BsonString("foo")) + .append("location", new BsonString("bar")) + .append("keyRing", new BsonString("baz")) + .append("keyName", new BsonString("qux")) + .append("endpoint", new BsonString(FAILPOINT_SERVER_ADDRESS)); + break; + default: + throw new UnsupportedOperationException("Unsupported KMS provider: " + provider); + } + return new DataKeyOptions().masterKey(masterKey); + } + + private static void setFailpoint(final String failpointType, final int count) { + postToFailpointServer("/set_failpoint/" + failpointType, "{\"count\": " + count + "}"); + } + + /** + * Clears any failpoint counts left armed by an earlier test or suite - the failpoint server's + * state is global and outlives the test JVMs. In particular, the operation-timeout test arms + * more network failures than it consumes before its deadline expires. + */ + private static void resetFailpoints() { + postToFailpointServer("/reset", ""); + } + + private static void postToFailpointServer(final String path, final String body) { + try { + SSLContext sslContext = createFailpointSslContext(); + URL url = new URL(FAILPOINT_URL_BASE + path); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + try { + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + connection.setSSLSocketFactory(sslContext.getSocketFactory()); + // test-only: self-signed cert, hostname verification intentionally disabled + connection.setHostnameVerifier((hostname, session) -> true); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + // The failpoint server is single-threaded with HTTP/1.1 keep-alive: a kept-alive + // connection blocks it from accepting the driver's KMS connections, and over TLS + // the JDK keep-alive cache holds the socket open for several seconds even after + // disconnect(). Asking the server to close the connection avoids stalling the + // KMS requests that follow. + connection.setRequestProperty("Connection", "close"); + + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + connection.setRequestProperty("Content-Length", String.valueOf(bodyBytes.length)); + + try (OutputStream os = connection.getOutputStream()) { + os.write(bodyBytes); + } + + int responseCode = connection.getResponseCode(); + assertEquals(200, responseCode, + "Failpoint server request to " + path + " failed, HTTP status: " + responseCode); + try (InputStream is = connection.getInputStream()) { + while (is.read() != -1) { + // drain the response so the exchange completes cleanly + } + } + } finally { + connection.disconnect(); + } + } catch (Exception e) { + throw new RuntimeException("Failpoint server request to " + path + " failed", e); + } + } + + private static synchronized SSLContext createFailpointSslContext() { + if (failpointSslContext != null) { + return failpointSslContext; + } + try { + String caCertPath = System.getProperty("org.mongodb.test.kms.retry.ca.path"); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate caCert; + try (FileInputStream fis = new FileInputStream(caCertPath)) { + caCert = (X509Certificate) cf.generateCertificate(fis); + } + + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + trustStore.setCertificateEntry("ca", caCert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + failpointSslContext = sslContext; + return sslContext; + } catch (Exception e) { + throw new RuntimeException("Failed to create SSL context for failpoint server", e); + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java new file mode 100644 index 0000000000..7b51bf915e --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java @@ -0,0 +1,28 @@ +/* + * 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.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +public class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest { + @Override + public ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) { + return ClientEncryptions.create(settings); + } +} diff --git a/driver-sync/src/test/unit/com/mongodb/client/internal/CryptKmsRetryTest.java b/driver-sync/src/test/unit/com/mongodb/client/internal/CryptKmsRetryTest.java new file mode 100644 index 0000000000..b40b9887d9 --- /dev/null +++ b/driver-sync/src/test/unit/com/mongodb/client/internal/CryptKmsRetryTest.java @@ -0,0 +1,100 @@ +/* + * 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.internal; + +import com.mongodb.MongoClientException; +import com.mongodb.MongoOperationTimeoutException; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.internal.crypt.capi.MongoCrypt; +import com.mongodb.internal.crypt.capi.MongoCryptContext; +import com.mongodb.internal.crypt.capi.MongoKeyDecryptor; +import com.mongodb.internal.time.Timeout; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Verifies how KMS network failures are classified under CSOT: a {@code SocketTimeoutException} + * from the fixed-timeout connect/handshake phase with operation budget remaining is a transient, + * retryable network error, whereas an expired operation deadline must surface as + * {@link MongoOperationTimeoutException}. + */ +class CryptKmsRetryTest { + + private MongoCryptContext cryptContext; + private MongoKeyDecryptor keyDecryptor; + private KeyManagementService keyManagementService; + private Crypt crypt; + + @BeforeEach + void setUp() throws Exception { + MongoCrypt mongoCrypt = mock(MongoCrypt.class); + cryptContext = mock(MongoCryptContext.class); + keyDecryptor = mock(MongoKeyDecryptor.class); + keyManagementService = mock(KeyManagementService.class); + + when(mongoCrypt.createDataKeyContext(anyString(), any())).thenReturn(cryptContext); + when(cryptContext.getState()).thenReturn(MongoCryptContext.State.NEED_KMS); + when(cryptContext.nextKeyDecryptor()).thenReturn(keyDecryptor); + when(keyDecryptor.getKmsProvider()).thenReturn("aws"); + when(keyDecryptor.getHostName()).thenReturn("kms.example.com"); + when(keyDecryptor.getMessage()).thenReturn(ByteBuffer.allocate(0)); + when(keyDecryptor.sleepMicroseconds()).thenReturn(0L); + when(keyManagementService.stream(anyString(), anyString(), any(), any())) + .thenThrow(new SocketTimeoutException("connect timed out")); + + crypt = new Crypt(mongoCrypt, mock(KeyRetriever.class), keyManagementService, emptyMap(), emptyMap()); + } + + @Test + void shouldTreatSocketTimeoutAsRetryableNetworkErrorWhenBudgetRemains() { + // retries exhausted, so the network error itself surfaces + when(keyDecryptor.fail()).thenReturn(false); + Timeout operationTimeout = Timeout.expiresIn(30, TimeUnit.SECONDS, Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED); + + MongoClientException e = assertThrows(MongoClientException.class, + () -> crypt.createDataKey("aws", new DataKeyOptions(), operationTimeout)); + + // the connect/handshake socket timeout is a fixed KMS timeout, not the operation deadline, + // so with budget remaining it must consume retry budget rather than report a CSOT timeout + assertFalse(e instanceof MongoOperationTimeoutException, + "a socket timeout with operation budget remaining must not be classified as an operation timeout"); + verify(keyDecryptor).fail(); + } + + @Test + void shouldThrowOperationTimeoutWhenDeadlineHasExpired() { + Timeout operationTimeout = Timeout.expiresIn(0, TimeUnit.SECONDS, Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED); + + assertThrows(MongoOperationTimeoutException.class, + () -> crypt.createDataKey("aws", new DataKeyOptions(), operationTimeout)); + verify(keyDecryptor, never()).fail(); + } +} diff --git a/mongodb-crypt/AGENTS.md b/mongodb-crypt/AGENTS.md index bf9737febf..7c5793b211 100644 --- a/mongodb-crypt/AGENTS.md +++ b/mongodb-crypt/AGENTS.md @@ -15,6 +15,35 @@ Client-side field-level encryption (CSFLE) support via JNA bindings to libmongoc review** - `com.mongodb.internal.crypt.capi` — Internal encryption state management +## CAPI.java — JNA Binding Declarations + +`CAPI.java` declares the JNA native method signatures for libmongocrypt. Javadoc on each +declaration that has a counterpart in `mongocrypt.h` must match the documentation in the header +of the libmongocrypt version the driver binds to (`downloadRevision` in +`mongodb-crypt/build.gradle.kts`), e.g. +[mongocrypt.h at 1.18.1](https://github.com/mongodb/libmongocrypt/blob/1.18.1/src/mongocrypt.h). + +For declarations with a `mongocrypt.h` counterpart (native methods, typedef wrapper classes): +- Copy the doc comment from the header verbatim — do not add, remove, or reword sentences, + even when extra context (e.g. cross-references to related setopt functions) seems helpful +- Convert Doxygen markup to Javadoc, keeping the result valid for the `javadoc` tool + (escape raw `<`/`>` as `{@code ...}` or HTML entities; paragraphs use `

`): + - `@ref type_name` → `{@link type_name}` (for types) or `{@link #function_name}` + (for functions in the same class) + - `@p param_name` → `{@code param_name}` + - `@param[in] name` / `@param[out] name` → `@param name` + - `@returns` → `@return` + - `@pre condition` → a `

Requires that condition.` paragraph (keeping the condition text verbatim) + - `@code{.c}` example blocks may be omitted +- Preserve the header's own quirks (e.g. a `@ref` pointing at an unexpected status function) — + fidelity to the header beats local consistency + +Declarations with no `mongocrypt.h` counterpart (e.g. the `cstring` helper, the legacy +`mongocrypt_opts_t` class) get Java-authored Javadoc; keep it accurate and note when a type is +retained only for backwards compatibility. + +Verify the result still generates cleanly with `./gradlew :mongodb-crypt:javadoc`. + ## Build & Test ```bash 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 41cc8ced31..f600f91e58 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 @@ -18,10 +18,12 @@ package com.mongodb.internal.crypt.capi; import com.sun.jna.Callback; +import com.sun.jna.FromNativeContext; import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.PointerType; +import com.sun.jna.ptr.ByteByReference; import com.sun.jna.ptr.PointerByReference; //CHECKSTYLE:OFF @@ -43,8 +45,22 @@ public cstring(String string) { setPointer(m); } + // By default JNA returns a null cstring reference when a native function returns a NULL + // pointer (e.g. mongocrypt_status_message for a status with no message). Override fromNative + // to return a cstring with a null pointer instead, so callers never receive a null reference + // and toString below can normalize it to an empty string. + @Override + public Object fromNative(final Object nativeValue, final FromNativeContext context) { + cstring result = new cstring(); + if (nativeValue != null) { + result.setPointer((Pointer) nativeValue); + } + return result; + } + public String toString() { - return getPointer().getString(0); + Pointer pointer = getPointer(); + return pointer == null ? "" : pointer.getString(0); } } @@ -52,16 +68,18 @@ public String toString() { /** * Indicates success or contains error information. *

- * Functions like @ref mongocrypt_ctx_encrypt_init follow a pattern to expose a + * Functions like {@link #mongocrypt_ctx_encrypt_init} follow a pattern to expose a * status. A boolean is returned. True indicates success, and false indicates * failure. On failure a status on the handle is set, and is accessible with a - * corresponding status function. E.g. @ref mongocrypt_ctx_status. + * corresponding (handle)_status function. E.g. {@link #mongocrypt_ctx_status}. */ public static class mongocrypt_status_t extends PointerType { } /** - * Contains all options passed on initialization of a @ref mongocrypt_ctx_t. + * Contains all options passed on initialization of a {@link mongocrypt_t}. + * + *

Not present in mongocrypt.h: retained for backwards compatibility of this binding class. */ public static class mongocrypt_opts_t extends PointerType { } @@ -69,8 +87,19 @@ public static class mongocrypt_opts_t extends PointerType { /** * A non-owning view of a byte buffer. *

- * Functions returning a mongocrypt_binary_t* expect it to be destroyed with - * mongocrypt_binary_destroy. + * When constructing a {@link mongocrypt_binary_t} it is the responsibility of the + * caller to maintain the lifetime of the viewed data. However, all public + * functions that take a {@link mongocrypt_binary_t} as an argument will make a copy of + * the viewed data. + *

+ * Functions with a {@link mongocrypt_binary_t}* out guarantee the lifetime of the + * viewed data to live as long as the parent object. For example, + * {@link #mongocrypt_ctx_mongo_op} guarantees that the viewed data of + * {@link mongocrypt_binary_t} is valid until the parent ctx is destroyed with + * {@link #mongocrypt_ctx_destroy}. + *

+ * The {@code mongocrypt_binary_t} struct definition is public. + * Consumers may rely on the struct layout. */ public static class mongocrypt_binary_t extends PointerType { // The `mongocrypt_binary_t` struct layout is part of libmongocrypt's ABI: @@ -83,9 +112,11 @@ public static class mongocrypt_binary_t extends PointerType { public mongocrypt_binary_t() { super(); } + public Pointer data() { return this.getPointer().getPointer(0); } + public int len() { int len = this.getPointer().getInt(Native.POINTER_SIZE); // mongocrypt_binary_t represents length as an unsigned `uint32_t`. @@ -107,7 +138,7 @@ public int len() { * encryption, decryption, registering log callbacks, etc. *

* Functions on a mongocrypt_t are thread safe, though functions on derived - * handle (e.g. mongocrypt_encryptor_t) are not and must be owned by a single + * handles (e.g. mongocrypt_ctx_t) are not and must be owned by a single * thread. See each handle's documentation for thread-safety considerations. *

* Multiple mongocrypt_t handles may be created. @@ -152,7 +183,7 @@ public static class mongocrypt_kms_ctx_t extends PointerType { * Create a new non-owning view of a buffer (data + length). * * @param data A pointer to an array of bytes. This is not copied. data must outlive the binary object. - * @param len The length of the @p data byte array. + * @param len The length of the {@code data} byte array. * @return A new mongocrypt_binary_t. */ public static native mongocrypt_binary_t @@ -160,31 +191,29 @@ public static class mongocrypt_kms_ctx_t extends PointerType { /** - * Get a pointer to the referenced data. + * Get a pointer to the viewed data. * - * @param binary The @ref mongocrypt_binary_t. - * @return A pointer to the referenced data. + * @param binary The {@link mongocrypt_binary_t}. + * @return A pointer to the viewed data. */ public static native Pointer mongocrypt_binary_data(mongocrypt_binary_t binary); /** - * Get the length of the referenced data. + * Get the length of the viewed data. * - * @param binary The @ref mongocrypt_binary_t. - * @return The length of the referenced data. + * @param binary The {@link mongocrypt_binary_t}. + * @return The length of the viewed data. */ public static native int mongocrypt_binary_len(mongocrypt_binary_t binary); /** - * Free the @ref mongocrypt_binary_t. + * Free the {@link mongocrypt_binary_t}. *

- * This does not free the referenced data. Refer to individual function - * documentation to determine the lifetime guarantees of the underlying - * data. + * This does not free the viewed data. * * @param binary The mongocrypt_binary_t destroy. */ @@ -200,8 +229,8 @@ public static class mongocrypt_kms_ctx_t extends PointerType { * Create a new status object. *

* Use a new status object to retrieve the status from a handle by passing - * this as an out-parameter to functions like @ref mongocrypt_ctx_status. - * When done, destroy it with @ref mongocrypt_status_destroy. + * this as an out-parameter to functions like {@link #mongocrypt_ctx_status}. + * When done, destroy it with {@link #mongocrypt_status_destroy}. * * @return A new status object. */ @@ -217,20 +246,23 @@ public static class mongocrypt_kms_ctx_t extends PointerType { * @param type The status type. * @param code The status code. * @param message The message. - * @param message_len The length of @p message. Pass -1 to determine the * string length with strlen (must * be NULL terminated). + * @param message_len Due to historical behavior, pass 1 + the string length + * of {@code message} (which differs from other functions accepting string + * arguments). Alternatively, if message is NULL terminated this may be -1 to + * tell mongocrypt to determine the string's length with strlen. */ public static native void mongocrypt_status_set(mongocrypt_status_t status, - int type, - int code, - cstring message, - int message_len); + int type, + int code, + cstring message, + int message_len); /** * Indicates success or the type of error. * * @param status The status object. - * @return A @ref mongocrypt_status_type_t. + * @return A mongocrypt_status_type_t. */ public static native int @@ -248,11 +280,12 @@ public static class mongocrypt_kms_ctx_t extends PointerType { /** - * Get the error message associated with a status, or an empty string. + * Get the error message associated with a status or NULL. * * @param status The status object. - * @param len an optional length of the returned string. May be NULL. - * @return An error message or an empty string. + * @param len An optional length of the returned string (excluding the + * trailing NULL byte). May be NULL. + * @return A NULL terminated error message or NULL. */ public static native cstring mongocrypt_status_message(mongocrypt_status_t status, Pointer len); @@ -310,24 +343,26 @@ public interface mongocrypt_random_fn extends Callback { } /** - * Allocate a new @ref mongocrypt_t object. + * Allocate a new {@link mongocrypt_t} object. *

- * Initialize with @ref mongocrypt_init. When done, free with @ref - * mongocrypt_destroy. + * Set options using mongocrypt_setopt_* functions, then initialize with {@link #mongocrypt_init}. + * When done with the {@link mongocrypt_t}, free with {@link #mongocrypt_destroy}. * - * @return A new @ref mongocrypt_t object. + * @return A new {@link mongocrypt_t} object. */ public static native mongocrypt_t mongocrypt_new(); /** - * Set a handler to get called on every log message. + * Set a handler on the {@link mongocrypt_t} object to get called on every log message. + * + *

Requires that {@link #mongocrypt_init} has not been called on {@code crypt}. * - * @param crypt The @ref mongocrypt_t object. + * @param crypt The {@link mongocrypt_t} object. * @param log_fn The log callback. * @param log_ctx A context passed as an argument to the log callback every - * invokation. - * @return A boolean indicating success. + * invocation. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_setopt_log_handler(mongocrypt_t crypt, @@ -348,19 +383,18 @@ public interface mongocrypt_random_fn extends Callback { /** * Set a crypto hook for the AES256-CTR operations. * - * @param crypt The @ref mongocrypt_t object. + * @param crypt The {@link mongocrypt_t} object. * @param aes_256_ctr_encrypt The crypto callback function for encrypt - * operation. + * operation. * @param aes_256_ctr_decrypt The crypto callback function for decrypt - * operation. - * @param ctx A context passed as an argument to the crypto callback - * every invocation. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_status + * operation. + * @param ctx A context passed as an argument to the crypto callback + * every invocation. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_status} * */ public static native boolean - mongocrypt_setopt_aes_256_ctr (mongocrypt_t crypt, + mongocrypt_setopt_aes_256_ctr(mongocrypt_t crypt, mongocrypt_crypto_fn aes_256_ctr_encrypt, mongocrypt_crypto_fn aes_256_ctr_decrypt, Pointer ctx); @@ -373,12 +407,11 @@ public interface mongocrypt_random_fn extends Callback { *

Note: this function has the wrong name. It should be: * mongocrypt_setopt_crypto_hook_sign_rsassa_pkcs1_v1_5

* - * @param crypt The @ref mongocrypt_t object. + * @param crypt The {@link mongocrypt_t} object. * @param sign_rsaes_pkcs1_v1_5 The crypto callback function. - * @param sign_ctx A context passed as an argument to the crypto callback - * every invocation. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_status + * @param sign_ctx A context passed as an argument to the crypto callback + * every invocation. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_status} */ public static native boolean mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5( @@ -387,20 +420,24 @@ public interface mongocrypt_random_fn extends Callback { Pointer sign_ctx); /** - * Set a handler to get called on every log message. + * Configure an AWS KMS provider on the {@link mongocrypt_t} object. * - * @param crypt The @ref mongocrypt_t object. - * @param aws_access_key_id The AWS access key ID used to generate KMS - * messages. - * @param aws_access_key_id_len The string length (in bytes) of @p - * * aws_access_key_id. Pass -1 to determine the string length with strlen (must - * * be NULL terminated). - * @param aws_secret_access_key The AWS secret access key used to generate - * KMS messages. - * @param aws_secret_access_key_len The string length (in bytes) of @p - * aws_secret_access_key. Pass -1 to determine the string length with strlen - * (must be NULL terminated). - * @return A boolean indicating success. + *

This has been superseded by the more flexible: + * {@link #mongocrypt_setopt_kms_providers}. + * + *

Requires that {@link #mongocrypt_init} has not been called on {@code crypt}. + * + * @param crypt The {@link mongocrypt_t} object. + * @param aws_access_key_id The AWS access key ID used to generate KMS + * messages. + * @param aws_access_key_id_len The string length (in bytes) of {@code aws_access_key_id}. + * Pass -1 to determine the string length with strlen (must be NULL terminated). + * @param aws_secret_access_key The AWS secret access key used to generate + * KMS messages. + * @param aws_secret_access_key_len The string length (in bytes) of {@code aws_secret_access_key}. + * Pass -1 to determine the string length + * with strlen (must be NULL terminated). + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_setopt_kms_provider_aws(mongocrypt_t crypt, @@ -410,11 +447,18 @@ public interface mongocrypt_random_fn extends Callback { int aws_secret_access_key_len); /** - * Configure a local KMS provider on the @ref mongocrypt_t object. + * Configure a local KMS provider on the {@link mongocrypt_t} object. * - * @param crypt The @ref mongocrypt_t object. - * @param key A 64 byte master key used to encrypt and decrypt key vault keys. - * @return A boolean indicating success. + *

This has been superseded by the more flexible: + * {@link #mongocrypt_setopt_kms_providers}. + * + *

Requires that {@link #mongocrypt_init} has not been called on {@code crypt}. + * + * @param crypt The {@link mongocrypt_t} object. + * @param key A 96 byte master key used to encrypt and decrypt key vault + * keys. The viewed data is copied. It is valid to destroy {@code key} with + * {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_setopt_kms_provider_local(mongocrypt_t crypt, @@ -423,10 +467,13 @@ public interface mongocrypt_random_fn extends Callback { /** * Configure KMS providers with a BSON document. * - * @param crypt The @ref mongocrypt_t object. - * @param kms_providers A BSON document mapping the KMS provider names to credentials. - * @return A boolean indicating success. If false, an error status is set. - * @since 1.1 + *

Requires that {@link #mongocrypt_init} has not been called on {@code crypt}. + * + * @param crypt The {@link mongocrypt_t} object. + * @param kms_providers A BSON document mapping the KMS provider names to credentials. Set a KMS + * provider value to an empty document to supply credentials on-demand with + * {@link #mongocrypt_ctx_provide_kms_providers}. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_setopt_kms_providers(mongocrypt_t crypt, @@ -435,41 +482,50 @@ public interface mongocrypt_random_fn extends Callback { /** * Set a local schema map for encryption. * - * @param crypt The @ref mongocrypt_t object. + *

Requires that {@code crypt} has not been initialized. + * + * @param crypt The {@link mongocrypt_t} object. * @param schema_map A BSON document representing the schema map supplied by - * the user. The keys are collection namespaces and values are JSON schemas. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_status + * the user. The keys are collection namespaces and values are JSON schemas. + * The viewed data copied. It is valid to destroy {@code schema_map} with + * {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_status} */ public static native boolean - mongocrypt_setopt_schema_map (mongocrypt_t crypt, mongocrypt_binary_t schema_map); + mongocrypt_setopt_schema_map(mongocrypt_t crypt, mongocrypt_binary_t schema_map); /** - * Opt-into setting KMS providers before each KMS request. + * Opt-into handling the {@link #MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS} state. * - * If set, before entering the MONGOCRYPT_CTX_NEED_KMS state, - * contexts will enter the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state - * and then wait for credentials to be supplied through @ref mongocrypt_ctx_provide_kms_providers. + *

If set, before entering the {@link #MONGOCRYPT_CTX_NEED_KMS} state, + * contexts may enter the {@link #MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS} state + * and then wait for credentials to be supplied through + * {@link #mongocrypt_ctx_provide_kms_providers}. * - * @param crypt The @ref mongocrypt_t object to update + *

A context will only enter {@link #MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS} + * if an empty document was set for a KMS provider in + * {@link #mongocrypt_setopt_kms_providers}. + * + * @param crypt The {@link mongocrypt_t} object to update */ public static native void - mongocrypt_setopt_use_need_kms_credentials_state (mongocrypt_t crypt); + mongocrypt_setopt_use_need_kms_credentials_state(mongocrypt_t crypt); /** * Set a local EncryptedFieldConfigMap for encryption. * - * @param crypt The @ref mongocrypt_t object. + *

Requires that {@code crypt} has not been initialized. + * + * @param crypt The {@link mongocrypt_t} object. * @param encryptedFieldConfigMap A BSON document representing the EncryptedFieldConfigMap - * supplied by the user. The keys are collection namespaces and values are - * EncryptedFieldConfigMap documents. The viewed data copied. It is valid to - * destroy @p efc_map with @ref mongocrypt_binary_destroy immediately after. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_status + * supplied by the user. The keys are collection namespaces and values are + * EncryptedFieldConfigMap documents. The viewed data copied. It is valid to + * destroy {@code efc_map} with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_status} */ public static native boolean - mongocrypt_setopt_encrypted_field_config_map (mongocrypt_t crypt, mongocrypt_binary_t encryptedFieldConfigMap); + mongocrypt_setopt_encrypted_field_config_map(mongocrypt_t crypt, mongocrypt_binary_t encryptedFieldConfigMap); /** * Opt-into skipping query analysis. @@ -477,113 +533,118 @@ public interface mongocrypt_random_fn extends Callback { *

If opted in: *

    *
  • The crypt_shared shared library will not attempt to be loaded.
  • - *
  • A mongocrypt_ctx_t will never enter the MONGOCRYPT_CTX_NEED_MARKINGS state.
  • + *
  • A mongocrypt_ctx_t will never enter the {@link #MONGOCRYPT_CTX_NEED_MONGO_MARKINGS} state.
  • *
* - * @param crypt The @ref mongocrypt_t object to update - * @since 1.5 + * @param crypt The {@link mongocrypt_t} object to update */ public static native void - mongocrypt_setopt_bypass_query_analysis (mongocrypt_t crypt); + mongocrypt_setopt_bypass_query_analysis(mongocrypt_t crypt); + + /** + * Enable or disable KMS retry behavior. + * + *

Requires that {@link #mongocrypt_init} has not been called on {@code crypt}. + * + * @param crypt The {@link mongocrypt_t} object. + * @param enable A boolean indicating whether to retry operations. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} + */ + public static native boolean + mongocrypt_setopt_retry_kms(mongocrypt_t crypt, boolean enable); /** * Set the expiration time for the data encryption key cache. Defaults to 60 seconds if not set. * - * @param crypt The @ref mongocrypt_t object to update + * @param crypt The {@link mongocrypt_t} object to update * @param cache_expiration_ms if 0 the cache never expires * @return A boolean indicating success. If false, an error status is set. - * @since 5.4 */ public static native boolean - mongocrypt_setopt_key_expiration (mongocrypt_t crypt, long cache_expiration_ms); + mongocrypt_setopt_key_expiration(mongocrypt_t crypt, long cache_expiration_ms); /** * Opt-into enabling sending multiple collection info documents. * - * @param crypt The @ref mongocrypt_t object to update + * @param crypt The {@link mongocrypt_t} object to update */ public static native void - mongocrypt_setopt_enable_multiple_collinfo (mongocrypt_t crypt); + mongocrypt_setopt_enable_multiple_collinfo(mongocrypt_t crypt); /** * Set the contention factor used for explicit encryption. * The contention factor is only used for indexed Queryable Encryption. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param contention_factor the contention factor - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status. - * @since 1.5 + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status}. */ public static native boolean - mongocrypt_ctx_setopt_contention_factor (mongocrypt_ctx_t ctx, long contention_factor); + mongocrypt_ctx_setopt_contention_factor(mongocrypt_ctx_t ctx, long contention_factor); /** * Set the index key id to use for Queryable Encryption explicit encryption. + *

+ * If the index key id not set, the key id from {@link #mongocrypt_ctx_setopt_key_id} is used. * - * If the index key id not set, the key id from @ref mongocrypt_ctx_setopt_key_id is used. - * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param key_id The binary corresponding to the _id (a UUID) of the data key to use from * the key vault collection. Note, the UUID must be encoded with RFC-4122 byte order. - * The viewed data is copied. It is valid to destroy key_id with @ref mongocrypt_binary_destroy immediately after. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status - * @since 1.5 + * The viewed data is copied. It is valid to destroy key_id with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_index_key_id (mongocrypt_ctx_t ctx, mongocrypt_binary_t key_id); + mongocrypt_ctx_setopt_index_key_id(mongocrypt_ctx_t ctx, mongocrypt_binary_t key_id); /** * Append an additional search directory to the search path for loading * the crypt_shared dynamic library. * - * @param crypt The @ref mongocrypt_t object to update - * @param path A null-terminated sequence of bytes for the search path. On - * some filesystems, this may be arbitrary bytes. On other filesystems, this may - * be required to be a valid UTF-8 code unit sequence. If the leading element of - * the path is the literal string "$ORIGIN", that substring will be replaced - * with the directory path containing the executable libmongocrypt module. If - * the path string is literal "$SYSTEM", then libmongocrypt will defer to the - * system's library resolution mechanism to find the crypt_shared library. - * - *

If no crypt_shared dynamic library is found in any of the directories - * specified by the search paths loaded here, @ref mongocrypt_init() will still - * succeed and continue to operate without crypt_shared.

- * - *

The search paths are searched in the order that they are appended. This - * allows one to provide a precedence in how the library will be discovered. For - * example, appending known directories before appending "$SYSTEM" will allow - * one to supersede the system's installed library, but still fall-back to it if - * the library wasn't found otherwise. If one does not ever append "$SYSTEM", - * then the system's library-search mechanism will never be consulted.

- * - *

If an absolute path to the library is specified using @ref mongocrypt_setopt_set_crypt_shared_lib_path_override, - * then paths appended here will have no effect.

- * @since 1.5 + * @param crypt The {@link mongocrypt_t} object to update + * @param path A null-terminated sequence of bytes for the search path. On + * some filesystems, this may be arbitrary bytes. On other filesystems, this may + * be required to be a valid UTF-8 code unit sequence. If the leading element of + * the path is the literal string "$ORIGIN", that substring will be replaced + * with the directory path containing the executable libmongocrypt module. If + * the path string is literal "$SYSTEM", then libmongocrypt will defer to the + * system's library resolution mechanism to find the crypt_shared library. + * + *

If no crypt_shared dynamic library is found in any of the directories + * specified by the search paths loaded here, {@link #mongocrypt_init}() will still + * succeed and continue to operate without crypt_shared.

+ * + *

The search paths are searched in the order that they are appended. This + * allows one to provide a precedence in how the library will be discovered. For + * example, appending known directories before appending "$SYSTEM" will allow + * one to supersede the system's installed library, but still fall-back to it if + * the library wasn't found otherwise. If one does not ever append "$SYSTEM", + * then the system's library-search mechanism will never be consulted.

+ * + *

If an absolute path to the library is specified using {@link #mongocrypt_setopt_set_crypt_shared_lib_path_override}, + * then paths appended here will have no effect.

*/ public static native void - mongocrypt_setopt_append_crypt_shared_lib_search_path (mongocrypt_t crypt, cstring path); + mongocrypt_setopt_append_crypt_shared_lib_search_path(mongocrypt_t crypt, cstring path); /** * Set a single override path for loading the crypt_shared dynamic library. - * @param crypt The @ref mongocrypt_t object to update - * @param path A null-terminated sequence of bytes for a path to the crypt_shared - * dynamic library. On some filesystems, this may be arbitrary bytes. On other - * filesystems, this may be required to be a valid UTF-8 code unit sequence. If - * the leading element of the path is the literal string `$ORIGIN`, that - * substring will be replaced with the directory path containing the executable - * libmongocrypt module. - * - *

This function will do no IO nor path validation. All validation will - * occur during the call to @ref mongocrypt_init.

- *

If a crypt_shared library path override is specified here, then no paths given - * to @ref mongocrypt_setopt_append_crypt_shared_lib_search_path will be consulted when - * opening the crypt_shared library.

- *

If a path is provided via this API and @ref mongocrypt_init fails to - * initialize a valid crypt_shared library instance for the path specified, then - * the initialization of mongocrypt_t will fail with an error.

- * @since 1.5 + * + * @param crypt The {@link mongocrypt_t} object to update + * @param path A null-terminated sequence of bytes for a path to the crypt_shared + * dynamic library. On some filesystems, this may be arbitrary bytes. On other + * filesystems, this may be required to be a valid UTF-8 code unit sequence. If + * the leading element of the path is the literal string `$ORIGIN`, that + * substring will be replaced with the directory path containing the executable + * libmongocrypt module. + * + *

This function will do no IO nor path validation. All validation will + * occur during the call to {@link #mongocrypt_init}.

+ *

If a crypt_shared library path override is specified here, then no paths given + * to {@link #mongocrypt_setopt_append_crypt_shared_lib_search_path} will be consulted when + * opening the crypt_shared library.

+ *

If a path is provided via this API and {@link #mongocrypt_init} fails to + * initialize a valid crypt_shared library instance for the path specified, then + * the initialization of mongocrypt_t will fail with an error.

*/ public static native void mongocrypt_setopt_set_crypt_shared_lib_path_override(mongocrypt_t crypt, cstring path); @@ -592,80 +653,80 @@ public interface mongocrypt_random_fn extends Callback { * Set the query type to use for Queryable Encryption explicit encryption. * The query type is only used for indexed Queryable Encryption. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param query_type the query type - * @param len the length - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status + * @param len the length + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_query_type (mongocrypt_ctx_t ctx, cstring query_type, int len); + mongocrypt_ctx_setopt_query_type(mongocrypt_ctx_t ctx, cstring query_type, int len); /** * Set options for explicit encryption with the "range" algorithm. - * NOTE: "range" is currently unstable API and subject to backwards breaking changes. - * + *

* opts is a BSON document of the form: * { - * "min": Optional<BSON value>, - * "max": Optional<BSON value>, - * "sparsity": Int64, - * "precision": Optional<Int32> - * "trimFactor": Optional<Int32> + * "min": Optional<BSON value>, + * "max": Optional<BSON value>, + * "sparsity": Optional<Int64>, + * "precision": Optional<Int32> + * "trimFactor": Optional<Int32> * } * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param opts BSON. * @return A boolean indicating success. If false, an error status is set. - * @since 1.7 */ public static native boolean - mongocrypt_ctx_setopt_algorithm_range (mongocrypt_ctx_t ctx, mongocrypt_binary_t opts); + 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, - * }, + * "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 ctx The {@link 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. + * Initialize new {@link mongocrypt_t} object. + * + *

Set options before using {@link #mongocrypt_setopt_kms_provider_local}, + * {@link #mongocrypt_setopt_kms_provider_aws}, or {@link #mongocrypt_setopt_log_handler}. * - * @param crypt The @ref mongocrypt_t object. - * @return A boolean indicating success. Failure may occur if previously set options are invalid. + * @param crypt The {@link mongocrypt_t} object. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with + * {@link #mongocrypt_ctx_status} Failure may occur if previously set options are invalid. */ public static native boolean mongocrypt_init(mongocrypt_t crypt); /** - * Get the status associated with a @ref mongocrypt_t object. + * Get the status associated with a {@link mongocrypt_t} object. * - * @param crypt The @ref mongocrypt_t object. + * @param crypt The {@link mongocrypt_t} object. * @param status Receives the status. * @return A boolean indicating success. */ @@ -685,9 +746,9 @@ public interface mongocrypt_random_fn extends Callback { mongocrypt_is_crypto_available(); /** - * Destroy the @ref mongocrypt_t object. + * Destroy the {@link mongocrypt_t} object. * - * @param crypt The @ref mongocrypt_t object to destroy. + * @param crypt The {@link mongocrypt_t} object to destroy. */ public static native void mongocrypt_destroy(mongocrypt_t crypt); @@ -695,45 +756,49 @@ public interface mongocrypt_random_fn extends Callback { /** * Obtain a nul-terminated version string of the loaded crypt_shared dynamic library, * if available. - * + *

* If no crypt_shared was successfully loaded, this function returns NULL. * - * @param crypt The mongocrypt_t object after a successful call to mongocrypt_init. - * @param len an optional length of the returned string. May be NULL. - * + * @param crypt The {@link mongocrypt_t} object after a successful call to {@link #mongocrypt_init}. + * @param len an optional length of the returned string. May be NULL. * @return A nul-terminated string of the dynamically loaded crypt_shared library. - * @since 1.5 */ public static native cstring - mongocrypt_crypt_shared_lib_version_string (mongocrypt_t crypt, Pointer len); + mongocrypt_crypt_shared_lib_version_string(mongocrypt_t crypt, Pointer len); /** - * Call in response to the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state + * Call in response to the {@link #MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS} state * to set per-context KMS provider settings. These follow the same format - * as @ref mongocrypt_setopt_kms_providers. If no keys are present in the - * BSON input, the KMS provider settings configured for the @ref mongocrypt_t + * as {@link #mongocrypt_setopt_kms_providers}. If no keys are present in the + * BSON input, the KMS provider settings configured for the {@link mongocrypt_t} * at initialization are used. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param kms_providers A BSON document mapping the KMS provider names - * to credentials. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status. + * to credentials. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status}. */ public static native boolean - mongocrypt_ctx_provide_kms_providers (mongocrypt_ctx_t ctx, - mongocrypt_binary_t kms_providers); + mongocrypt_ctx_provide_kms_providers(mongocrypt_ctx_t ctx, + mongocrypt_binary_t kms_providers); /** * Set the key id to use for explicit encryption. * - * @param ctx The @ref mongocrypt_ctx_t object. - * @param key_id The key_id to use. - * @return A boolean indicating success. + *

It is an error to set both this and the key alt name. + * + *

Requires that {@code ctx} has not been initialized. + * + * @param ctx The {@link mongocrypt_ctx_t} object. + * @param key_id The binary corresponding to the _id (a UUID) of the data + * key to use from the key vault collection. Note, the UUID must be encoded with + * RFC-4122 byte order. The viewed data is copied. It is valid to destroy + * {@code key_id} with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_key_id (mongocrypt_ctx_t ctx, - mongocrypt_binary_t key_id); + mongocrypt_ctx_setopt_key_id(mongocrypt_ctx_t ctx, + mongocrypt_binary_t key_id); /** * Set the keyAltName to use for explicit encryption. @@ -742,14 +807,13 @@ public interface mongocrypt_random_fn extends Callback { * *

It is an error to set both this and the key id.

* - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param key_alt_name The name to use. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_key_alt_name (mongocrypt_ctx_t ctx, - mongocrypt_binary_t key_alt_name); + mongocrypt_ctx_setopt_key_alt_name(mongocrypt_ctx_t ctx, + mongocrypt_binary_t key_alt_name); /** * Set the keyMaterial to use for encrypting data. @@ -759,32 +823,32 @@ public interface mongocrypt_random_fn extends Callback { * { "keyMaterial" : (BSON BINARY value) } *

* - * @param ctx The @ref mongocrypt_ctx_t object. + *

Requires that {@code ctx} has not been initialized. + * + * @param ctx The {@link mongocrypt_ctx_t} object. * @param key_material The data encryption key to use. The viewed data is - * copied. It is valid to destroy @p key_material with @ref - * mongocrypt_binary_destroy immediately after. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status + * copied. It is valid to destroy {@code key_material} with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_key_material (mongocrypt_ctx_t ctx, mongocrypt_binary_t key_material); + mongocrypt_ctx_setopt_key_material(mongocrypt_ctx_t ctx, mongocrypt_binary_t key_material); /** * Set the algorithm used for encryption to either * deterministic or random encryption. This value * should only be set when using explicit encryption. - * + *

* If -1 is passed in for "len", then "algorithm" is * assumed to be a null-terminated string. - * + *

* Valid values for algorithm are: - * "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" - * "AEAD_AES_256_CBC_HMAC_SHA_512-Randomized" + * "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + * "AEAD_AES_256_CBC_HMAC_SHA_512-Random" * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param algorithm A string specifying the algorithm to - * use for encryption. - * @param len The length of the algorithm string. + * use for encryption. + * @param len The length of the algorithm string. * @return A boolean indicating success. */ public static native boolean @@ -794,12 +858,12 @@ public interface mongocrypt_random_fn extends Callback { /** - * Create a new uninitialized @ref mongocrypt_ctx_t. + * Create a new uninitialized {@link mongocrypt_ctx_t}. *

- * Initialize the context with functions like @ref mongocrypt_ctx_encrypt_init. - * When done, destroy it with @ref mongocrypt_ctx_destroy. + * Initialize the context with functions like {@link #mongocrypt_ctx_encrypt_init}. + * When done, destroy it with {@link #mongocrypt_ctx_destroy}. * - * @param crypt The @ref mongocrypt_t object. + * @param crypt The {@link mongocrypt_t} object. * @return A new context. */ public static native mongocrypt_ctx_t @@ -807,9 +871,9 @@ public interface mongocrypt_random_fn extends Callback { /** - * Get the status associated with a @ref mongocrypt_ctx_t object. + * Get the status associated with a {@link mongocrypt_ctx_t} object. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param status Receives the status. * @return A boolean indicating success. */ @@ -821,14 +885,14 @@ public interface mongocrypt_random_fn extends Callback { /** * Identify the AWS KMS master key to use for creating a data key. * - * @param ctx The @ref mongocrypt_ctx_t object. - * @param region The AWS region. - * @param region_len The string length of @p region. Pass -1 to determine - * the string length with strlen (must be NULL terminated). - * @param cmk The Amazon Resource Name (ARN) of the customer master key - * (CMK). - * @param cmk_len The string length of @p cmk_len. Pass -1 to determine the - * string length with strlen (must be NULL terminated). + * @param ctx The {@link mongocrypt_ctx_t} object. + * @param region The AWS region. + * @param region_len The string length of {@code region}. Pass -1 to determine + * the string length with strlen (must be NULL terminated). + * @param cmk The Amazon Resource Name (ARN) of the customer master key + * (CMK). + * @param cmk_len The string length of {@code cmk_len}. Pass -1 to determine the + * string length with strlen (must be NULL terminated). * @return A boolean indicating success. */ public static native boolean @@ -845,12 +909,11 @@ public interface mongocrypt_random_fn extends Callback { * is persisted in the new data key, and will be returned via * mongocrypt_kms_ctx_endpoint. * - * @param ctx The @ref mongocrypt_ctx_t object. - * @param endpoint The endpoint. - * @param endpoint_len The string length of @p endpoint. Pass -1 to - * determine the string length with strlen (must be NULL terminated). - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status + * @param ctx The {@link mongocrypt_ctx_t} object. + * @param endpoint The endpoint. + * @param endpoint_len The string length of {@code endpoint}. Pass -1 to + * determine the string length with strlen (must be NULL terminated). + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_ctx_setopt_masterkey_aws_endpoint (mongocrypt_ctx_t ctx, @@ -860,53 +923,52 @@ public interface mongocrypt_random_fn extends Callback { /** * Set the master key to "local" for creating a data key. + * This has been superseded by the more flexible: + * {@link #mongocrypt_ctx_setopt_key_encryption_key} * - * @param ctx The @ref mongocrypt_ctx_t object. - * @return A boolean indicating success. + *

Requires that {@code ctx} has not been initialized. + * + * @param ctx The {@link mongocrypt_ctx_t} object. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean - mongocrypt_ctx_setopt_masterkey_local (mongocrypt_ctx_t ctx); + mongocrypt_ctx_setopt_masterkey_local(mongocrypt_ctx_t ctx); /** * Set key encryption key document for creating a data key. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param keyDocument BSON representing the key encryption key document. * @return A boolean indicating success. If false, and error status is set. - * @since 1.1 */ public static native boolean mongocrypt_ctx_setopt_key_encryption_key(mongocrypt_ctx_t ctx, - mongocrypt_binary_t keyDocument); + mongocrypt_binary_t keyDocument); /** * Initialize a context to create a data key. - * - * Set options before using @ref mongocrypt_ctx_setopt_masterkey_aws and + *

+ * Set options before using {@link #mongocrypt_ctx_setopt_masterkey_aws} and * mongocrypt_ctx_setopt_masterkey_local. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @return A boolean indicating success. - * + *

* Assumes a master key option has been set, and an associated KMS provider - * has been set on the parent @ref mongocrypt_t. + * has been set on the parent {@link mongocrypt_t}. */ public static native boolean - mongocrypt_ctx_datakey_init (mongocrypt_ctx_t ctx); + mongocrypt_ctx_datakey_init(mongocrypt_ctx_t ctx); /** * Initialize a context for encryption. * - * Associated options: - * - @ref mongocrypt_ctx_setopt_cache_noblock - * - @ref mongocrypt_ctx_setopt_schema - * - * @param ctx The @ref mongocrypt_ctx_t object. - * @param db The database name. - * @param db_len The byte length of @p db. Pass -1 to determine the string length with strlen (must be NULL terminated). - * @param cmd The BSON command to be encrypted. - * @return A boolean indicating success. If false, an error status is set. - * Retrieve it with @ref mongocrypt_ctx_status + * @param ctx The {@link mongocrypt_ctx_t} object. + * @param db The database name. + * @param db_len The byte length of {@code db}. Pass -1 to determine the string length with strlen (must be NULL terminated). + * @param cmd The BSON command to be encrypted. The viewed data is copied. + * It is valid to destroy {@code cmd} with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_ctx_encrypt_init(mongocrypt_ctx_t ctx, @@ -917,65 +979,67 @@ public interface mongocrypt_random_fn extends Callback { /** * Explicit helper method to encrypt a single BSON object. Contexts * created for explicit encryption will not go through mongocryptd. - * + *

* To specify a key_id, algorithm, or iv to use, please use the * corresponding mongocrypt_setopt methods before calling this. - * + *

* This method expects the passed-in BSON to be of the form: * { "v" : BSON value to encrypt } * - * @param ctx A @ref mongocrypt_ctx_t. - * @param msg A @ref mongocrypt_binary_t the plaintext BSON value. + * @param ctx A {@link mongocrypt_ctx_t}. + * @param msg A {@link mongocrypt_binary_t} the plaintext BSON value. * @return A boolean indicating success. */ public static native boolean - mongocrypt_ctx_explicit_encrypt_init (mongocrypt_ctx_t ctx, - mongocrypt_binary_t msg); + mongocrypt_ctx_explicit_encrypt_init(mongocrypt_ctx_t ctx, + mongocrypt_binary_t msg); /** * Explicit helper method to encrypt a Match Expression or Aggregate Expression. * Contexts created for explicit encryption will not go through mongocryptd. * Requires query_type to be "range". - * NOTE: "range" is currently unstable API and subject to backwards breaking changes. - * + *

* This method expects the passed-in BSON to be of the form: - * { "v" : FLE2RangeFindDriverSpec } + * {@code { "v" : FLE2RangeFindDriverSpec } } * * FLE2RangeFindDriverSpec is a BSON document with one of these forms: * + *

+     * 
      * 1. A Match Expression of this form:
-     *    {$and: [{<field>: {<op>: <value1>, {<field>: {<op>: <value2> }}]}
+     *    {$and: [{: {: , {: {:  }}]}
      * 2. An Aggregate Expression of this form:
-     *    {$and: [{<op>: [<fieldpath>, <value1>]}, {<op>: [<fieldpath>, <value2>]}]
+     *    {$and: [{: [, ]}, {: [, ]}]
      *
      * may be $lt, $lte, $gt, or $gte.
+     * 
+     * 
* * The value of "v" is expected to be the BSON value passed to a driver * ClientEncryption.encryptExpression helper. * + *

* Associated options for FLE 1: - * - @ref mongocrypt_ctx_setopt_key_id - * - @ref mongocrypt_ctx_setopt_key_alt_name - * - @ref mongocrypt_ctx_setopt_algorithm - * + * - {@link #mongocrypt_ctx_setopt_key_id} + * - {@link #mongocrypt_ctx_setopt_key_alt_name} + * - {@link #mongocrypt_ctx_setopt_algorithm} + *

* Associated options for Queryable Encryption: - * - @ref mongocrypt_ctx_setopt_key_id - * - @ref mongocrypt_ctx_setopt_index_key_id - * - @ref mongocrypt_ctx_setopt_contention_factor - * - @ref mongocrypt_ctx_setopt_query_type - * - @ref mongocrypt_ctx_setopt_algorithm_range - * + * - {@link #mongocrypt_ctx_setopt_key_id} + * - {@link #mongocrypt_ctx_setopt_index_key_id} + * - {@link #mongocrypt_ctx_setopt_contention_factor} + * - {@link #mongocrypt_ctx_setopt_query_type} + * - {@link #mongocrypt_ctx_setopt_algorithm_range} + *

* An error is returned if FLE 1 and Queryable Encryption incompatible options * are set. * - * @param ctx A @ref mongocrypt_ctx_t. - * @param msg A @ref mongocrypt_binary_t the plaintext BSON value. + * @param ctx A {@link mongocrypt_ctx_t}. + * @param msg A {@link mongocrypt_binary_t} the plaintext BSON value. * @return A boolean indicating success. - * @since 1.7 */ public static native boolean - mongocrypt_ctx_explicit_encrypt_expression_init (mongocrypt_ctx_t ctx, - mongocrypt_binary_t msg); + mongocrypt_ctx_explicit_encrypt_expression_init(mongocrypt_ctx_t ctx, mongocrypt_binary_t msg); /** * Initialize a context for decryption. @@ -991,26 +1055,24 @@ public interface mongocrypt_random_fn extends Callback { /** * Explicit helper method to decrypt a single BSON object. * - * @param ctx A @ref mongocrypt_ctx_t. - * @param msg A @ref mongocrypt_binary_t the encrypted BSON. + * @param ctx A {@link mongocrypt_ctx_t}. + * @param msg A {@link mongocrypt_binary_t} the encrypted BSON. * @return A boolean indicating success. */ public static native boolean - mongocrypt_ctx_explicit_decrypt_init (mongocrypt_ctx_t ctx, - mongocrypt_binary_t msg); + mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t ctx, mongocrypt_binary_t msg); /** * Initialize a context to rewrap datakeys. - * + *

* Associated options {@link #mongocrypt_ctx_setopt_key_encryption_key(mongocrypt_ctx_t, mongocrypt_binary_t)} * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param filter The filter to use for the find command on the key vault collection to retrieve datakeys to rewrap. * @return A boolean indicating success. If false, and error status is set. - * @since 1.5 */ public static native boolean - mongocrypt_ctx_rewrap_many_datakey_init (mongocrypt_ctx_t ctx, mongocrypt_binary_t filter); + mongocrypt_ctx_rewrap_many_datakey_init(mongocrypt_ctx_t ctx, mongocrypt_binary_t filter); public static final int MONGOCRYPT_CTX_ERROR = 0; @@ -1029,8 +1091,8 @@ public interface mongocrypt_random_fn extends Callback { /** * Get the current state of a context. * - * @param ctx The @ref mongocrypt_ctx_t object. - * @return A @ref mongocrypt_ctx_state_t. + * @param ctx The {@link mongocrypt_ctx_t} object. + * @return A mongocrypt_ctx_state_t. */ public static native int mongocrypt_ctx_state(mongocrypt_ctx_t ctx); @@ -1041,15 +1103,20 @@ public interface mongocrypt_random_fn extends Callback { * is in MONGOCRYPT_CTX_NEED_MONGO_* states. * *

- * op_bson is a BSON document to be used for the operation. - * - For MONGOCRYPT_CTX_NEED_MONGO_COLLINFO it is a listCollections filter. - * - For MONGOCRYPT_CTX_NEED_MONGO_KEYS it is a find filter. - * - For MONGOCRYPT_CTX_NEED_MONGO_MARKINGS it is a JSON schema to append. + * {@code op_bson} is a BSON document to be used for the operation. + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_COLLINFO}(_WITH_DB) it is a listCollections filter. + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_KEYS} it is a find filter. + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_MARKINGS} it is a command to send to mongocryptd. *

* - * @param ctx The @ref mongocrypt_ctx_t object. - * @param op_bson A BSON document for the MongoDB operation. - * @return A boolean indicating success. + *

The lifetime of {@code op_bson} is tied to the lifetime of {@code ctx}. It is valid + * until {@link #mongocrypt_ctx_destroy} is called. + * + * @param ctx The {@link mongocrypt_ctx_t} object. + * @param op_bson A BSON document for the MongoDB operation. The data + * viewed by {@code op_bson} is guaranteed to be valid until {@code ctx} is destroyed with + * {@link #mongocrypt_ctx_destroy}. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_ctx_mongo_op(mongocrypt_ctx_t ctx, mongocrypt_binary_t op_bson); @@ -1061,12 +1128,12 @@ public interface mongocrypt_random_fn extends Callback { * depending on the operation. *

* op_bson is a BSON document to be used for the operation. - * - For MONGOCRYPT_CTX_NEED_MONGO_COLLINFO it is a doc from a listCollections + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_COLLINFO} it is a doc from a listCollections * cursor. - * - For MONGOCRYPT_CTX_NEED_MONGO_KEYS it is a doc from a find cursor. - * - For MONGOCRYPT_CTX_NEED_MONGO_MARKINGS it is a reply from mongocryptd. + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_KEYS} it is a doc from a find cursor. + * - For {@link #MONGOCRYPT_CTX_NEED_MONGO_MARKINGS} it is a reply from mongocryptd. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @param reply A BSON document for the MongoDB operation. * @return A boolean indicating success. */ @@ -1077,7 +1144,7 @@ public interface mongocrypt_random_fn extends Callback { /** * Call when done feeding the reply (or replies) back to the context. * - * @param ctx The @ref mongocrypt_ctx_t object. + * @param ctx The {@link mongocrypt_ctx_t} object. * @return A boolean indicating success. */ @@ -1091,35 +1158,36 @@ public interface mongocrypt_random_fn extends Callback { * out multiple concurrent KMS HTTP requests. Feeding multiple KMS requests * is thread-safe. *

- * Is KMS handles are being handled synchronously, the driver can reuse the same + * If KMS handles are being handled synchronously, the driver can reuse the same * TLS socket to send HTTP requests and receive responses. + *

+ * The returned KMS handle does not outlive {@code ctx}. * - * @param ctx A @ref mongocrypt_ctx_t. - * @return a new @ref mongocrypt_kms_ctx_t or NULL. + * @param ctx A {@link mongocrypt_ctx_t}. + * @return a new {@link mongocrypt_kms_ctx_t} or NULL. */ public static native mongocrypt_kms_ctx_t mongocrypt_ctx_next_kms_ctx(mongocrypt_ctx_t ctx); /** * Get the KMS provider identifier associated with this KMS request. - * + *

* This is used to conditionally configure TLS connections based on the KMS * request. It is useful for KMIP, which authenticates with a client * certificate. * * @param kms The mongocrypt_kms_ctx_t object. * @param len Receives the length of the returned string. - * * @return The name of the KMS provider */ public static native cstring mongocrypt_kms_ctx_get_kms_provider(mongocrypt_kms_ctx_t kms, - Pointer len); + Pointer len); /** * Get the HTTP request message for a KMS handle. * - * @param kms A @ref mongocrypt_kms_ctx_t. + * @param kms A {@link mongocrypt_kms_ctx_t}. * @param msg The HTTP request to send to KMS. * @return A boolean indicating success. */ @@ -1130,11 +1198,11 @@ public interface mongocrypt_random_fn extends Callback { /** * Get the hostname from which to connect over TLS. *

- * The storage for @p endpoint is not owned by the caller, but - * is valid until calling @ref mongocrypt_ctx_kms_done on the - * parent @ref mongocrypt_ctx_t. + * The storage for {@code endpoint} is not owned by the caller, but + * is valid until calling {@link #mongocrypt_ctx_kms_done} on the + * parent {@link mongocrypt_ctx_t}. * - * @param kms A @ref mongocrypt_kms_ctx_t. + * @param kms A {@link mongocrypt_kms_ctx_t}. * @param endpoint The output hostname. * @return A boolean indicating success. */ @@ -1142,9 +1210,9 @@ public interface mongocrypt_random_fn extends Callback { mongocrypt_kms_ctx_endpoint(mongocrypt_kms_ctx_t kms, PointerByReference endpoint); /** - * Indicates how many bytes to feed into @ref mongocrypt_kms_ctx_feed. + * Indicates how many bytes to feed into {@link #mongocrypt_kms_ctx_feed}. * - * @param kms The @ref mongocrypt_kms_ctx_t. + * @param kms The {@link mongocrypt_kms_ctx_t}. * @return The number of requested bytes. */ public static native int @@ -1154,34 +1222,72 @@ public interface mongocrypt_random_fn extends Callback { /** * Feed bytes from the HTTP response. *

- * Feeding more bytes than what has been returned in @ref - * mongocrypt_kms_ctx_bytes_needed is an error. + * Feeding more bytes than what has been returned in + * {@link #mongocrypt_kms_ctx_bytes_needed} is an error. * - * @param kms The @ref mongocrypt_kms_ctx_t. - * @param bytes The bytes to feed. - * @return A boolean indicating success. + * @param kms The {@link mongocrypt_kms_ctx_t}. + * @param bytes The bytes to feed. The viewed data is copied. It is valid to + * destroy bytes with {@link #mongocrypt_binary_destroy} immediately after. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_kms_ctx_status} */ public static native boolean mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t kms, mongocrypt_binary_t bytes); + /** + * Indicates how long to sleep before sending this request. + * + * @param kms The {@link mongocrypt_kms_ctx_t}. + * @return How long to sleep in microseconds. + */ + public static native long + mongocrypt_kms_ctx_usleep(mongocrypt_kms_ctx_t kms); + + /** + * Feed bytes from the KMS response. + * + *

Feeding more bytes than what has been returned in + * {@link #mongocrypt_kms_ctx_bytes_needed} is an error. + * + * @param kms The {@link mongocrypt_kms_ctx_t}. + * @param bytes The bytes to feed. The viewed data is copied. It is valid to + * destroy {@code bytes} with {@link #mongocrypt_binary_destroy} immediately after. + * @param should_retry Whether the KMS request should be retried. Retry in-place + * without calling {@link #mongocrypt_kms_ctx_fail}. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_kms_ctx_status} + */ + public static native boolean + mongocrypt_kms_ctx_feed_with_retry(mongocrypt_kms_ctx_t kms, + mongocrypt_binary_t bytes, + ByteByReference should_retry); /** - * Get the status associated with a @ref mongocrypt_kms_ctx_t object. + * Indicate a network error. Discards all data fed to this KMS context with + * {@link #mongocrypt_kms_ctx_feed}. The {@link mongocrypt_kms_ctx_t} may be reused. * - * @param kms The @ref mongocrypt_kms_ctx_t object. + * @param kms The {@link mongocrypt_kms_ctx_t}. + * @return A boolean indicating whether the failed request may be retried. + */ + public static native boolean + mongocrypt_kms_ctx_fail(mongocrypt_kms_ctx_t kms); + + + /** + * Get the status associated with a {@link mongocrypt_kms_ctx_t} object. + * + * @param kms The {@link mongocrypt_kms_ctx_t} object. * @param status Receives the status. * @return A boolean indicating success. */ public static native boolean mongocrypt_kms_ctx_status(mongocrypt_kms_ctx_t kms, - mongocrypt_status_t status); + mongocrypt_status_t status); /** * Call when done handling all KMS contexts. * - * @param ctx The @ref mongocrypt_ctx_t object. - * @return A boolean indicating success. + * @param ctx The {@link mongocrypt_ctx_t} object. + * @return A boolean indicating success. If false, an error status is set. Retrieve it with {@link #mongocrypt_ctx_status} */ public static native boolean mongocrypt_ctx_kms_done(mongocrypt_ctx_t ctx); @@ -1190,7 +1296,7 @@ public interface mongocrypt_random_fn extends Callback { /** * Perform the final encryption or decryption. * - * @param ctx A @ref mongocrypt_ctx_t. + * @param ctx A {@link mongocrypt_ctx_t}. * @param out The final BSON to send to the server. * @return a boolean indicating success. */ @@ -1199,9 +1305,9 @@ public interface mongocrypt_random_fn extends Callback { /** - * Destroy and free all memory associated with a @ref mongocrypt_ctx_t. + * Destroy and free all memory associated with a {@link mongocrypt_ctx_t}. * - * @param ctx A @ref mongocrypt_ctx_t. + * @param ctx A {@link mongocrypt_ctx_t}. */ public static native void mongocrypt_ctx_destroy(mongocrypt_ctx_t ctx); 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 774b9e718c..e4d0a74e1d 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 @@ -73,6 +73,7 @@ import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_kms_provider_local; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_kms_providers; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_log_handler; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_retry_kms; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_schema_map; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_set_crypt_shared_lib_path_override; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_use_need_kms_credentials_state; @@ -198,6 +199,9 @@ class MongoCryptImpl implements MongoCrypt { mongocrypt_setopt_use_need_kms_credentials_state(wrapped); } + // always enabled; backoff and budget management are the driver's responsibility + configure(() -> mongocrypt_setopt_retry_kms(wrapped, true)); + if (options.getKmsProviderOptions() != null) { withBinaryHolder(options.getKmsProviderOptions(), binary -> configure(() -> mongocrypt_setopt_kms_providers(wrapped, binary))); diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptor.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptor.java index 9b0eae6776..6bb6a34806 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptor.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptor.java @@ -70,7 +70,32 @@ public interface MongoKeyDecryptor { * returns 0. *

* + *

The driver uses {@link #feedWithRetry(ByteBuffer)}; this method is retained to mirror the libmongocrypt API. + * * @param bytes the received bytes + * @see #feedWithRetry(ByteBuffer) */ void feed(ByteBuffer bytes); + + /** + * Gets the number of microseconds to sleep before sending the next KMS request. + * + * @return the number of microseconds to sleep, or 0 if no delay is needed + */ + long sleepMicroseconds(); + + /** + * Feed the received bytes to the decryptor, with retry support. + * + * @param bytes the received bytes + * @return true if the KMS request should be retried + */ + boolean feedWithRetry(ByteBuffer bytes); + + /** + * Signal to libmongocrypt that a network error occurred on this KMS request. + * + * @return true if the request should be retried, false if retries are exhausted + */ + boolean fail(); } diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptorImpl.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptorImpl.java index 1411adffc2..24b5e3cc67 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptorImpl.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoKeyDecryptorImpl.java @@ -22,6 +22,7 @@ import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_t; import com.mongodb.internal.crypt.capi.CAPI.mongocrypt_status_t; import com.sun.jna.Pointer; +import com.sun.jna.ptr.ByteByReference; import com.sun.jna.ptr.PointerByReference; import java.nio.ByteBuffer; @@ -30,10 +31,13 @@ import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_binary_new; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_bytes_needed; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_endpoint; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_fail; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_feed; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_feed_with_retry; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_get_kms_provider; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_message; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_status; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_kms_ctx_usleep; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_status_code; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_status_destroy; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_status_message; @@ -96,6 +100,28 @@ public void feed(final ByteBuffer bytes) { } } + @Override + public long sleepMicroseconds() { + return mongocrypt_kms_ctx_usleep(wrapped); + } + + @Override + public boolean feedWithRetry(final ByteBuffer bytes) { + try (BinaryHolder binaryHolder = toBinary(bytes)) { + ByteByReference shouldRetry = new ByteByReference(); + boolean success = mongocrypt_kms_ctx_feed_with_retry(wrapped, binaryHolder.getBinary(), shouldRetry); + if (!success) { + throwExceptionFromStatus(); + } + return shouldRetry.getValue() != 0; + } + } + + @Override + public boolean fail() { + return mongocrypt_kms_ctx_fail(wrapped); + } + private void throwExceptionFromStatus() { mongocrypt_status_t status = mongocrypt_status_new(); mongocrypt_kms_ctx_status(wrapped, status); 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 14bb2a5ccd..e6b7d36865 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 @@ -49,10 +49,12 @@ import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @SuppressWarnings("SameParameterValue") @@ -313,6 +315,87 @@ public void testExplicitEncryptionDecryptionKeyAltName() throws IOException, URI mongoCrypt.close(); } + @Test + public void testKmsRetryOnHttpErrorAndNetworkError() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + MongoCryptContext decryptor = mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json")); + MongoKeyDecryptor keyDecryptor = feedKeysAndGetKeyDecryptor(decryptor); + + // No backoff is requested before the first attempt + assertEquals(0, keyDecryptor.sleepMicroseconds()); + + // A complete HTTP 429 response marks the request as retryable. The backoff is jittered, + // so it may legitimately be zero; only that the call succeeds can be asserted. + assertTrue(keyDecryptor.feedWithRetry(getHttpResourceAsByteBuffer("kms-reply-429.txt"))); + keyDecryptor.sleepMicroseconds(); + + // The context stays in NEED_KMS and the key decryptor is re-presented for the retry + keyDecryptor = reenterNeedKms(decryptor); + assertTrue(keyDecryptor.bytesNeeded() > 0); + assertTrue(keyDecryptor.getMessage().remaining() > 0); + + // A network error is retryable while budget remains + assertTrue(keyDecryptor.fail()); + keyDecryptor = reenterNeedKms(decryptor); + + // A successful response completes the key decryption + assertFalse(keyDecryptor.feedWithRetry(getHttpResourceAsByteBuffer("kms-reply.txt"))); + assertEquals(0, keyDecryptor.bytesNeeded()); + + assertNull(decryptor.nextKeyDecryptor()); + decryptor.completeKeyDecryptors(); + assertEquals(State.READY, decryptor.getState()); + + RawBsonDocument decryptedDocument = decryptor.finish(); + assertEquals(State.DONE, decryptor.getState()); + assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument); + + decryptor.close(); + mongoCrypt.close(); + } + + @Test + public void testKmsRetryExhaustsBudget() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + MongoCryptContext decryptor = mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json")); + MongoKeyDecryptor keyDecryptor = feedKeysAndGetKeyDecryptor(decryptor); + + int retriesAllowed = 0; + while (retriesAllowed < 100 && keyDecryptor.fail()) { + retriesAllowed++; + } + assertTrue(retriesAllowed > 0, "expected at least one retry to be allowed"); + assertTrue(retriesAllowed < 100, "expected the retry budget to be exhausted"); + + decryptor.close(); + mongoCrypt.close(); + } + + private MongoKeyDecryptor reenterNeedKms(final MongoCryptContext context) { + assertEquals(State.NEED_KMS, context.getState()); + MongoKeyDecryptor keyDecryptor = context.nextKeyDecryptor(); + assertNotNull(keyDecryptor); + return keyDecryptor; + } + + private MongoKeyDecryptor feedKeysAndGetKeyDecryptor(final MongoCryptContext context) { + assertEquals(State.NEED_MONGO_KEYS, context.getState()); + BsonDocument keyFilter = context.getMongoOperation(); + assertEquals(getResourceAsDocument("key-filter.json"), keyFilter); + context.addMongoOperationResult(getResourceAsDocument("key-document.json")); + context.completeMongoOperation(); + assertEquals(State.NEED_KMS, context.getState()); + + MongoKeyDecryptor keyDecryptor = context.nextKeyDecryptor(); + assertNotNull(keyDecryptor); + assertEquals("aws", keyDecryptor.getKmsProvider()); + return keyDecryptor; + } + private void testKeyDecryptor(final MongoCryptContext context) { testKeyDecryptor(context, "key-filter.json", "key-document.json"); } diff --git a/mongodb-crypt/src/test/java/com/mongodb/internal/crypt/capi/CstringTest.java b/mongodb-crypt/src/test/java/com/mongodb/internal/crypt/capi/CstringTest.java new file mode 100644 index 0000000000..12e1f6939c --- /dev/null +++ b/mongodb-crypt/src/test/java/com/mongodb/internal/crypt/capi/CstringTest.java @@ -0,0 +1,68 @@ +/* + * 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.internal.crypt.capi; + +import com.mongodb.internal.crypt.capi.CAPI.cstring; +import com.sun.jna.Pointer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CstringTest { + + // A native function declared to return a cstring (e.g. mongocrypt_status_message) may return a + // NULL pointer. By default JNA would marshal that to a null cstring reference, causing callers + // that immediately call toString() to throw a NullPointerException. The fromNative override must + // return a non-null cstring instead. + @Test + void fromNativeReturnsEmptyStringBackedInstanceForNullPointer() { + cstring result = (cstring) new cstring().fromNative(null, null); + + assertNotNull(result); + assertEquals("", result.toString()); + } + + @Test + void fromNativeWrapsNonNullPointer() { + cstring source = new cstring("hello world"); + + cstring result = (cstring) new cstring().fromNative(source.getPointer(), null); + + assertNotNull(result); + assertEquals("hello world", result.toString()); + } + + @Test + void toStringReturnsEmptyStringForNullPointer() { + assertEquals("", new cstring().toString()); + } + + @Test + void toStringReturnsEmptyStringForExplicitNullPointer() { + cstring value = new cstring(); + value.setPointer(Pointer.NULL); + + assertEquals("", value.toString()); + } + + @Test + void toStringReadsBackConstructedValue() { + assertEquals("a value", new cstring("a value").toString()); + } +} diff --git a/mongodb-crypt/src/test/resources/kms-reply-429.txt b/mongodb-crypt/src/test/resources/kms-reply-429.txt new file mode 100644 index 0000000000..e9fcc56d51 --- /dev/null +++ b/mongodb-crypt/src/test/resources/kms-reply-429.txt @@ -0,0 +1,6 @@ +HTTP/1.1 429 Too Many Requests +x-amzn-RequestId: 00000000-0000-0000-0000-000000000000 +Content-Type: application/x-amz-json-1.1 +Content-Length: 32 + +{"__type":"ThrottlingException"}