From df2a759cc3c46329f29a0d6dc32b1ace09cf5c8d Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Thu, 11 Jun 2026 09:10:29 +0100
Subject: [PATCH 1/6] Updated CAPI.java javadocs to their 1.18.1 mongocrypt.h
equivalents and documented the rules in AGENTS.md
---
mongodb-crypt/AGENTS.md | 29 +
.../com/mongodb/internal/crypt/capi/CAPI.java | 731 +++++++++---------
2 files changed, 415 insertions(+), 345 deletions(-)
diff --git a/mongodb-crypt/AGENTS.md b/mongodb-crypt/AGENTS.md
index bf9737febf7..7c5793b2116 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 41cc8ced31b..521037057ea 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
@@ -52,16 +52,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 +71,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 +96,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 +122,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 +167,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 +175,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 +213,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 +230,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 +264,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 +327,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 +367,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 +391,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 +404,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 +431,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 +451,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 +466,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 +517,106 @@ 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);
/**
* 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 +625,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.
*
- * @param crypt The @ref mongocrypt_t object.
- * @return A boolean indicating success. Failure may occur if previously set options are invalid.
+ *
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 {@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 +718,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 +728,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 +779,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 +795,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 +830,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 +843,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 +857,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 +881,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 +895,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 +951,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 +1027,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 +1063,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 +1075,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 +1100,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 +1116,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 +1130,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 +1170,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 +1182,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 +1194,35 @@ 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);
/**
- * Get the status associated with a @ref mongocrypt_kms_ctx_t object.
+ * Get the status associated with a {@link mongocrypt_kms_ctx_t} object.
*
- * @param kms The @ref 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 +1231,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 +1240,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);
From 5bff2aea9500357c532571b73b7d44c9581fbfca Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Wed, 17 Jun 2026 15:22:25 +0100
Subject: [PATCH 2/6] Add libmongocrypt KMS retry bindings
Expose libmongocrypt's KMS retry primitives so transient KMS failures
can be retried with library-supplied backoff delays.
- Add CAPI bindings: mongocrypt_setopt_retry_kms,
mongocrypt_kms_ctx_usleep, mongocrypt_kms_ctx_feed_with_retry and
mongocrypt_kms_ctx_fail
- Expose them via sleepMicroseconds(), feedWithRetry() and fail() on
MongoKeyDecryptor, and enable retry_kms unconditionally in
MongoCryptImpl
- HTTP 429 responses are signalled by feedWithRetry and network errors
by fail(); in both cases the attempt ends early and libmongocrypt
re-presents the KMS context for retry with the next backoff delay
- Add mongodb-crypt unit tests for the new bindings and a 429 KMS
reply fixture
JAVA-5391
---
.../com/mongodb/internal/crypt/capi/CAPI.java | 50 +++++++++++
.../internal/crypt/capi/MongoCryptImpl.java | 4 +
.../crypt/capi/MongoKeyDecryptor.java | 25 ++++++
.../crypt/capi/MongoKeyDecryptorImpl.java | 26 ++++++
.../mongodb/crypt/capi/MongoCryptTest.java | 83 +++++++++++++++++++
.../src/test/resources/kms-reply-429.txt | 6 ++
6 files changed, 194 insertions(+)
create mode 100644 mongodb-crypt/src/test/resources/kms-reply-429.txt
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 521037057ea..eafc0e50828 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
@@ -22,6 +22,7 @@
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
@@ -525,6 +526,18 @@ public interface mongocrypt_random_fn extends Callback {
public static native void
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.
*
@@ -1205,6 +1218,43 @@ public interface mongocrypt_random_fn extends Callback {
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);
+
+ /**
+ * 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 {@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.
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 774b9e718cb..e4d0a74e1d6 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 9b0eae6776f..6bb6a348067 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 1411adffc21..24b5e3cc67b 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 14bb2a5ccdc..e6b7d36865f 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/resources/kms-reply-429.txt b/mongodb-crypt/src/test/resources/kms-reply-429.txt
new file mode 100644
index 00000000000..e9fcc56d51b
--- /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"}
From 68115e87e2b74346c352f03730ec592708292f3a Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Wed, 17 Jun 2026 15:22:39 +0100
Subject: [PATCH 3/6] Retry KMS requests on transient errors
Wire libmongocrypt's KMS retry behaviour through the sync and reactive
drivers. Transient KMS failures are retried with backoff delays
supplied by libmongocrypt; retry is enabled unconditionally.
- The sync Crypt sleeps the backoff before each attempt, surfaces
MongoOperationTimeoutException only once the CSOT deadline has
actually expired (a fixed-timeout connect or handshake stall with
budget remaining stays retryable) and treats unexpected EOF as a
retryable network error
- The reactive KeyManagementService applies the backoff via
Mono.delay, checks the CSOT deadline before consuming retry budget,
and releases buffers and closes streams on all completion paths;
the shared backoff budget check lives in MongoCryptHelper
- Add the spec Section 24 KMS retry prose tests (sync and reactive)
plus a CSOT-during-retry test; each test resets the failpoint
server and control requests send Connection: close so the
single-threaded server cannot stall KMS connections or leak armed
failpoints between tests and suites
- Add a sync unit test covering the CSOT classification of KMS network
failures, and an Evergreen task running the KMS retry suites
JAVA-5391
---
.evergreen/.evg.yml | 27 ++
.evergreen/run-kms-retry-tests.sh | 43 +++
.../internal/capi/MongoCryptHelper.java | 28 ++
.../internal/crypt/KeyManagementService.java | 167 ++++++---
...ClientSideEncryptionKmsRetryProseTest.java | 30 ++
.../KeyManagementServiceKmsRetryTest.java | 130 +++++++
.../com/mongodb/client/internal/Crypt.java | 68 +++-
...ClientSideEncryptionKmsRetryProseTest.java | 327 ++++++++++++++++++
...ClientSideEncryptionKmsRetryProseTest.java | 28 ++
.../client/internal/CryptKmsRetryTest.java | 100 ++++++
10 files changed, 896 insertions(+), 52 deletions(-)
create mode 100755 .evergreen/run-kms-retry-tests.sh
create mode 100644 driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionKmsRetryProseTest.java
create mode 100644 driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementServiceKmsRetryTest.java
create mode 100644 driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java
create mode 100644 driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java
create mode 100644 driver-sync/src/test/unit/com/mongodb/client/internal/CryptKmsRetryTest.java
diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml
index 7de253baac6..b0772260acd 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 00000000000..5f9e577a497
--- /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 240f5051c92..f29e32ee99c 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 67ebf421c9c..814825edde9 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 00000000000..1dd4f0f833d
--- /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 00000000000..c35cba2a792
--- /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 67fac13770c..11049ed2208 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 00000000000..2b3360fcc38
--- /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 00000000000..7b51bf915e5
--- /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 00000000000..b40b9887d99
--- /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();
+ }
+}
From 226b0f771f18c09872cfa3206acfdb2cc315c53b Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Wed, 17 Jun 2026 17:17:20 +0100
Subject: [PATCH 4/6] Harden cstring binding against NULL native returns
Native functions declared to return a cstring (e.g. mongocrypt_status_message)
may return a NULL pointer. By default JNA marshals that to a null cstring
reference, so call sites that immediately call toString() would throw a
NullPointerException.
Override cstring.fromNative to return a cstring with a null pointer rather than
a null reference, and have toString normalize a null pointer to an empty string.
This makes the binding's documented NULL contract safe for existing callers
without changing each call site.
---
.../internal/crypt/KeyManagementService.java | 10 ++-
.../com/mongodb/internal/crypt/capi/CAPI.java | 17 ++++-
.../internal/crypt/capi/CstringTest.java | 68 +++++++++++++++++++
3 files changed, 91 insertions(+), 4 deletions(-)
create mode 100644 mongodb-crypt/src/test/java/com/mongodb/internal/crypt/capi/CstringTest.java
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 814825edde9..6c0be0db624 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
@@ -27,6 +27,7 @@
import com.mongodb.connection.SslSettings;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.TimeoutSettings;
+import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.connection.AsynchronousChannelStream;
import com.mongodb.internal.connection.DefaultInetAddressResolver;
import com.mongodb.internal.connection.OperationContext;
@@ -37,10 +38,10 @@
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.time.Timeout;
+import com.mongodb.lang.NonNull;
import com.mongodb.lang.Nullable;
import org.bson.ByteBuf;
import org.bson.ByteBufNIO;
-import com.mongodb.lang.NonNull;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
@@ -53,6 +54,7 @@
import java.util.List;
import java.util.Map;
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
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;
@@ -69,6 +71,7 @@ class KeyManagementService implements Closeable {
this(kmsProviderSslContextMap, timeoutMillis, new TlsChannelStreamFactoryFactory(new DefaultInetAddressResolver()));
}
+ @VisibleForTesting(otherwise = PRIVATE)
KeyManagementService(final Map kmsProviderSslContextMap, final int timeoutMillis,
final TlsChannelStreamFactoryFactory tlsChannelStreamFactoryFactory) {
assertTrue("timeoutMillis > 0", timeoutMillis > 0);
@@ -264,8 +267,9 @@ private static TimeoutSettings createTimeoutSettings(final SocketSettings socket
}
private Throwable unWrapException(final Throwable 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.
+ // openAsync wraps IOException in MongoSocketOpenException; unwrap it to match the sync path, which throws IOException directly.
+ // Timeout subclasses (MongoSocketReadTimeoutException, MongoSocketWriteTimeoutException) carry meaningful type identity
+ // used by isTimeoutException(), so preserve them rather than unwrapping.
if (t instanceof MongoSocketReadTimeoutException || t instanceof MongoSocketWriteTimeoutException) {
return t;
}
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 eafc0e50828..f600f91e58b 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,6 +18,7 @@
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;
@@ -44,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);
}
}
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 00000000000..12e1f6939c3
--- /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());
+ }
+}
From 26152ec48939c270b7d447d103d9dd03c2882d47 Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Mon, 27 Jul 2026 09:31:48 +0100
Subject: [PATCH 5/6] Update
driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java
Co-authored-by: Valentin Kovalenko
---
.../mongodb/internal/capi/MongoCryptHelper.java | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
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 f29e32ee99c..dd5c65dfd85 100644
--- a/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java
@@ -67,17 +67,9 @@ public static void checkKmsRetryBackoff(@Nullable final Timeout operationTimeout
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);
- });
+ operationTimeout.shortenBy(backoffMicros, TimeUnit.MICROSECONDS).onExpired(() -> {
+ throw TimeoutContext.createMongoTimeoutException(KMS_TIMEOUT_ERROR_MESSAGE);
+ });
}
public static MongoCryptOptions createMongoCryptOptions(final ClientEncryptionSettings settings) {
From 9dbeaa0d62de09ce85eade8399bfb86aecd4d50f Mon Sep 17 00:00:00 2001
From: Ross Lawley
Date: Mon, 27 Jul 2026 11:08:42 +0100
Subject: [PATCH 6/6] PR review updates
Co-authored-by: Valentin Kovalenko
---
.agents/references/testing-guide.md | 4 +
.evergreen/run-kms-retry-tests.sh | 4 +-
.../internal/capi/MongoCryptHelper.java | 3 +-
...ClientSideEncryptionKmsRetryProseTest.java | 5 +-
...ClientSideEncryptionKmsRetryProseTest.java | 93 +---
...ClientSideEncryptionKmsRetryProseTest.java | 5 +-
.../internal/crypt/capi/MongoCryptImpl.java | 1 -
.../mongodb/crypt/capi/MongoCryptTest.java | 417 ++++++++----------
.../internal/crypt/capi/CstringTest.java | 13 +-
9 files changed, 235 insertions(+), 310 deletions(-)
diff --git a/.agents/references/testing-guide.md b/.agents/references/testing-guide.md
index 1679150291c..80bb5f4dfd0 100644
--- a/.agents/references/testing-guide.md
+++ b/.agents/references/testing-guide.md
@@ -28,6 +28,10 @@ description: Testing frameworks, conventions, and commands for the MongoDB Java
- Unit tests must not require a running MongoDB instance
- Descriptive method names: `shouldReturnEmptyListWhenNoDocumentsMatch()` not `test1()`
- Use `@DisplayName` for human-readable names
+- JUnit 5 does not require `public` visibility: declare test classes and their `@Test`,
+ `@ParameterizedTest`, `@BeforeEach`/`@AfterEach`/`@BeforeAll`/`@AfterAll` methods as
+ package-private (no modifier). Reserve `public`/`protected` for members a subclass or another
+ package genuinely needs to access (e.g. an abstract base test's `protected` hook methods)
- Clean up test data in `@AfterEach` / `cleanup()` to prevent pollution
- Import types at the top of the file — do not use inline fully-qualified names
(`com.mongodb.connection.AsyncCompletionHandler`, `java.util.List`) in signatures or bodies
diff --git a/.evergreen/run-kms-retry-tests.sh b/.evergreen/run-kms-retry-tests.sh
index 5f9e577a497..eb160b594b7 100755
--- a/.evergreen/run-kms-retry-tests.sh
+++ b/.evergreen/run-kms-retry-tests.sh
@@ -23,13 +23,13 @@ export GRADLE_EXTRA_VARS="-Pssl.enabled=true -Pssl.trustStoreType=jks -Pssl.trus
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" \
+ -Dorg.mongodb.test.kms.retry.run="true" \
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" \
+ -Dorg.mongodb.test.kms.retry.run="true" \
driver-reactive-streams:cleanTest driver-reactive-streams:test --tests ClientSideEncryptionKmsRetryProseTest
second=$?
echo "reactive exit code: $second"
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 dd5c65dfd85..3cebc2cffda 100644
--- a/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/capi/MongoCryptHelper.java
@@ -23,6 +23,7 @@
import com.mongodb.MongoClientException;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoConfigurationException;
+import com.mongodb.MongoOperationTimeoutException;
import com.mongodb.client.model.vault.RewrapManyDataKeyOptions;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.authentication.AwsCredentialHelper;
@@ -57,7 +58,7 @@ 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
+ * Throws a {@link 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
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
index 1dd4f0f833d..05411b5a507 100644
--- 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
@@ -22,9 +22,10 @@
import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption;
import com.mongodb.reactivestreams.client.vault.ClientEncryptions;
-public class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest {
+class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest {
+
@Override
- public ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) {
+ protected ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) {
return new SyncClientEncryption(ClientEncryptions.create(settings));
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java
index 2b3360fcc38..3fb32ef1006 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsRetryProseTest.java
@@ -21,7 +21,6 @@
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;
@@ -34,16 +33,10 @@
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;
@@ -53,8 +46,8 @@
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static com.mongodb.client.Fixture.getMongoClient;
import static com.mongodb.client.Fixture.getMongoClientSettings;
+import static java.lang.Boolean.parseBoolean;
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;
@@ -62,24 +55,18 @@
* 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.
+ *
Requires the {@value RUN_TESTS_SYSTEM_PROPERTY_NAME} system property to be set to {@code "true"}.
*/
public abstract class AbstractClientSideEncryptionKmsRetryProseTest {
-
+ private static final String RUN_TESTS_SYSTEM_PROPERTY_NAME = "org.mongodb.test.kms.retry.run";
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");
+ void setUp() {
+ assumeTrue(parseBoolean(System.getProperty(RUN_TESTS_SYSTEM_PROPERTY_NAME)));
assumeTrue(hasEncryptionTestsEnabled());
assumeTrue(serverVersionAtLeast(4, 2));
resetFailpoints();
@@ -87,13 +74,11 @@ public void setUp() {
}
@AfterEach
- public void tearDown() {
+ 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()) {
+ if (!parseBoolean(System.getProperty(RUN_TESTS_SYSTEM_PROPERTY_NAME)) || !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();
}
@@ -102,7 +87,7 @@ public void tearDown() {
*/
@ParameterizedTest(name = "Case 1: TCP retry with {0}")
@ValueSource(strings = {"aws", "azure", "gcp"})
- public void testCreateDataKeyAndEncryptWithTcpRetry(final String provider) {
+ void testCreateDataKeyAndEncryptWithTcpRetry(final String provider) {
try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) {
setFailpoint("network", 1);
BsonBinary keyId = assertDoesNotThrow(
@@ -120,7 +105,7 @@ public void testCreateDataKeyAndEncryptWithTcpRetry(final String provider) {
*/
@ParameterizedTest(name = "Case 2: HTTP retry with {0}")
@ValueSource(strings = {"aws", "azure", "gcp"})
- public void testCreateDataKeyAndEncryptWithHttpRetry(final String provider) {
+ void testCreateDataKeyAndEncryptWithHttpRetry(final String provider) {
try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) {
setFailpoint("http", 1);
BsonBinary keyId = assertDoesNotThrow(
@@ -138,7 +123,7 @@ public void testCreateDataKeyAndEncryptWithHttpRetry(final String provider) {
*/
@ParameterizedTest(name = "Case 3: Exhausted retries with {0}")
@ValueSource(strings = {"aws", "azure", "gcp"})
- public void testCreateDataKeyFailsAfterTooManyRetries(final String provider) {
+ void testCreateDataKeyFailsAfterTooManyRetries(final String provider) {
try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest()) {
setFailpoint("network", 4);
assertThrows(MongoClientException.class,
@@ -152,7 +137,7 @@ public void testCreateDataKeyFailsAfterTooManyRetries(final String provider) {
* backoff normally pushes the operation past its deadline.
*/
@Test
- public void testCreateDataKeyTimesOutDuringRetry() {
+ void testCreateDataKeyTimesOutDuringRetry() {
try (ClientEncryption clientEncryption = createClientEncryptionForRetryTest(100L)) {
setFailpoint("network", 4);
// The 100ms deadline races libmongocrypt's jittered retry backoff: usually the deadline
@@ -170,18 +155,10 @@ private ClientEncryption createClientEncryptionForRetryTest() {
}
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);
+ .kmsProviders(getKmsProvidersForRetryTest());
if (timeoutMS != null) {
builder.timeout(timeoutMS, TimeUnit.MILLISECONDS);
}
@@ -244,25 +221,18 @@ 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
+ // self-signed cert, hostname verification intentionally disabled
connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
@@ -279,11 +249,14 @@ private static void postToFailpointServer(final String path, final String body)
try (OutputStream os = connection.getOutputStream()) {
os.write(bodyBytes);
+ os.flush();
}
int responseCode = connection.getResponseCode();
- assertEquals(200, responseCode,
- "Failpoint server request to " + path + " failed, HTTP status: " + responseCode);
+ if (responseCode != 200) {
+ throw new RuntimeException("Failpoint server request to " + path + " with body " + body
+ + " failed, HTTP status: " + responseCode);
+ }
try (InputStream is = connection.getInputStream()) {
while (is.read() != -1) {
// drain the response so the exchange completes cleanly
@@ -293,35 +266,7 @@ private static void postToFailpointServer(final String path, final String body)
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);
+ throw new RuntimeException("Failpoint server request to " + path + " with body " + body + " failed", 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
index 7b51bf915e5..0f7694c3a14 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionKmsRetryProseTest.java
@@ -20,9 +20,10 @@
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions;
-public class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest {
+class ClientSideEncryptionKmsRetryProseTest extends AbstractClientSideEncryptionKmsRetryProseTest {
+
@Override
- public ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) {
+ protected ClientEncryption getClientEncryption(final ClientEncryptionSettings settings) {
return ClientEncryptions.create(settings);
}
}
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 e4d0a74e1d6..5731ca20689 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
@@ -199,7 +199,6 @@ 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) {
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 e6b7d36865f..e4a43208266 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
@@ -61,61 +61,50 @@
public class MongoCryptTest {
@Test
public void testEncrypt() throws URISyntaxException, IOException {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- MongoCryptContext encryptor = mongoCrypt.createEncryptionContext("test", getResourceAsDocument("command.json"));
-
- assertEquals(State.NEED_MONGO_COLLINFO, encryptor.getState());
-
- BsonDocument listCollectionsFilter = encryptor.getMongoOperation();
- assertEquals(getResourceAsDocument("list-collections-filter.json"), listCollectionsFilter);
-
- encryptor.addMongoOperationResult(getResourceAsDocument("collection-info.json"));
- encryptor.completeMongoOperation();
- assertEquals(State.NEED_MONGO_MARKINGS, encryptor.getState());
+ try (MongoCrypt mongoCrypt = createMongoCrypt();
+ MongoCryptContext encryptor = mongoCrypt.createEncryptionContext("test", getResourceAsDocument("command.json"))) {
+ assertEquals(State.NEED_MONGO_COLLINFO, encryptor.getState());
- BsonDocument jsonSchema = encryptor.getMongoOperation();
- assertEquals(getResourceAsDocument("mongocryptd-command.json"), jsonSchema);
+ BsonDocument listCollectionsFilter = encryptor.getMongoOperation();
+ assertEquals(getResourceAsDocument("list-collections-filter.json"), listCollectionsFilter);
- encryptor.addMongoOperationResult(getResourceAsDocument("mongocryptd-reply.json"));
- encryptor.completeMongoOperation();
- assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
+ encryptor.addMongoOperationResult(getResourceAsDocument("collection-info.json"));
+ encryptor.completeMongoOperation();
+ assertEquals(State.NEED_MONGO_MARKINGS, encryptor.getState());
- testKeyDecryptor(encryptor);
+ BsonDocument jsonSchema = encryptor.getMongoOperation();
+ assertEquals(getResourceAsDocument("mongocryptd-command.json"), jsonSchema);
- assertEquals(State.READY, encryptor.getState());
+ encryptor.addMongoOperationResult(getResourceAsDocument("mongocryptd-reply.json"));
+ encryptor.completeMongoOperation();
+ assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
- RawBsonDocument encryptedDocument = encryptor.finish();
- assertEquals(State.DONE, encryptor.getState());
- assertEquals(getResourceAsDocument("encrypted-command.json"), encryptedDocument);
+ testKeyDecryptor(encryptor);
- encryptor.close();
+ assertEquals(State.READY, encryptor.getState());
- mongoCrypt.close();
+ RawBsonDocument encryptedDocument = encryptor.finish();
+ assertEquals(State.DONE, encryptor.getState());
+ assertEquals(getResourceAsDocument("encrypted-command.json"), encryptedDocument);
+ }
}
@Test
public void testDecrypt() throws IOException, URISyntaxException {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- MongoCryptContext decryptor = mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json"));
+ try (MongoCrypt mongoCrypt = createMongoCrypt();
+ MongoCryptContext decryptor =
+ mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json"))) {
+ assertEquals(State.NEED_MONGO_KEYS, decryptor.getState());
- assertEquals(State.NEED_MONGO_KEYS, decryptor.getState());
-
- testKeyDecryptor(decryptor);
-
- assertEquals(State.READY, decryptor.getState());
+ testKeyDecryptor(decryptor);
- RawBsonDocument decryptedDocument = decryptor.finish();
- assertEquals(State.DONE, decryptor.getState());
- assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument);
+ assertEquals(State.READY, decryptor.getState());
- decryptor.close();
-
- mongoCrypt.close();
+ RawBsonDocument decryptedDocument = decryptor.finish();
+ assertEquals(State.DONE, decryptor.getState());
+ assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument);
+ }
}
@Test
@@ -144,7 +133,6 @@ public void testEmptyAwsCredentials() throws URISyntaxException, IOException {
@Test
public void testMultipleCloseCalls() {
MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
mongoCrypt.close();
mongoCrypt.close();
@@ -152,237 +140,218 @@ public void testMultipleCloseCalls() {
@Test
public void testDataKeyCreation() {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
List keyAltNames = Arrays.asList("first", "second");
- MongoCryptContext dataKeyContext = mongoCrypt.createDataKeyContext("local",
- MongoDataKeyOptions.builder().masterKey(new BsonDocument())
- .keyAltNames(keyAltNames)
- .build());
- assertEquals(State.READY, dataKeyContext.getState());
-
- RawBsonDocument dataKeyDocument = dataKeyContext.finish();
- assertEquals(State.DONE, dataKeyContext.getState());
- assertNotNull(dataKeyDocument);
-
- List actualKeyAltNames = dataKeyDocument.getArray("keyAltNames").stream()
- .map(bsonValue -> bsonValue.asString().getValue())
- .sorted()
- .collect(Collectors.toList());
- assertIterableEquals(keyAltNames, actualKeyAltNames);
- dataKeyContext.close();
- mongoCrypt.close();
+ try (MongoCrypt mongoCrypt = createMongoCrypt();
+ MongoCryptContext dataKeyContext = mongoCrypt.createDataKeyContext("local",
+ MongoDataKeyOptions.builder().masterKey(new BsonDocument())
+ .keyAltNames(keyAltNames)
+ .build())) {
+ assertEquals(State.READY, dataKeyContext.getState());
+
+ RawBsonDocument dataKeyDocument = dataKeyContext.finish();
+ assertEquals(State.DONE, dataKeyContext.getState());
+ assertNotNull(dataKeyDocument);
+
+ List actualKeyAltNames = dataKeyDocument.getArray("keyAltNames").stream()
+ .map(bsonValue -> bsonValue.asString().getValue())
+ .sorted()
+ .collect(Collectors.toList());
+ assertIterableEquals(keyAltNames, actualKeyAltNames);
+ }
}
@Test
public void testExplicitEncryptionDecryption() {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello"));
- MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
- .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ==")))
- .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
- .build();
- MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options);
- assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
-
- testKeyDecryptor(encryptor);
-
- assertEquals(State.READY, encryptor.getState());
-
- RawBsonDocument encryptedDocument = encryptor.finish();
- assertEquals(State.DONE, encryptor.getState());
- assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument);
-
- MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument);
-
- assertEquals(State.READY, decryptor.getState());
-
- RawBsonDocument decryptedDocument = decryptor.finish();
- assertEquals(State.DONE, decryptor.getState());
- assertEquals(documentToEncrypt, decryptedDocument);
+ try (MongoCrypt mongoCrypt = createMongoCrypt()) {
+ BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello"));
+ MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
+ .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ==")))
+ .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
+ .build();
+ RawBsonDocument encryptedDocument;
+ try (MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options)) {
+ assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
+
+ testKeyDecryptor(encryptor);
+
+ assertEquals(State.READY, encryptor.getState());
+
+ encryptedDocument = encryptor.finish();
+ assertEquals(State.DONE, encryptor.getState());
+ assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument);
+ }
- encryptor.close();
+ try (MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument)) {
+ assertEquals(State.READY, decryptor.getState());
- mongoCrypt.close();
+ RawBsonDocument decryptedDocument = decryptor.finish();
+ assertEquals(State.DONE, decryptor.getState());
+ assertEquals(documentToEncrypt, decryptedDocument);
+ }
+ }
}
@Test
public void testExplicitExpressionEncryption() {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
- BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
- BsonDocument expectedEncryptedPayload = getResourceAsDocument("fle2-find-range-explicit-v2/int32/encrypted-payload.json");
-
- MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
- .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
- .algorithm("Range")
- .queryType("range")
- .contentionFactor(4L)
- .rangeOptions(rangeOptions)
- .build();
- MongoCryptContext encryptor = mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options);
- assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
-
- testKeyDecryptor(encryptor, "fle2-find-range-explicit-v2/int32/key-filter.json", "keys/ABCDEFAB123498761234123456789012-local-document.json");
-
- assertEquals(State.READY, encryptor.getState());
-
- RawBsonDocument actualEncryptedPayload = encryptor.finish();
- assertEquals(State.DONE, encryptor.getState());
- assertEquals(expectedEncryptedPayload, actualEncryptedPayload);
-
- encryptor.close();
- mongoCrypt.close();
+ try (MongoCrypt mongoCrypt = createMongoCrypt()) {
+ BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
+ BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
+ BsonDocument expectedEncryptedPayload = getResourceAsDocument("fle2-find-range-explicit-v2/int32/encrypted-payload.json");
+
+ MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
+ .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
+ .algorithm("Range")
+ .queryType("range")
+ .contentionFactor(4L)
+ .rangeOptions(rangeOptions)
+ .build();
+ try (MongoCryptContext encryptor = mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options)) {
+ assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
+
+ testKeyDecryptor(encryptor, "fle2-find-range-explicit-v2/int32/key-filter.json",
+ "keys/ABCDEFAB123498761234123456789012-local-document.json");
+
+ assertEquals(State.READY, encryptor.getState());
+
+ RawBsonDocument actualEncryptedPayload = encryptor.finish();
+ assertEquals(State.DONE, encryptor.getState());
+ assertEquals(expectedEncryptedPayload, actualEncryptedPayload);
+ }
+ }
}
@Test
public void testRangePreviewQueryTypeIsNotSupported() {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
- BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
-
- MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
- .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
- .algorithm("Range")
- .queryType("rangePreview")
- .contentionFactor(4L)
- .rangeOptions(rangeOptions)
- .build();
-
- MongoCryptException exp = assertThrows(MongoCryptException.class, () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options));
- assertEquals("Query type 'rangePreview' is deprecated, please use 'range'", exp.getMessage());
- mongoCrypt.close();
+ try (MongoCrypt mongoCrypt = createMongoCrypt()) {
+ BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
+ BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
+
+ MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
+ .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
+ .algorithm("Range")
+ .queryType("rangePreview")
+ .contentionFactor(4L)
+ .rangeOptions(rangeOptions)
+ .build();
+
+ MongoCryptException exp = assertThrows(MongoCryptException.class,
+ () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options));
+ assertEquals("Query type 'rangePreview' is deprecated, please use 'range'", exp.getMessage());
+ }
}
@Test
public void testRangePreviewAlgorithmIsNotSupported() {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
- BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
-
- MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
- .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
- .algorithm("RangePreview")
- .rangeOptions(rangeOptions)
- .build();
-
- MongoCryptException exp = assertThrows(MongoCryptException.class, () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options));
- assertEquals("Algorithm 'rangePreview' is deprecated, please use 'range'", exp.getMessage());
- mongoCrypt.close();
+ try (MongoCrypt mongoCrypt = createMongoCrypt()) {
+ BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json");
+ BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json");
+
+ MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
+ .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg==")))
+ .algorithm("RangePreview")
+ .rangeOptions(rangeOptions)
+ .build();
+
+ MongoCryptException exp = assertThrows(MongoCryptException.class,
+ () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options));
+ assertEquals("Algorithm 'rangePreview' is deprecated, please use 'range'", exp.getMessage());
+ }
}
@Test
public void testExplicitEncryptionDecryptionKeyAltName() throws IOException, URISyntaxException {
- MongoCrypt mongoCrypt = createMongoCrypt();
- assertNotNull(mongoCrypt);
-
- BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello"));
- MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
- .keyAltName("altKeyName")
- .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
- .build();
- MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options);
-
- assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
- testKeyDecryptor(encryptor, "key-filter-keyAltName.json", "key-document.json");
-
- assertEquals(State.READY, encryptor.getState());
-
- RawBsonDocument encryptedDocument = encryptor.finish();
- assertEquals(State.DONE, encryptor.getState());
- assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument);
-
- MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument);
-
- assertEquals(State.READY, decryptor.getState());
-
- RawBsonDocument decryptedDocument = decryptor.finish();
- assertEquals(State.DONE, decryptor.getState());
- assertEquals(documentToEncrypt, decryptedDocument);
+ try (MongoCrypt mongoCrypt = createMongoCrypt()) {
+ BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello"));
+ MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder()
+ .keyAltName("altKeyName")
+ .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
+ .build();
+
+ RawBsonDocument encryptedDocument;
+ try (MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options)) {
+ assertEquals(State.NEED_MONGO_KEYS, encryptor.getState());
+ testKeyDecryptor(encryptor, "key-filter-keyAltName.json", "key-document.json");
+
+ assertEquals(State.READY, encryptor.getState());
+
+ encryptedDocument = encryptor.finish();
+ assertEquals(State.DONE, encryptor.getState());
+ assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument);
+ }
- encryptor.close();
+ try (MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument)) {
+ assertEquals(State.READY, decryptor.getState());
- mongoCrypt.close();
+ RawBsonDocument decryptedDocument = decryptor.finish();
+ assertEquals(State.DONE, decryptor.getState());
+ assertEquals(documentToEncrypt, decryptedDocument);
+ }
+ }
}
@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());
+ try (MongoCrypt mongoCrypt = createMongoCrypt();
+ MongoCryptContext decryptor =
+ mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json"))) {
+ MongoKeyDecryptor keyDecryptor = feedKeysAndGetKeyDecryptor(decryptor);
- // 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();
+ // No backoff is requested before the first attempt
+ assertEquals(0, 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 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();
- // A network error is retryable while budget remains
- assertTrue(keyDecryptor.fail());
- keyDecryptor = reenterNeedKms(decryptor);
+ // The context stays in NEED_KMS and the key decryptor is re-presented for the retry
+ keyDecryptor = reenterNeedKms(decryptor);
- // A successful response completes the key decryption
- assertFalse(keyDecryptor.feedWithRetry(getHttpResourceAsByteBuffer("kms-reply.txt")));
- assertEquals(0, keyDecryptor.bytesNeeded());
+ // A network error is retryable while budget remains
+ assertTrue(keyDecryptor.fail());
+ keyDecryptor = reenterNeedKms(decryptor);
- assertNull(decryptor.nextKeyDecryptor());
- decryptor.completeKeyDecryptors();
- assertEquals(State.READY, decryptor.getState());
+ // A successful response completes the key decryption
+ assertFalse(keyDecryptor.feedWithRetry(getHttpResourceAsByteBuffer("kms-reply.txt")));
+ assertEquals(0, keyDecryptor.bytesNeeded());
- RawBsonDocument decryptedDocument = decryptor.finish();
- assertEquals(State.DONE, decryptor.getState());
- assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument);
+ assertNull(decryptor.nextKeyDecryptor());
+ decryptor.completeKeyDecryptors();
+ assertEquals(State.READY, decryptor.getState());
- decryptor.close();
- mongoCrypt.close();
+ RawBsonDocument decryptedDocument = decryptor.finish();
+ assertEquals(State.DONE, decryptor.getState());
+ assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument);
+ }
}
@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++;
+ try (MongoCrypt mongoCrypt = createMongoCrypt();
+ 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");
}
- 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) {
+ private static MongoKeyDecryptor reenterNeedKms(final MongoCryptContext context) {
assertEquals(State.NEED_KMS, context.getState());
MongoKeyDecryptor keyDecryptor = context.nextKeyDecryptor();
assertNotNull(keyDecryptor);
+ assertTrue(keyDecryptor.bytesNeeded() > 0);
+ assertTrue(keyDecryptor.getMessage().remaining() > 0);
return keyDecryptor;
}
- private MongoKeyDecryptor feedKeysAndGetKeyDecryptor(final MongoCryptContext context) {
+ private static MongoKeyDecryptor feedKeysAndGetKeyDecryptor(final MongoCryptContext context) {
assertEquals(State.NEED_MONGO_KEYS, context.getState());
BsonDocument keyFilter = context.getMongoOperation();
assertEquals(getResourceAsDocument("key-filter.json"), keyFilter);
@@ -431,7 +400,7 @@ private void testKeyDecryptor(final MongoCryptContext context, final String keyF
}
private MongoCrypt createMongoCrypt() {
- return MongoCrypts.create(MongoCryptOptions
+ MongoCrypt mongoCrypt = MongoCrypts.create(MongoCryptOptions
.builder()
.awsKmsProviderOptions(MongoAwsKmsProviderOptions.builder()
.accessKeyId("example")
@@ -441,6 +410,8 @@ private MongoCrypt createMongoCrypt() {
.localMasterKey(ByteBuffer.wrap(new byte[96]))
.build())
.build());
+ assertNotNull(mongoCrypt);
+ return mongoCrypt;
}
private static BsonDocument getResourceAsDocument(final String fileName) {
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
index 12e1f6939c3..735a08480f4 100644
--- 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
@@ -26,12 +26,15 @@
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.
+ /**
+ * 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; that cstring is backed by {@link Pointer#NULL}, and its
+ * toString() returns the empty string.
+ */
@Test
- void fromNativeReturnsEmptyStringBackedInstanceForNullPointer() {
+ void fromNativeReturnsNonNullCstringForNullPointer() {
cstring result = (cstring) new cstring().fromNative(null, null);
assertNotNull(result);