/*
 * Copyright 2019-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.
 */

/*
 * Comments in this implementation refer to:
 * [MCGREW] https://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05
 */

#include <bson/bson.h>

#include "mongocrypt-binary-private.h"
#include "mongocrypt-buffer-private.h"
#include "mongocrypt-crypto-private.h"
#include "mongocrypt-log-private.h"
#include "mongocrypt-private.h"
#include "mongocrypt-status-private.h"

#include <inttypes.h>

/* This function uses ECB callback to simulate CTR encrypt and decrypt
 *
 * Note: the same function performs both encrypt and decrypt using same ECB
 * encryption function
 */

static bool _crypto_aes_256_ctr_encrypt_decrypt_via_ecb(void *ctx,
                                                        mongocrypt_crypto_fn aes_256_ecb_encrypt,
                                                        aes_256_args_t args,
                                                        mongocrypt_status_t *status) {
    BSON_ASSERT(args.iv && args.iv->len);
    BSON_ASSERT(args.in);
    BSON_ASSERT(args.out);

    if (args.out->len < args.in->len) {
        CLIENT_ERR("output buffer too small");
        return false;
    }

    _mongocrypt_buffer_t ctr, tmp;
    mongocrypt_binary_t key_bin, out_bin, in_bin, ctr_bin, tmp_bin;
    bool ret;

    _mongocrypt_buffer_to_binary(args.key, &key_bin);
    _mongocrypt_buffer_init(&ctr);
    _mongocrypt_buffer_copy_to(args.iv, &ctr);
    _mongocrypt_buffer_to_binary(&ctr, &ctr_bin);
    _mongocrypt_buffer_to_binary(args.out, &out_bin);
    _mongocrypt_buffer_to_binary(args.in, &in_bin);
    _mongocrypt_buffer_init_size(&tmp, args.iv->len);
    _mongocrypt_buffer_to_binary(&tmp, &tmp_bin);

    for (uint32_t ptr = 0; ptr < args.in->len;) {
        /* Encrypt value in CTR buffer */
        uint32_t bytes_written = 0;
        if (!aes_256_ecb_encrypt(ctx, &key_bin, NULL, &ctr_bin, &tmp_bin, &bytes_written, status)) {
            ret = false;
            goto cleanup;
        }

        if (bytes_written != tmp_bin.len) {
            CLIENT_ERR("encryption hook returned unexpected length");
            ret = false;
            goto cleanup;
        }

        /* XOR resulting stream with original data */
        for (uint32_t i = 0; i < bytes_written && ptr < args.in->len; i++, ptr++) {
            uint8_t *in_bin_u8 = in_bin.data;
            uint8_t *out_bin_u8 = out_bin.data;
            uint8_t *tmp_bin_u8 = tmp_bin.data;
            out_bin_u8[ptr] = in_bin_u8[ptr] ^ tmp_bin_u8[i];
        }

        /* Increment value in CTR buffer */
        uint32_t carry = 1;
        /* assert rather than return since this should never happen */
        BSON_ASSERT(ctr_bin.len == 0u || ctr_bin.len - 1u <= INT_MAX);
        for (int i = (int)ctr_bin.len - 1; i >= 0 && carry != 0; --i) {
            uint8_t *ctr_bin_u8 = ctr_bin.data;
            uint32_t bpp = carry + ctr_bin_u8[i];
            carry = bpp >> 8;
            ctr_bin_u8[i] = bpp & 0xFF;
        }
    }

    if (args.bytes_written) {
        *args.bytes_written = args.in->len;
    }

    ret = true;

cleanup:
    _mongocrypt_buffer_cleanup(&ctr);
    _mongocrypt_buffer_cleanup(&tmp);
    return ret;
}

/* Crypto primitives. These either call the native built in crypto primitives or
 * user supplied hooks. */
static bool _crypto_aes_256_cbc_encrypt(_mongocrypt_crypto_t *crypto, aes_256_args_t args) {
    mongocrypt_status_t *status = args.status;

    BSON_ASSERT_PARAM(crypto);

    BSON_ASSERT(args.key);
    if (args.key->len != MONGOCRYPT_ENC_KEY_LEN) {
        CLIENT_ERR("invalid encryption key length");
        return false;
    }

    BSON_ASSERT(args.iv);
    if (args.iv->len != MONGOCRYPT_IV_LEN) {
        CLIENT_ERR("invalid iv length");
        return false;
    }

    if (crypto->hooks_enabled) {
        mongocrypt_binary_t enc_key_bin, iv_bin, out_bin, in_bin;
        bool ret;

        _mongocrypt_buffer_to_binary(args.key, &enc_key_bin);
        _mongocrypt_buffer_to_binary(args.iv, &iv_bin);
        _mongocrypt_buffer_to_binary(args.out, &out_bin);
        _mongocrypt_buffer_to_binary(args.in, &in_bin);

        ret = crypto->aes_256_cbc_encrypt(crypto->ctx,
                                          &enc_key_bin,
                                          &iv_bin,
                                          &in_bin,
                                          &out_bin,
                                          args.bytes_written,
                                          status);
        return ret;
    }
    return _native_crypto_aes_256_cbc_encrypt(args);
}

static bool _crypto_aes_256_ctr_encrypt(_mongocrypt_crypto_t *crypto, aes_256_args_t args) {
    mongocrypt_status_t *status = args.status;

    BSON_ASSERT_PARAM(crypto);

    BSON_ASSERT(args.key);
    if (args.key->len != MONGOCRYPT_ENC_KEY_LEN) {
        CLIENT_ERR("invalid encryption key length");
        return false;
    }

    BSON_ASSERT(args.iv);
    if (args.iv->len != MONGOCRYPT_IV_LEN) {
        CLIENT_ERR("invalid iv length");
        return false;
    }

    if (crypto->aes_256_ctr_encrypt) {
        mongocrypt_binary_t enc_key_bin, iv_bin, out_bin, in_bin;
        bool ret;

        _mongocrypt_buffer_to_binary(args.key, &enc_key_bin);
        _mongocrypt_buffer_to_binary(args.iv, &iv_bin);
        _mongocrypt_buffer_to_binary(args.out, &out_bin);
        _mongocrypt_buffer_to_binary(args.in, &in_bin);

        ret = crypto->aes_256_ctr_encrypt(crypto->ctx,
                                          &enc_key_bin,
                                          &iv_bin,
                                          &in_bin,
                                          &out_bin,
                                          args.bytes_written,
                                          status);
        return ret;
    }

    if (crypto->aes_256_ecb_encrypt) {
        return _crypto_aes_256_ctr_encrypt_decrypt_via_ecb(crypto->ctx, crypto->aes_256_ecb_encrypt, args, status);
    }

    return _native_crypto_aes_256_ctr_encrypt(args);
}

static bool _crypto_aes_256_cbc_decrypt(_mongocrypt_crypto_t *crypto, aes_256_args_t args) {
    mongocrypt_status_t *status = args.status;

    BSON_ASSERT_PARAM(crypto);

    BSON_ASSERT(args.key);
    if (args.key->len != MONGOCRYPT_ENC_KEY_LEN) {
        CLIENT_ERR("invalid encryption key length");
        return false;
    }

    if (crypto->hooks_enabled) {
        mongocrypt_binary_t enc_key_bin, iv_bin, out_bin, in_bin;
        bool ret;

        _mongocrypt_buffer_to_binary(args.key, &enc_key_bin);
        _mongocrypt_buffer_to_binary(args.iv, &iv_bin);
        _mongocrypt_buffer_to_binary(args.out, &out_bin);
        _mongocrypt_buffer_to_binary(args.in, &in_bin);

        ret = crypto->aes_256_cbc_decrypt(crypto->ctx,
                                          &enc_key_bin,
                                          &iv_bin,
                                          &in_bin,
                                          &out_bin,
                                          args.bytes_written,
                                          status);
        return ret;
    }
    return _native_crypto_aes_256_cbc_decrypt(args);
}

static bool _crypto_aes_256_ctr_decrypt(_mongocrypt_crypto_t *crypto, aes_256_args_t args) {
    mongocrypt_status_t *status = args.status;

    BSON_ASSERT_PARAM(crypto);

    BSON_ASSERT(args.key);
    if (args.key->len != MONGOCRYPT_ENC_KEY_LEN) {
        CLIENT_ERR("invalid encryption key length");
        return false;
    }

    if (crypto->aes_256_ctr_decrypt) {
        mongocrypt_binary_t enc_key_bin, iv_bin, out_bin, in_bin;
        bool ret;

        _mongocrypt_buffer_to_binary(args.key, &enc_key_bin);
        _mongocrypt_buffer_to_binary(args.iv, &iv_bin);
        _mongocrypt_buffer_to_binary(args.out, &out_bin);
        _mongocrypt_buffer_to_binary(args.in, &in_bin);

        ret = crypto->aes_256_ctr_decrypt(crypto->ctx,
                                          &enc_key_bin,
                                          &iv_bin,
                                          &in_bin,
                                          &out_bin,
                                          args.bytes_written,
                                          status);
        return ret;
    }

    if (crypto->aes_256_ecb_encrypt) {
        return _crypto_aes_256_ctr_encrypt_decrypt_via_ecb(crypto->ctx, crypto->aes_256_ecb_encrypt, args, status);
    }

    return _native_crypto_aes_256_ctr_decrypt(args);
}

static bool _crypto_hmac_sha_512(_mongocrypt_crypto_t *crypto,
                                 const _mongocrypt_buffer_t *hmac_key,
                                 const _mongocrypt_buffer_t *in,
                                 _mongocrypt_buffer_t *out,
                                 mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(hmac_key);
    BSON_ASSERT_PARAM(in);
    BSON_ASSERT_PARAM(out);

    if (hmac_key->len != MONGOCRYPT_MAC_KEY_LEN) {
        CLIENT_ERR("invalid hmac key length");
        return false;
    }

    if (out->len != MONGOCRYPT_HMAC_SHA512_LEN) {
        CLIENT_ERR("out does not contain %d bytes", MONGOCRYPT_HMAC_SHA512_LEN);
        return false;
    }

    if (crypto->hooks_enabled) {
        mongocrypt_binary_t hmac_key_bin, out_bin, in_bin;
        bool ret;

        _mongocrypt_buffer_to_binary(hmac_key, &hmac_key_bin);
        _mongocrypt_buffer_to_binary(out, &out_bin);
        _mongocrypt_buffer_to_binary(in, &in_bin);

        ret = crypto->hmac_sha_512(crypto->ctx, &hmac_key_bin, &in_bin, &out_bin, status);
        return ret;
    }
    return _native_crypto_hmac_sha_512(hmac_key, in, out, status);
}

bool _mongocrypt_hmac_sha_256(_mongocrypt_crypto_t *crypto,
                              const _mongocrypt_buffer_t *key,
                              const _mongocrypt_buffer_t *in,
                              _mongocrypt_buffer_t *out,
                              mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(key);
    BSON_ASSERT_PARAM(in);
    BSON_ASSERT_PARAM(out);

    if (key->len != MONGOCRYPT_MAC_KEY_LEN) {
        CLIENT_ERR("invalid hmac_sha_256 key length. Got %" PRIu32 ", expected: %d", key->len, MONGOCRYPT_MAC_KEY_LEN);
        return false;
    }

    if (crypto->hooks_enabled) {
        mongocrypt_binary_t key_bin, out_bin, in_bin;
        _mongocrypt_buffer_to_binary(key, &key_bin);
        _mongocrypt_buffer_to_binary(out, &out_bin);
        _mongocrypt_buffer_to_binary(in, &in_bin);

        return crypto->hmac_sha_256(crypto->ctx, &key_bin, &in_bin, &out_bin, status);
    }
    return _native_crypto_hmac_sha_256(key, in, out, status);
}

static bool
_crypto_random(_mongocrypt_crypto_t *crypto, _mongocrypt_buffer_t *out, uint32_t count, mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(out);

    if (out->len != count) {
        CLIENT_ERR("out does not contain %u bytes", count);
        return false;
    }

    if (crypto->hooks_enabled) {
        mongocrypt_binary_t out_bin;

        _mongocrypt_buffer_to_binary(out, &out_bin);
        return crypto->random(crypto->ctx, &out_bin, count, status);
    }
    return _native_crypto_random(out, count, status);
}

/*
 * Secure memcmp copied from the C driver.
 */
int _mongocrypt_memequal(const void *const b1, const void *const b2, size_t len) {
    const unsigned char *p1 = b1, *p2 = b2;
    int ret = 0;

    BSON_ASSERT_PARAM(b1);
    BSON_ASSERT_PARAM(b2);

    for (; len > 0; len--) {
        ret |= *p1++ ^ *p2++;
    }

    return ret;
}

typedef enum {
    MODE_CBC,
    MODE_CTR,
} _mongocrypt_encryption_mode_t;

typedef enum {
    HMAC_NONE,
    HMAC_SHA_512_256, // sha512 truncated to 256 bits
    HMAC_SHA_256,
} _mongocrypt_hmac_type_t;

typedef enum {
    KEY_FORMAT_FLE1,       // 32 octets MAC key, 32 DATA key, 32 IV key (ignored)
    KEY_FORMAT_FLE2,       // 32 octets DATA key
    KEY_FORMAT_FLE2AEAD,   // 32 octets DATA key, 32 MAC key, 32 IV key (ignored)
    KEY_FORMAT_FLE2v2AEAD, // 32 octets DATA key, 32 MAC key, 32 IV key (ignored)
} _mongocrypt_key_format_t;

typedef enum {
    MAC_FORMAT_FLE1,       // HMAC(AAD || IV || S || LEN(AAD) as uint64be)
    MAC_FORMAT_FLE2,       // NONE
    MAC_FORMAT_FLE2AEAD,   // HMAC(AAD || IV || S)
    MAC_FORMAT_FLE2v2AEAD, // HMAC(AAD || IV || S)
} _mongocrypt_mac_format_t;

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_calculate_ciphertext_len
 *
 * Calculate the space needed for a ciphertext payload of a given size
 * and using fixed iv/hmac lengths.
 *
 * MODE_CBC: Assumes the ciphertext will be padded according to PKCS#7
 * which rounds up to the next block size, adding up to a complete block
 * for block aligned input payloads.
 *
 * MODE_CTR: Assumes no additional padding since CTR is a streaming cipher.
 *
 * Assumes all algorithms use identical IV length and blocksizes.
 *
 * ----------------------------------------------------------------------------
 */
static uint32_t _mongocrypt_calculate_ciphertext_len(uint32_t inlen,
                                                     _mongocrypt_encryption_mode_t mode,
                                                     _mongocrypt_hmac_type_t hmac,
                                                     mongocrypt_status_t *status) {
    const uint32_t hmaclen = (hmac == HMAC_NONE) ? 0 : MONGOCRYPT_HMAC_LEN;
    const uint32_t maxinlen = UINT32_MAX - (MONGOCRYPT_IV_LEN + MONGOCRYPT_BLOCK_SIZE + hmaclen);
    uint32_t fill;
    if (inlen > maxinlen) {
        CLIENT_ERR("plaintext too long");
        return 0;
    }

    if (mode == MODE_CBC) {
        fill = MONGOCRYPT_BLOCK_SIZE - (inlen % MONGOCRYPT_BLOCK_SIZE);
    } else {
        BSON_ASSERT(mode == MODE_CTR);
        fill = 0;
    }

    return MONGOCRYPT_IV_LEN + inlen + fill + hmaclen;
}

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_calculate_plaintext_len
 *
 * Calculate the space needed for a plaintext payload of a given size
 * and using fixed iv/hmac lengths.
 *
 * MODE_CBC: In practice, plaintext will be between 1 and {blocksize} bytes
 * shorter
 * than the input ciphertext, but it's easier and safer to assume the
 * full ciphertext length and waste a few bytes.
 *
 * MODE_CTR: Assumes no additional padding since CTR is a streaming cipher.
 *
 * Assumes all algorithms use identical IV length and blocksizes.
 *
 * ----------------------------------------------------------------------------
 */
static uint32_t _mongocrypt_calculate_plaintext_len(uint32_t inlen,
                                                    _mongocrypt_encryption_mode_t mode,
                                                    _mongocrypt_hmac_type_t hmac,
                                                    mongocrypt_status_t *status) {
    const uint32_t hmaclen = (hmac == HMAC_NONE) ? 0 : MONGOCRYPT_HMAC_LEN;
    const uint32_t mincipher = (mode == MODE_CTR) ? 0 : MONGOCRYPT_BLOCK_SIZE;
    if (inlen < (MONGOCRYPT_IV_LEN + mincipher + hmaclen)) {
        CLIENT_ERR("input ciphertext too small. Must be at least %" PRIu32 " bytes",
                   MONGOCRYPT_IV_LEN + mincipher + hmaclen);
        return 0;
    }
    return inlen - (MONGOCRYPT_IV_LEN + hmaclen);
}

/* ----------------------------------------------------------------------------
 *
 * _aes256_cbc_encrypt --
 *
 *    Encrypts using AES256 CBC using a secret key and a known IV.
 *
 * Parameters:
 *    @iv a 16 byte IV.
 *    @enc_key a 32 byte key.
 *    @plaintext the plaintext to encrypt.
 *    @ciphertext the resulting ciphertext.
 *    @bytes_written a location for the resulting number of bytes written into
 *    ciphertext->data.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 * Preconditions:
 *    1. ciphertext->data has been pre-allocated with enough space for the
 *    resulting ciphertext.
 *
 * Postconditions:
 *    1. bytes_written is set to the length of the written ciphertext. This
 *    is the same as
 *    _mongocrypt_calculate_ciphertext_len (plaintext->len, mode, hmac, status).
 *
 * ----------------------------------------------------------------------------
 */
static bool _encrypt_step(_mongocrypt_crypto_t *crypto,
                          _mongocrypt_encryption_mode_t mode,
                          const _mongocrypt_buffer_t *iv,
                          const _mongocrypt_buffer_t *enc_key,
                          const _mongocrypt_buffer_t *plaintext,
                          _mongocrypt_buffer_t *ciphertext,
                          uint32_t *bytes_written,
                          mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(iv);
    BSON_ASSERT_PARAM(enc_key);
    BSON_ASSERT_PARAM(plaintext);
    BSON_ASSERT_PARAM(ciphertext);

    BSON_ASSERT_PARAM(bytes_written);
    *bytes_written = 0;

    if (MONGOCRYPT_IV_LEN != iv->len) {
        CLIENT_ERR("IV should have length %d, but has length %" PRIu32, MONGOCRYPT_IV_LEN, iv->len);
        return false;
    }

    if (MONGOCRYPT_ENC_KEY_LEN != enc_key->len) {
        CLIENT_ERR("Encryption key should have length %d, but has length %" PRIu32,
                   MONGOCRYPT_ENC_KEY_LEN,
                   enc_key->len);
        return false;
    }

    if (mode == MODE_CTR) {
        // Streaming cipher, no padding required.
        return _crypto_aes_256_ctr_encrypt(crypto,
                                           (aes_256_args_t){.key = enc_key,
                                                            .iv = iv,
                                                            .in = plaintext,
                                                            .out = ciphertext,
                                                            .bytes_written = bytes_written,
                                                            .status = status});
    }

    BSON_ASSERT(mode == MODE_CBC);

    /* calculate how many extra bytes there are after a block boundary */
    const uint32_t unaligned = plaintext->len % MONGOCRYPT_BLOCK_SIZE;
    uint32_t padding_byte = MONGOCRYPT_BLOCK_SIZE - unaligned;
    _mongocrypt_buffer_t intermediates[2], to_encrypt;
    uint8_t final_block_storage[MONGOCRYPT_BLOCK_SIZE];
    bool ret;

    BSON_ASSERT(MONGOCRYPT_BLOCK_SIZE >= unaligned);

    /* Some crypto providers disallow variable length inputs, and require
     * the input to be a multiple of the block size. So add everything up
     * to but excluding the last block if not block aligned, then add
     * the last block with padding. */
    _mongocrypt_buffer_init(&intermediates[0]);
    _mongocrypt_buffer_init(&intermediates[1]);
    intermediates[0].data = (uint8_t *)plaintext->data;
    /* don't check plaintext->len, as the above modulo operation guarantees
     * that unaligned will be smaller */
    intermediates[0].len = plaintext->len - unaligned;
    intermediates[1].data = final_block_storage;
    intermediates[1].len = sizeof(final_block_storage);

    /* [MCGREW]: "Prior to CBC encryption, the plaintext P is padded by appending
     * a padding string PS to that data, to ensure that len(P || PS) is a
     * multiple of 128". This is also known as PKCS #7 padding. */
    if (unaligned) {
        /* Copy the unaligned bytes. */
        memcpy(intermediates[1].data, plaintext->data + (plaintext->len - unaligned), unaligned);
    }
    /* Fill out block remained or whole block with padding_byte */
    memset(intermediates[1].data + unaligned, (int)padding_byte, padding_byte);

    _mongocrypt_buffer_init(&to_encrypt);
    if (!_mongocrypt_buffer_concat(&to_encrypt, intermediates, 2)) {
        CLIENT_ERR("failed to allocate buffer");
        _mongocrypt_buffer_cleanup(&to_encrypt);
        return false;
    }

    ret = _crypto_aes_256_cbc_encrypt(crypto,
                                      (aes_256_args_t){.key = enc_key,
                                                       .iv = iv,
                                                       .in = &to_encrypt,
                                                       .out = ciphertext,
                                                       .bytes_written = bytes_written,
                                                       .status = status});
    _mongocrypt_buffer_cleanup(&to_encrypt);
    if (!ret) {
        return false;
    }

    if (*bytes_written % MONGOCRYPT_BLOCK_SIZE != 0) {
        CLIENT_ERR("encryption failure, wrote %" PRIu32 " bytes, not a multiple of %d",
                   *bytes_written,
                   MONGOCRYPT_BLOCK_SIZE);
        return false;
    }

    return true;
}

/* ----------------------------------------------------------------------------
 *
 * _hmac_step --
 *
 *    Compute the selected HMAC with a secret key.
 *
 * Parameters:
 *    @Km a 32 byte key.
 *    @AAD associated data to add into the HMAC. This may be
 *    an empty buffer.
 *    @iv_and_ciphertext the IV and S components to add into the HMAC.
 *    @out a location for the resulting HMAC tag.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 * Preconditions:
 *    1. out->data has been pre-allocated with at least 64 bytes.
 *
 * Postconditions:
 *    1. out->data will have a 64 byte tag appended.
 *
 * ----------------------------------------------------------------------------
 */
static bool _hmac_step(_mongocrypt_crypto_t *crypto,
                       _mongocrypt_mac_format_t mac_format,
                       _mongocrypt_hmac_type_t hmac,
                       const _mongocrypt_buffer_t *Km,
                       const _mongocrypt_buffer_t *AAD,
                       const _mongocrypt_buffer_t *iv_and_ciphertext,
                       _mongocrypt_buffer_t *out,
                       mongocrypt_status_t *status) {
    _mongocrypt_buffer_t to_hmac = {0};
    bool ret = false;

    BSON_ASSERT(hmac != HMAC_NONE);
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(Km);
    // AAD may be NULL
    BSON_ASSERT_PARAM(iv_and_ciphertext);
    BSON_ASSERT_PARAM(out);

    _mongocrypt_buffer_init(&to_hmac);

    if (MONGOCRYPT_MAC_KEY_LEN != Km->len) {
        CLIENT_ERR("HMAC key wrong length: %" PRIu32, Km->len);
        goto done;
    }

    if (out->len != MONGOCRYPT_HMAC_LEN) {
        CLIENT_ERR("out wrong length: %" PRIu32, out->len);
        goto done;
    }

    /* Construct the input to the HMAC */
    uint32_t num_intermediates = 0;
    _mongocrypt_buffer_t intermediates[3];
    if (AAD && !_mongocrypt_buffer_from_subrange(&intermediates[num_intermediates++], AAD, 0, AAD->len)) {
        CLIENT_ERR("Failed creating MAC subrange on AD");
        goto done;
    }
    if (!_mongocrypt_buffer_from_subrange(&intermediates[num_intermediates++],
                                          iv_and_ciphertext,
                                          0,
                                          iv_and_ciphertext->len)) {
        CLIENT_ERR("Failed creating MAC subrange on IV and S");
        goto done;
    }

    // {AL} must be stored in the function's lexical scope so that
    // {intermediates}'s reference to it survives until the
    // _mongocrypt_buffer_concat operation later.
    uint64_t AL;
    if (mac_format == MAC_FORMAT_FLE1) {
        /* T := HMAC(AAD || IV || S || AL)
         * AL is equal to the number of bits in AAD expressed
         * as a 64bit unsigned big-endian integer.
         * Multiplying a uint32_t by 8 won't bring it anywhere close to
         * UINT64_MAX.
         */
        AL = AAD ? BSON_UINT64_TO_BE(8 * (uint64_t)AAD->len) : 0;
        _mongocrypt_buffer_init(&intermediates[num_intermediates]);
        intermediates[num_intermediates].data = (uint8_t *)&AL;
        intermediates[num_intermediates++].len = sizeof(uint64_t);

    } else {
        /* T := HMAC(AAD || IV || S) */
        BSON_ASSERT((mac_format == MAC_FORMAT_FLE2AEAD) || (mac_format == MAC_FORMAT_FLE2v2AEAD));
    }

    if (!_mongocrypt_buffer_concat(&to_hmac, intermediates, num_intermediates)) {
        CLIENT_ERR("failed to allocate buffer");
        goto done;
    }

    if (hmac == HMAC_SHA_512_256) {
        uint8_t storage[64];
        _mongocrypt_buffer_t tag = {.data = storage, .len = sizeof(storage)};

        if (!_crypto_hmac_sha_512(crypto, Km, &to_hmac, &tag, status)) {
            goto done;
        }

        // Truncate sha512 to first 256 bits.
        memcpy(out->data, tag.data, MONGOCRYPT_HMAC_LEN);

    } else {
        BSON_ASSERT(hmac == HMAC_SHA_256);
        if (!_mongocrypt_hmac_sha_256(crypto, Km, &to_hmac, out, status)) {
            goto done;
        }
    }

    ret = true;
done:
    _mongocrypt_buffer_cleanup(&to_hmac);
    return ret;
}

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_do_encryption --
 *
 *    Defer encryption to whichever crypto library libmongocrypt is using.
 *
 * Parameters:
 *    @iv a 16 byte IV.
 *    @associated_data associated data for the HMAC. May be NULL.
 *    @key is the encryption key. The size depends on @key_format.
 *    @plaintext the plaintext to encrypt.
 *    @ciphertext a location for the resulting ciphertext and HMAC tag.
 *    @bytes_written a location for the resulting bytes written.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 * Preconditions:
 *    1. ciphertext->data has been pre-allocated with enough space for the
 *    resulting ciphertext. Use _mongocrypt_calculate_ciphertext_len.
 *
 * Postconditions:
 *    1. bytes_written is set to the length of the written ciphertext. This
 *    is the same as
 *    _mongocrypt_calculate_ciphertext_len (plaintext->len, mode, hmac, status).
 *
 * ----------------------------------------------------------------------------
 */
static bool _mongocrypt_do_encryption(_mongocrypt_crypto_t *crypto,
                                      _mongocrypt_key_format_t key_format,
                                      _mongocrypt_mac_format_t mac_format,
                                      _mongocrypt_encryption_mode_t mode,
                                      _mongocrypt_hmac_type_t hmac,
                                      const _mongocrypt_buffer_t *iv,
                                      const _mongocrypt_buffer_t *associated_data,
                                      const _mongocrypt_buffer_t *key,
                                      const _mongocrypt_buffer_t *plaintext,
                                      _mongocrypt_buffer_t *ciphertext,
                                      uint32_t *bytes_written,
                                      mongocrypt_status_t *status) {
    _mongocrypt_buffer_t Ke = {0}; // Ke == Key for Encryption
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(iv);
    /* associated_data is checked at the point it is used, so it can be NULL */
    BSON_ASSERT_PARAM(key);
    BSON_ASSERT_PARAM(plaintext);
    BSON_ASSERT_PARAM(ciphertext);

    if (plaintext->len <= 0) {
        CLIENT_ERR("input plaintext too small. Must be more than zero bytes.");
        return false;
    }

    const uint32_t expect_ciphertext_len = _mongocrypt_calculate_ciphertext_len(plaintext->len, mode, hmac, status);
    if (mongocrypt_status_type(status) != MONGOCRYPT_STATUS_OK) {
        return false;
    }
    if (expect_ciphertext_len != ciphertext->len) {
        CLIENT_ERR("output ciphertext should have been allocated with %" PRIu32 " bytes", expect_ciphertext_len);
        return false;
    }

    if (MONGOCRYPT_IV_LEN != iv->len) {
        CLIENT_ERR("IV should have length %d, but has length %" PRIu32, MONGOCRYPT_IV_LEN, iv->len);
        return false;
    }

    const uint32_t expected_key_len = (key_format == KEY_FORMAT_FLE2) ? MONGOCRYPT_ENC_KEY_LEN : MONGOCRYPT_KEY_LEN;
    if (key->len != expected_key_len) {
        CLIENT_ERR("key should have length %" PRIu32 ", but has length %" PRIu32, expected_key_len, key->len);
        return false;
    }

    // Copy IV into the output, and clear remainder.
    memmove(ciphertext->data, iv->data, MONGOCRYPT_IV_LEN);
    memset(ciphertext->data + MONGOCRYPT_IV_LEN, 0, ciphertext->len - MONGOCRYPT_IV_LEN);

    // S is the encryption payload without IV or HMAC
    _mongocrypt_buffer_t S;
    if (!_mongocrypt_buffer_from_subrange(&S, ciphertext, MONGOCRYPT_IV_LEN, ciphertext->len - MONGOCRYPT_IV_LEN)) {
        CLIENT_ERR("unable to create S subrange from C");
        return false;
    }
    if (hmac != HMAC_NONE) {
        S.len -= MONGOCRYPT_HMAC_LEN;
    }

    // Ke is the key used for payload encryption
    const uint32_t Ke_offset = (key_format == KEY_FORMAT_FLE1) ? MONGOCRYPT_MAC_KEY_LEN : 0;
    if (!_mongocrypt_buffer_from_subrange(&Ke, key, Ke_offset, MONGOCRYPT_ENC_KEY_LEN)) {
        CLIENT_ERR("unable to create Ke subrange from key");
        return false;
    }

    uint32_t S_bytes_written = 0;
    if (!_encrypt_step(crypto, mode, iv, &Ke, plaintext, &S, &S_bytes_written, status)) {
        return false;
    }
    BSON_ASSERT_PARAM(bytes_written);
    BSON_ASSERT((UINT32_MAX - S_bytes_written) > MONGOCRYPT_IV_LEN);
    *bytes_written = MONGOCRYPT_IV_LEN + S_bytes_written;

    if (hmac != HMAC_NONE) {
        // Km == Key for MAC
        const uint32_t Km_offset = (key_format == KEY_FORMAT_FLE1) ? 0 : MONGOCRYPT_ENC_KEY_LEN;

        // Km is the HMAC Key.
        _mongocrypt_buffer_t Km;
        if (!_mongocrypt_buffer_from_subrange(&Km, key, Km_offset, MONGOCRYPT_MAC_KEY_LEN)) {
            CLIENT_ERR("unable to create Km subrange from key");
            return false;
        }

        /* Primary payload to MAC. */
        _mongocrypt_buffer_t iv_and_ciphertext;
        if (!_mongocrypt_buffer_from_subrange(&iv_and_ciphertext, ciphertext, 0, *bytes_written)) {
            CLIENT_ERR("unable to create IV || S subrange from C");
            return false;
        }

        // T == HMAC Tag
        _mongocrypt_buffer_t T;
        if (!_mongocrypt_buffer_from_subrange(&T, ciphertext, *bytes_written, MONGOCRYPT_HMAC_LEN)) {
            CLIENT_ERR("unable to create T subrange from C");
            return false;
        }

        if (!_hmac_step(crypto, mac_format, hmac, &Km, associated_data, &iv_and_ciphertext, &T, status)) {
            return false;
        }

        *bytes_written += MONGOCRYPT_HMAC_LEN;
    }

    return true;
}

/* ----------------------------------------------------------------------------
 *
 * _decrypt_step --
 *
 *    Decrypts using AES256 using a secret key and a known IV.
 *
 * Parameters:
 *    @enc_key a 32 byte key.
 *    @ciphertext the ciphertext to decrypt.
 *    @plaintext the resulting plaintext.
 *    @bytes_written a location for the resulting number of bytes written into
 *    plaintext->data.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 * Preconditions:
 *    1. plaintext->data has been pre-allocated with enough space for the
 *    resulting plaintext.
 *
 * Postconditions:
 *    1. bytes_written is set to the length of the written plaintext, excluding
 *    padding. This may be less than
 *    _mongocrypt_calculate_plaintext_len (ciphertext->len, home, hmac, status).
 *
 * ----------------------------------------------------------------------------
 */
static bool _decrypt_step(_mongocrypt_crypto_t *crypto,
                          _mongocrypt_encryption_mode_t mode,
                          const _mongocrypt_buffer_t *iv,
                          const _mongocrypt_buffer_t *enc_key,
                          const _mongocrypt_buffer_t *ciphertext,
                          _mongocrypt_buffer_t *plaintext,
                          uint32_t *bytes_written,
                          mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(iv);
    BSON_ASSERT_PARAM(enc_key);
    BSON_ASSERT_PARAM(ciphertext);
    BSON_ASSERT_PARAM(plaintext);

    BSON_ASSERT_PARAM(bytes_written);
    *bytes_written = 0;

    if (MONGOCRYPT_IV_LEN != iv->len) {
        CLIENT_ERR("IV should have length %d, but has length %" PRIu32, MONGOCRYPT_IV_LEN, iv->len);
        return false;
    }
    if (MONGOCRYPT_ENC_KEY_LEN != enc_key->len) {
        CLIENT_ERR("encryption key should have length %d, but has length %" PRIu32,
                   MONGOCRYPT_ENC_KEY_LEN,
                   enc_key->len);
        return false;
    }

    if (mode == MODE_CBC) {
        if (ciphertext->len % MONGOCRYPT_BLOCK_SIZE > 0) {
            CLIENT_ERR("error, ciphertext length is not a multiple of block size");
            return false;
        }

        if (!_crypto_aes_256_cbc_decrypt(crypto,
                                         (aes_256_args_t){.iv = iv,
                                                          .key = enc_key,
                                                          .in = ciphertext,
                                                          .out = plaintext,
                                                          .bytes_written = bytes_written,
                                                          .status = status})) {
            return false;
        }

        BSON_ASSERT(*bytes_written > 0);
        uint8_t padding_byte = plaintext->data[*bytes_written - 1];
        if (padding_byte > 16) {
            CLIENT_ERR("error, ciphertext malformed padding");
            return false;
        }
        *bytes_written -= padding_byte;

    } else {
        BSON_ASSERT(mode == MODE_CTR);
        if (!_crypto_aes_256_ctr_decrypt(crypto,
                                         (aes_256_args_t){.iv = iv,
                                                          .key = enc_key,
                                                          .in = ciphertext,
                                                          .out = plaintext,
                                                          .bytes_written = bytes_written,
                                                          .status = status})) {
            return false;
        }
        BSON_ASSERT(*bytes_written == plaintext->len);
    }

    return true;
}

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_do_decryption --
 *
 *    Defer decryption to whichever crypto library libmongocrypt is using.
 *
 * Parameters:
 *    @associated_data associated data for the HMAC. May be NULL.
 *    @key a 96 byte key.
 *    @ciphertext the ciphertext to decrypt. This contains the IV prepended.
 *    @plaintext a location for the resulting plaintext.
 *    @bytes_written a location for the resulting bytes written.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 *  Preconditions:
 *    1. plaintext->data has been pre-allocated with enough space for the
 *    resulting plaintext and padding. See _mongocrypt_calculate_plaintext_len.
 *
 *  Postconditions:
 *    1. bytes_written is set to the length of the written plaintext, excluding
 *    padding. This may be less than
 *    _mongocrypt_calculate_plaintext_len (ciphertext->len, mode, hmac, status).
 *
 * ----------------------------------------------------------------------------
 */
static bool _mongocrypt_do_decryption(_mongocrypt_crypto_t *crypto,
                                      _mongocrypt_key_format_t key_format,
                                      _mongocrypt_mac_format_t mac_format,
                                      _mongocrypt_encryption_mode_t mode,
                                      _mongocrypt_hmac_type_t hmac,
                                      const _mongocrypt_buffer_t *associated_data,
                                      const _mongocrypt_buffer_t *key,
                                      const _mongocrypt_buffer_t *ciphertext,
                                      _mongocrypt_buffer_t *plaintext,
                                      uint32_t *bytes_written,
                                      mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    /* associated_data is checked at the point it is used, so it can be NULL */
    BSON_ASSERT_PARAM(key);
    BSON_ASSERT_PARAM(ciphertext);
    BSON_ASSERT_PARAM(plaintext);
    BSON_ASSERT_PARAM(bytes_written);

    const uint32_t expect_plaintext_len = _mongocrypt_calculate_plaintext_len(ciphertext->len, mode, hmac, status);
    if (mongocrypt_status_type(status) != MONGOCRYPT_STATUS_OK) {
        return false;
    }
    if (plaintext->len != expect_plaintext_len) {
        CLIENT_ERR("output plaintext should have been allocated with %" PRIu32 " bytes, "
                   "but has: %" PRIu32,
                   expect_plaintext_len,
                   plaintext->len);
        return false;
    }
    if (expect_plaintext_len == 0) {
        // While a ciphertext string describing a zero length plaintext is
        // technically valid,
        // it's not actually particularly useful in the context of FLE where such
        // values aren't encoded.
        CLIENT_ERR("input ciphertext too small. Must be more than %" PRIu32 " bytes",
                   _mongocrypt_calculate_ciphertext_len(0, mode, hmac, NULL));
        return false;
    }

    const uint32_t expected_key_len = (key_format == KEY_FORMAT_FLE2) ? MONGOCRYPT_ENC_KEY_LEN : MONGOCRYPT_KEY_LEN;
    if (expected_key_len != key->len) {
        CLIENT_ERR("key should have length %" PRIu32 ", but has length %" PRIu32, expected_key_len, key->len);
        return false;
    }

    const uint32_t min_cipherlen = _mongocrypt_calculate_ciphertext_len(0, mode, hmac, NULL);
    if (ciphertext->len < min_cipherlen) {
        CLIENT_ERR("corrupt ciphertext - must be >= %" PRIu32 " bytes", min_cipherlen);
        return false;
    }

    _mongocrypt_buffer_t Ke;
    const uint32_t Ke_offset = (key_format == KEY_FORMAT_FLE1) ? MONGOCRYPT_MAC_KEY_LEN : 0;
    if (!_mongocrypt_buffer_from_subrange(&Ke, key, Ke_offset, MONGOCRYPT_ENC_KEY_LEN)) {
        CLIENT_ERR("unable to create Ke subrange from key");
        return false;
    }

    _mongocrypt_buffer_t IV;
    if (!_mongocrypt_buffer_from_subrange(&IV, ciphertext, 0, MONGOCRYPT_IV_LEN)) {
        CLIENT_ERR("unable to create IV subrange from ciphertext");
        return false;
    }

    if (hmac == HMAC_NONE) {
        BSON_ASSERT(key_format == KEY_FORMAT_FLE2);

    } else {
        BSON_ASSERT(key_format != KEY_FORMAT_FLE2);

        uint8_t hmac_tag_storage[MONGOCRYPT_HMAC_LEN];
        const uint32_t mac_key_offset = (key_format == KEY_FORMAT_FLE1) ? 0 : MONGOCRYPT_ENC_KEY_LEN;
        _mongocrypt_buffer_t Km;
        if (!_mongocrypt_buffer_from_subrange(&Km, key, mac_key_offset, MONGOCRYPT_MAC_KEY_LEN)) {
            CLIENT_ERR("unable to create Km subrange from key");
            return false;
        }

        _mongocrypt_buffer_t iv_and_ciphertext;
        if (!_mongocrypt_buffer_from_subrange(&iv_and_ciphertext,
                                              ciphertext,
                                              0,
                                              ciphertext->len - MONGOCRYPT_HMAC_LEN)) {
            CLIENT_ERR("unable to create IV || S subrange from C");
            return false;
        }

        _mongocrypt_buffer_t hmac_tag = {.data = hmac_tag_storage, .len = MONGOCRYPT_HMAC_LEN};

        if (!_hmac_step(crypto, mac_format, hmac, &Km, associated_data, &iv_and_ciphertext, &hmac_tag, status)) {
            return false;
        }

        /* Constant time compare. */
        _mongocrypt_buffer_t T;
        if (!_mongocrypt_buffer_from_subrange(&T,
                                              ciphertext,
                                              ciphertext->len - MONGOCRYPT_HMAC_LEN,
                                              MONGOCRYPT_HMAC_LEN)) {
            CLIENT_ERR("unable to create T subrange from C");
            return false;
        }
        if (0 != _mongocrypt_memequal(hmac_tag.data, T.data, MONGOCRYPT_HMAC_LEN)) {
            CLIENT_ERR("HMAC validation failure");
            return false;
        }
    }

    /* Decrypt data excluding IV + HMAC. */
    const uint32_t hmac_len = (hmac == HMAC_NONE) ? 0 : MONGOCRYPT_HMAC_LEN;
    _mongocrypt_buffer_t S;
    if (!_mongocrypt_buffer_from_subrange(&S,
                                          ciphertext,
                                          MONGOCRYPT_IV_LEN,
                                          ciphertext->len - MONGOCRYPT_IV_LEN - hmac_len)) {
        CLIENT_ERR("unable to create S subrange from C");
        return false;
    }

    return _decrypt_step(crypto, mode, &IV, &Ke, &S, plaintext, bytes_written, status);
}

#define DECLARE_ALGORITHM(name, mode, hmac)                                                                            \
    static uint32_t _mc_##name##_ciphertext_len(uint32_t plaintext_len, mongocrypt_status_t *status) {                 \
        return _mongocrypt_calculate_ciphertext_len(plaintext_len, MODE_##mode, HMAC_##hmac, status);                  \
    }                                                                                                                  \
    static uint32_t _mc_##name##_plaintext_len(uint32_t ciphertext_len, mongocrypt_status_t *status) {                 \
        return _mongocrypt_calculate_plaintext_len(ciphertext_len, MODE_##mode, HMAC_##hmac, status);                  \
    }                                                                                                                  \
    static bool _mc_##name##_do_encryption(_mongocrypt_crypto_t *crypto,                                               \
                                           const _mongocrypt_buffer_t *iv,                                             \
                                           const _mongocrypt_buffer_t *aad,                                            \
                                           const _mongocrypt_buffer_t *key,                                            \
                                           const _mongocrypt_buffer_t *plaintext,                                      \
                                           _mongocrypt_buffer_t *ciphertext,                                           \
                                           uint32_t *written,                                                          \
                                           mongocrypt_status_t *status) {                                              \
        return _mongocrypt_do_encryption(crypto,                                                                       \
                                         KEY_FORMAT_##name,                                                            \
                                         MAC_FORMAT_##name,                                                            \
                                         MODE_##mode,                                                                  \
                                         HMAC_##hmac,                                                                  \
                                         iv,                                                                           \
                                         aad,                                                                          \
                                         key,                                                                          \
                                         plaintext,                                                                    \
                                         ciphertext,                                                                   \
                                         written,                                                                      \
                                         status);                                                                      \
    }                                                                                                                  \
    static bool _mc_##name##_do_decryption(_mongocrypt_crypto_t *crypto,                                               \
                                           const _mongocrypt_buffer_t *aad,                                            \
                                           const _mongocrypt_buffer_t *key,                                            \
                                           const _mongocrypt_buffer_t *ciphertext,                                     \
                                           _mongocrypt_buffer_t *plaintext,                                            \
                                           uint32_t *written,                                                          \
                                           mongocrypt_status_t *status) {                                              \
        return _mongocrypt_do_decryption(crypto,                                                                       \
                                         KEY_FORMAT_##name,                                                            \
                                         MAC_FORMAT_##name,                                                            \
                                         MODE_##mode,                                                                  \
                                         HMAC_##hmac,                                                                  \
                                         aad,                                                                          \
                                         key,                                                                          \
                                         ciphertext,                                                                   \
                                         plaintext,                                                                    \
                                         written,                                                                      \
                                         status);                                                                      \
    }                                                                                                                  \
    static const _mongocrypt_value_encryption_algorithm_t _mc##name##Algorithm_definition = {                          \
        _mc_##name##_ciphertext_len,                                                                                   \
        _mc_##name##_plaintext_len,                                                                                    \
        _mc_##name##_do_encryption,                                                                                    \
        _mc_##name##_do_decryption,                                                                                    \
    };                                                                                                                 \
    const _mongocrypt_value_encryption_algorithm_t *_mc##name##Algorithm(void) {                                       \
        return &_mc##name##Algorithm_definition;                                                                       \
    }

// FLE1 algorithm: AES-256-CBC HMAC/SHA-512-256 (SHA-512 truncated to 256 bits)
DECLARE_ALGORITHM(FLE1, CBC, SHA_512_256)

// FLE2 AEAD used value algorithm: AES-256-CTR HMAC/SHA-256
DECLARE_ALGORITHM(FLE2AEAD, CTR, SHA_256)

// FLE2 used with ESC/ECOC tokens: AES-256-CTR no HMAC
DECLARE_ALGORITHM(FLE2, CTR, NONE)

// FLE2v2 AEAD general algorithm: AES-256-CBC HMAC/SHA-256
DECLARE_ALGORITHM(FLE2v2AEAD, CBC, SHA_256)

#undef DECLARE_ALGORITHM

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_random --
 *
 *    Generates a string of random bytes.
 *
 * Parameters:
 *    @out an output buffer that has been pre-allocated.
 *    @status set on error.
 *    @count the size of the random string in bytes.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 *  Preconditions:
 *    1. out has been pre-allocated with at least 'count' bytes of space.
 *
 * ----------------------------------------------------------------------------
 */
bool _mongocrypt_random(_mongocrypt_crypto_t *crypto,
                        _mongocrypt_buffer_t *out,
                        uint32_t count,
                        mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(out);

    if (count != out->len) {
        CLIENT_ERR("out should have length %" PRIu32 ", but has length %" PRIu32, count, out->len);
        return false;
    }

    return _crypto_random(crypto, out, count, status);
}

/* ----------------------------------------------------------------------------
 *
 * _mongocrypt_calculate_deterministic_iv --
 *
 *    Compute the IV for deterministic encryption from the plaintext and IV
 *    key by using HMAC function.
 *
 * Parameters:
 *    @key the 96 byte key. The last 32 represent the IV key.
 *    @plaintext the plaintext to be encrypted.
 *    @associated_data associated data to include in the HMAC.
 *    @out an output buffer that has been pre-allocated.
 *    @status set on error.
 *
 * Returns:
 *    True on success. On error, sets @status and returns false.
 *
 *  Preconditions:
 *    1. out has been pre-allocated with at least MONGOCRYPT_IV_LEN bytes.
 *
 * ----------------------------------------------------------------------------
 */
bool _mongocrypt_calculate_deterministic_iv(_mongocrypt_crypto_t *crypto,
                                            const _mongocrypt_buffer_t *key,
                                            const _mongocrypt_buffer_t *plaintext,
                                            const _mongocrypt_buffer_t *associated_data,
                                            _mongocrypt_buffer_t *out,
                                            mongocrypt_status_t *status) {
    _mongocrypt_buffer_t intermediates[3];
    _mongocrypt_buffer_t to_hmac;
    _mongocrypt_buffer_t iv_key;
    uint64_t associated_data_len_be;
    uint8_t tag_storage[64];
    _mongocrypt_buffer_t tag;
    bool ret = false;

    _mongocrypt_buffer_init(&to_hmac);

    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(key);
    BSON_ASSERT_PARAM(plaintext);
    BSON_ASSERT_PARAM(associated_data);
    BSON_ASSERT_PARAM(out);

    if (MONGOCRYPT_KEY_LEN != key->len) {
        CLIENT_ERR("key should have length %d, but has length %" PRIu32 "\n", MONGOCRYPT_KEY_LEN, key->len);
        goto done;
    }
    if (MONGOCRYPT_IV_LEN != out->len) {
        CLIENT_ERR("out should have length %d, but has length %" PRIu32 "\n", MONGOCRYPT_IV_LEN, out->len);
        goto done;
    }

    _mongocrypt_buffer_init(&iv_key);
    iv_key.data = key->data + MONGOCRYPT_ENC_KEY_LEN + MONGOCRYPT_MAC_KEY_LEN;
    iv_key.len = MONGOCRYPT_IV_KEY_LEN;

    _mongocrypt_buffer_init(&intermediates[0]);
    _mongocrypt_buffer_init(&intermediates[1]);
    _mongocrypt_buffer_init(&intermediates[2]);
    /* Add associated data. */
    intermediates[0].data = associated_data->data;
    intermediates[0].len = associated_data->len;
    /* Add associated data length in bits. */
    /* multiplying a uint32_t by 8 won't bring it anywhere close to UINT64_MAX */
    associated_data_len_be = 8 * (uint64_t)associated_data->len;
    associated_data_len_be = BSON_UINT64_TO_BE(associated_data_len_be);
    intermediates[1].data = (uint8_t *)&associated_data_len_be;
    intermediates[1].len = sizeof(uint64_t);
    /* Add plaintext. */
    intermediates[2].data = (uint8_t *)plaintext->data;
    intermediates[2].len = plaintext->len;

    tag.data = tag_storage;
    tag.len = sizeof(tag_storage);

    if (!_mongocrypt_buffer_concat(&to_hmac, intermediates, 3)) {
        CLIENT_ERR("failed to allocate buffer");
        goto done;
    }

    if (!_crypto_hmac_sha_512(crypto, &iv_key, &to_hmac, &tag, status)) {
        goto done;
    }

    /* Truncate to IV length */
    memcpy(out->data, tag.data, MONGOCRYPT_IV_LEN);

    ret = true;
done:
    _mongocrypt_buffer_cleanup(&to_hmac);
    return ret;
}

bool _mongocrypt_wrap_key(_mongocrypt_crypto_t *crypto,
                          _mongocrypt_buffer_t *kek,
                          _mongocrypt_buffer_t *dek,
                          _mongocrypt_buffer_t *encrypted_dek,
                          mongocrypt_status_t *status) {
    const _mongocrypt_value_encryption_algorithm_t *fle1alg = _mcFLE1Algorithm();
    uint32_t bytes_written;
    _mongocrypt_buffer_t iv = {0};
    bool ret = false;

    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(kek);
    BSON_ASSERT_PARAM(dek);
    BSON_ASSERT_PARAM(encrypted_dek);

    _mongocrypt_buffer_init(encrypted_dek);

    if (dek->len != MONGOCRYPT_KEY_LEN) {
        CLIENT_ERR("data encryption key is incorrect length, expected: %d, got: %" PRIu32,
                   MONGOCRYPT_KEY_LEN,
                   dek->len);
        goto done;
    }

    // _mongocrypt_wrap_key() uses FLE1 algorithm parameters.
    _mongocrypt_buffer_resize(encrypted_dek, fle1alg->get_ciphertext_len(dek->len, status));
    _mongocrypt_buffer_resize(&iv, MONGOCRYPT_IV_LEN);

    if (!_mongocrypt_random(crypto, &iv, MONGOCRYPT_IV_LEN, status)) {
        goto done;
    }

    if (!fle1alg
             ->do_encrypt(crypto, &iv, NULL /* associated data. */, kek, dek, encrypted_dek, &bytes_written, status)) {
        goto done;
    }

    ret = true;
done:
    _mongocrypt_buffer_cleanup(&iv);
    return ret;
}

bool _mongocrypt_unwrap_key(_mongocrypt_crypto_t *crypto,
                            _mongocrypt_buffer_t *kek,
                            _mongocrypt_buffer_t *encrypted_dek,
                            _mongocrypt_buffer_t *dek,
                            mongocrypt_status_t *status) {
    const _mongocrypt_value_encryption_algorithm_t *fle1alg = _mcFLE1Algorithm();
    uint32_t bytes_written;

    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(kek);
    BSON_ASSERT_PARAM(dek);
    BSON_ASSERT_PARAM(encrypted_dek);

    // _mongocrypt_wrap_key() uses FLE1 algorithm parameters.
    _mongocrypt_buffer_init(dek);
    _mongocrypt_buffer_resize(dek, fle1alg->get_plaintext_len(encrypted_dek->len, status));

    if (!fle1alg->do_decrypt(crypto, NULL /* associated data. */, kek, encrypted_dek, dek, &bytes_written, status)) {
        return false;
    }
    dek->len = bytes_written;

    if (dek->len != MONGOCRYPT_KEY_LEN) {
        CLIENT_ERR("decrypted key is incorrect length, expected: %d, got: %" PRIu32, MONGOCRYPT_KEY_LEN, dek->len);
        return false;
    }
    return true;
}

/* This implementation avoids modulo bias. It is based on arc4random_uniform:
https://github.com/openbsd/src/blob/2207c4325726fdc5c4bcd0011af0fdf7d3dab137/lib/libc/crypt/arc4random_uniform.c#L33
*/
bool _mongocrypt_random_uint64(_mongocrypt_crypto_t *crypto,
                               uint64_t exclusive_upper_bound,
                               uint64_t *out,
                               mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(out);

    *out = 0;

    if (exclusive_upper_bound < 2) {
        *out = 0;
        return true;
    }

    /* 2**64 % x == (2**64 - x) % x */
    uint64_t min = (0 - exclusive_upper_bound) % exclusive_upper_bound;

    _mongocrypt_buffer_t rand_u64_buf;
    _mongocrypt_buffer_init(&rand_u64_buf);
    _mongocrypt_buffer_resize(&rand_u64_buf, (uint32_t)sizeof(uint64_t));

    uint64_t rand_u64;
    for (;;) {
        if (!_mongocrypt_random(crypto, &rand_u64_buf, rand_u64_buf.len, status)) {
            _mongocrypt_buffer_cleanup(&rand_u64_buf);
            return false;
        }

        memcpy(&rand_u64, rand_u64_buf.data, rand_u64_buf.len);
        // Use little-endian to enable deterministic tests on big-endian machines.
        rand_u64 = BSON_UINT64_FROM_LE(rand_u64);

        if (rand_u64 >= min) {
            break;
        }
    }

    *out = rand_u64 % exclusive_upper_bound;

    _mongocrypt_buffer_cleanup(&rand_u64_buf);
    return true;
}

bool _mongocrypt_random_int64(_mongocrypt_crypto_t *crypto,
                              int64_t exclusive_upper_bound,
                              int64_t *out,
                              mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(out);

    if (exclusive_upper_bound <= 0) {
        CLIENT_ERR("Expected exclusive_upper_bound > 0");
        return false;
    }

    uint64_t u64_exclusive_upper_bound = (uint64_t)exclusive_upper_bound;
    uint64_t u64_out;

    if (!_mongocrypt_random_uint64(crypto, u64_exclusive_upper_bound, &u64_out, status)) {
        return false;
    }

    /* Zero the leading bit to ensure rand_i64 is non-negative. */
    u64_out &= (~(1ull << 63));
    *out = (int64_t)u64_out;
    return true;
}
