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

#include "../mongocrypt-crypto-private.h"
#include "../mongocrypt-private.h"
#include <stdint.h>

#ifdef MONGOCRYPT_ENABLE_CRYPTO_CNG

#include <bcrypt.h>

static BCRYPT_ALG_HANDLE _algo_sha512_hmac = 0;
static BCRYPT_ALG_HANDLE _algo_sha256_hmac = 0;
static BCRYPT_ALG_HANDLE _algo_aes256_cbc = 0;
static BCRYPT_ALG_HANDLE _algo_aes256_ecb = 0;
static DWORD _aes256_key_blob_length;
static DWORD _aes256_block_length;

static BCRYPT_ALG_HANDLE _random;

#define STATUS_SUCCESS 0

bool _native_crypto_initialized = false;

void
_native_crypto_init (void)
{
   DWORD cbOutput;
   NTSTATUS nt_status;

   /* Note, there is no mechanism for libmongocrypt to close these providers,
    * If we ever add such a mechanism, call BCryptCloseAlgorithmProvider.
    */
   nt_status = BCryptOpenAlgorithmProvider (&_algo_sha512_hmac,
                                            BCRYPT_SHA512_ALGORITHM,
                                            MS_PRIMITIVE_PROVIDER,
                                            BCRYPT_ALG_HANDLE_HMAC_FLAG);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptOpenAlgorithmProvider (&_algo_sha256_hmac,
                                            BCRYPT_SHA256_ALGORITHM,
                                            MS_PRIMITIVE_PROVIDER,
                                            BCRYPT_ALG_HANDLE_HMAC_FLAG);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptOpenAlgorithmProvider (
      &_algo_aes256_cbc, BCRYPT_AES_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptSetProperty (
      _algo_aes256_cbc,
      BCRYPT_CHAINING_MODE,
      (PUCHAR) (BCRYPT_CHAIN_MODE_CBC),
      (ULONG) (sizeof (wchar_t) * wcslen (BCRYPT_CHAIN_MODE_CBC)),
      0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptOpenAlgorithmProvider (
      &_algo_aes256_ecb, BCRYPT_AES_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptSetProperty (
      _algo_aes256_ecb,
      BCRYPT_CHAINING_MODE,
      (PUCHAR) (BCRYPT_CHAIN_MODE_ECB),
      (ULONG) (sizeof (wchar_t) * wcslen (BCRYPT_CHAIN_MODE_ECB)),
      0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   cbOutput = sizeof (_aes256_key_blob_length);
   nt_status = BCryptGetProperty (_algo_aes256_cbc,
                                  BCRYPT_OBJECT_LENGTH,
                                  (PUCHAR) (&_aes256_key_blob_length),
                                  cbOutput,
                                  &cbOutput,
                                  0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   cbOutput = sizeof (_aes256_block_length);
   nt_status = BCryptGetProperty (_algo_aes256_cbc,
                                  BCRYPT_BLOCK_LENGTH,
                                  (PUCHAR) (&_aes256_block_length),
                                  cbOutput,
                                  &cbOutput,
                                  0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   nt_status = BCryptOpenAlgorithmProvider (
      &_random, BCRYPT_RNG_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0);
   if (nt_status != STATUS_SUCCESS) {
      return;
   }

   _native_crypto_initialized = true;
}

typedef struct {
   unsigned char *key_object;
   uint32_t key_object_length;

   BCRYPT_KEY_HANDLE key_handle;

   unsigned char *iv;
   uint32_t iv_len;
} cng_encrypt_state;

static void
_crypto_state_destroy (cng_encrypt_state *state);

static cng_encrypt_state *
_crypto_state_init (const _mongocrypt_buffer_t *key,
                    const _mongocrypt_buffer_t *iv,
                    mongocrypt_status_t *status)
{
   cng_encrypt_state *state;
   uint32_t keyBlobLength;
   unsigned char *keyBlob;
   BCRYPT_KEY_DATA_BLOB_HEADER blobHeader;
   NTSTATUS nt_status;

   BSON_ASSERT_PARAM (key);
   BSON_ASSERT_PARAM (iv);

   keyBlob = NULL;

   state = bson_malloc0 (sizeof (*state));
   BSON_ASSERT (state);

   state->key_handle = INVALID_HANDLE_VALUE;

   /* Initialize key storage buffer */
   state->key_object = bson_malloc0 (_aes256_key_blob_length);
   BSON_ASSERT (state->key_object);

   state->key_object_length = _aes256_key_blob_length;

   /* Allocate temporary buffer for key import */
   BSON_ASSERT (sizeof (BCRYPT_KEY_DATA_BLOB_HEADER) + key->len <= UINT32_MAX);
   keyBlobLength = sizeof (BCRYPT_KEY_DATA_BLOB_HEADER) + key->len;
   keyBlob = bson_malloc0 (keyBlobLength);
   BSON_ASSERT (keyBlob);


   blobHeader.dwMagic = BCRYPT_KEY_DATA_BLOB_MAGIC;
   blobHeader.dwVersion = BCRYPT_KEY_DATA_BLOB_VERSION1;
   blobHeader.cbKeyData = key->len;

   memcpy (keyBlob, &blobHeader, sizeof (BCRYPT_KEY_DATA_BLOB_HEADER));

   memcpy (keyBlob + sizeof (BCRYPT_KEY_DATA_BLOB_HEADER), key->data, key->len);

   nt_status = BCryptImportKey (_algo_aes256_cbc,
                                NULL,
                                BCRYPT_KEY_DATA_BLOB,
                                &(state->key_handle),
                                state->key_object,
                                state->key_object_length,
                                keyBlob,
                                keyBlobLength,
                                0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("Import Key Failed: 0x%x", (int) nt_status);
      goto fail;
   }

   bson_free (keyBlob);

   state->iv = bson_malloc0 (iv->len);
   BSON_ASSERT (state->iv);

   state->iv_len = iv->len;
   memcpy (state->iv, iv->data, iv->len);

   return state;
fail:
   _crypto_state_destroy (state);
   bson_free (keyBlob);

   return NULL;
}


static void
_crypto_state_destroy (cng_encrypt_state *state)
{
   if (state) {
      /* Free the key handle before the key_object that contains it */
      if (state->key_handle != INVALID_HANDLE_VALUE) {
         BCryptDestroyKey (state->key_handle);
      }

      bson_free (state->key_object);
      bson_free (state->iv);
      bson_free (state);
   }
}


bool
_native_crypto_aes_256_cbc_encrypt (aes_256_args_t args)
{
   BSON_ASSERT (args.in);
   BSON_ASSERT (args.out);

   bool ret = false;
   mongocrypt_status_t *status = args.status;
   cng_encrypt_state *state = _crypto_state_init (args.key, args.iv, status);

   BSON_ASSERT (state);

   NTSTATUS nt_status;

   nt_status = BCryptEncrypt (state->key_handle,
                              (PUCHAR) (args.in->data),
                              args.in->len,
                              NULL,
                              state->iv,
                              state->iv_len,
                              args.out->data,
                              args.out->len,
                              args.bytes_written,
                              0);

   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error initializing cipher: 0x%x", (int) nt_status);
      goto done;
   }

   ret = true;
done:
   _crypto_state_destroy (state);
   return ret;
}


bool
_native_crypto_aes_256_cbc_decrypt (aes_256_args_t args)
{
   BSON_ASSERT (args.in);
   BSON_ASSERT (args.out);

   bool ret = false;
   mongocrypt_status_t *status = args.status;
   cng_encrypt_state *state = _crypto_state_init (args.key, args.iv, status);

   BSON_ASSERT (state);

   NTSTATUS nt_status;

   nt_status = BCryptDecrypt (state->key_handle,
                              (PUCHAR) (args.in->data),
                              args.in->len,
                              NULL,
                              state->iv,
                              state->iv_len,
                              args.out->data,
                              args.out->len,
                              args.bytes_written,
                              0);


   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error initializing cipher: 0x%x", (int) nt_status);
      goto done;
   }

   ret = true;
done:
   _crypto_state_destroy (state);
   return ret;
}

/* _hmac_with_algorithm computes an HMAC of @in with the algorithm specified by
 * @hAlgorithm.
 * @key is the input key.
 * @out is the output. @out must be allocated by the caller with
 * the expected length @expect_out_len for the output.
 * Returns false and sets @status on error. @status is required. */
static bool
_hmac_with_algorithm (BCRYPT_ALG_HANDLE hAlgorithm,
                      const _mongocrypt_buffer_t *key,
                      const _mongocrypt_buffer_t *in,
                      _mongocrypt_buffer_t *out,
                      uint32_t expect_out_len,
                      mongocrypt_status_t *status)
{
   bool ret = false;
   BCRYPT_HASH_HANDLE hHash;
   NTSTATUS nt_status;

   BSON_ASSERT_PARAM (key);
   BSON_ASSERT_PARAM (in);
   BSON_ASSERT_PARAM (out);

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

   nt_status = BCryptCreateHash (
      hAlgorithm, &hHash, NULL, 0, (PUCHAR) key->data, (ULONG) key->len, 0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error initializing hmac: 0x%x", (int) nt_status);
      /* Only call BCryptDestroyHash if BCryptCreateHash succeeded. */
      return false;
   }

   nt_status = BCryptHashData (hHash, (PUCHAR) in->data, (ULONG) in->len, 0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error hashing data: 0x%x", (int) nt_status);
      goto done;
   }

   nt_status = BCryptFinishHash (hHash, out->data, out->len, 0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error finishing hmac: 0x%x", (int) nt_status);
      goto done;
   }

   ret = true;
done:
   (void) BCryptDestroyHash (hHash);
   return ret;
}

bool
_native_crypto_hmac_sha_512 (const _mongocrypt_buffer_t *key,
                             const _mongocrypt_buffer_t *in,
                             _mongocrypt_buffer_t *out,
                             mongocrypt_status_t *status)
{
   return _hmac_with_algorithm (
      _algo_sha512_hmac, key, in, out, MONGOCRYPT_HMAC_SHA512_LEN, status);
}


bool
_native_crypto_random (_mongocrypt_buffer_t *out,
                       uint32_t count,
                       mongocrypt_status_t *status)
{
   BSON_ASSERT_PARAM (out);

   NTSTATUS nt_status = BCryptGenRandom (_random, out->data, count, 0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("BCryptGenRandom Failed: 0x%x", (int) nt_status);
      return false;
   }

   return true;
}

typedef struct {
   BCRYPT_KEY_HANDLE key_handle;

   unsigned char *input_block;
   uint32_t input_block_len;

   unsigned char *output_block;
   uint32_t output_block_len;
   uint32_t output_block_ptr;
} cng_ctr_encrypt_state;

static bool
_cng_ctr_crypto_generate (cng_ctr_encrypt_state *state,
                          mongocrypt_status_t *status)
{
   BSON_ASSERT (state);

   uint32_t bytesEncrypted = 0;
   NTSTATUS nt_status = BCryptEncrypt (state->key_handle,
                                       state->input_block,
                                       state->input_block_len,
                                       NULL,
                                       NULL,
                                       0,
                                       state->output_block,
                                       state->output_block_len,
                                       &bytesEncrypted,
                                       0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("error encrypting: 0x%x", (int) nt_status);
      return false;
   }
   BSON_ASSERT (bytesEncrypted);
   state->output_block_ptr = 0;
   return true;
}

static void
_cng_ctr_crypto_advance (cng_ctr_encrypt_state *state)
{
   BSON_ASSERT_PARAM (state);

   /* Assert rather than return false/NULL since this function's type is void */
   BSON_ASSERT (sizeof (BCRYPT_KEY_DATA_BLOB_HEADER) <=
                UINT32_MAX - state->input_block_len);

   uint32_t carry = 1;
   for (int i = (int) state->input_block_len - 1; i >= 0 && carry != 0; --i) {
      uint32_t bpp = (uint32_t) (state->input_block[i]) + carry;
      carry = bpp >> 8;
      state->input_block[i] = bpp & 0xFF;
   }
}

static bool
_cng_ctr_crypto_next (cng_ctr_encrypt_state *state,
                      mongocrypt_status_t *status,
                      unsigned char *mask)
{
   BSON_ASSERT_PARAM (state);
   BSON_ASSERT_PARAM (mask);

   if (state->output_block_ptr >= state->output_block_len) {
      _cng_ctr_crypto_advance (state);
      if (!_cng_ctr_crypto_generate (state, status)) {
         return false;
      };
   }

   *mask = state->output_block[state->output_block_ptr];
   ++state->output_block_ptr;

   return true;
}

static void
_cng_ctr_crypto_state_destroy (cng_ctr_encrypt_state *state);

static cng_ctr_encrypt_state *
_cng_ctr_crypto_state_init (const _mongocrypt_buffer_t *key,
                            const _mongocrypt_buffer_t *iv,
                            mongocrypt_status_t *status)
{
   cng_ctr_encrypt_state *state;
   uint32_t keyBlobLength;
   unsigned char *keyBlob;
   BCRYPT_KEY_DATA_BLOB_HEADER blobHeader;
   NTSTATUS nt_status;

   BSON_ASSERT_PARAM (key);
   BSON_ASSERT_PARAM (iv);

   keyBlob = NULL;

   state = bson_malloc0 (sizeof (*state));
   BSON_ASSERT (state);

   if (UINT32_MAX - key->len < sizeof (BCRYPT_KEY_DATA_BLOB_HEADER)) {
      CLIENT_ERR ("key is too long");
      goto fail;
   }

   state->key_handle = INVALID_HANDLE_VALUE;

   /* Initialize input storage buffer */
   state->input_block = bson_malloc0 (_aes256_block_length);
   BSON_ASSERT (state->input_block);
   state->input_block_len = _aes256_block_length;
   BSON_ASSERT (iv->len == _aes256_block_length);
   memcpy (state->input_block, iv->data, iv->len);

   /* Initialize output storage buffer */
   state->output_block = bson_malloc0 (_aes256_block_length);
   BSON_ASSERT (state->output_block);
   state->output_block_len = _aes256_block_length;
   state->output_block_ptr = 0;

   /* Allocate temporary buffer for key import */
   keyBlobLength = sizeof (BCRYPT_KEY_DATA_BLOB_HEADER) + key->len;
   keyBlob = bson_malloc0 (keyBlobLength);
   BSON_ASSERT (keyBlob);

   blobHeader.dwMagic = BCRYPT_KEY_DATA_BLOB_MAGIC;
   blobHeader.dwVersion = BCRYPT_KEY_DATA_BLOB_VERSION1;
   blobHeader.cbKeyData = key->len;

   memcpy (keyBlob, &blobHeader, sizeof (BCRYPT_KEY_DATA_BLOB_HEADER));

   memcpy (keyBlob + sizeof (BCRYPT_KEY_DATA_BLOB_HEADER), key->data, key->len);

   nt_status = BCryptImportKey (_algo_aes256_ecb,
                                NULL,
                                BCRYPT_KEY_DATA_BLOB,
                                &(state->key_handle),
                                NULL,
                                0,
                                keyBlob,
                                keyBlobLength,
                                0);
   if (nt_status != STATUS_SUCCESS) {
      CLIENT_ERR ("Import Key Failed: 0x%x", (int) nt_status);
      goto fail;
   }

   bson_free (keyBlob);

   if (!_cng_ctr_crypto_generate (state, status)) {
      goto fail;
   }

   return state;
fail:
   _cng_ctr_crypto_state_destroy (state);
   bson_free (keyBlob);

   return NULL;
}

static void
_cng_ctr_crypto_state_destroy (cng_ctr_encrypt_state *state)
{
   if (state) {
      if (state->key_handle != INVALID_HANDLE_VALUE) {
         BCryptDestroyKey (state->key_handle);
      }

      bson_free (state->input_block);
      bson_free (state->output_block);
      bson_free (state);
   }
}

bool
_native_crypto_aes_256_ctr_encrypt (aes_256_args_t args)
{
   bool ret = false;
   cng_ctr_encrypt_state *state = NULL;
   mongocrypt_status_t *status = args.status;

   BSON_ASSERT (args.in && args.in->data);
   BSON_ASSERT (args.out && args.out->data);
   if (args.out->len < args.in->len) {
      CLIENT_ERR ("Output buffer is too small");
      goto fail;
   }

   state = _cng_ctr_crypto_state_init (args.key, args.iv, status);
   if (!state) {
      goto fail;
   }

   for (uint32_t i = 0; i < args.in->len; ++i) {
      unsigned char mask;
      if (!_cng_ctr_crypto_next (state, status, &mask)) {
         goto fail;
      }
      args.out->data[i] = args.in->data[i] ^ mask;
   }

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

   ret = true;

fail:
   _cng_ctr_crypto_state_destroy (state);
   return ret;
}

bool
_native_crypto_aes_256_ctr_decrypt (aes_256_args_t args)
{
   bool ret = false;
   cng_ctr_encrypt_state *state = NULL;
   mongocrypt_status_t *status = args.status;

   BSON_ASSERT (args.in && args.in->data);
   BSON_ASSERT (args.out && args.out->data);
   if (args.out->len < args.in->len) {
      CLIENT_ERR ("Output buffer is too small");
      goto fail;
   }

   state = _cng_ctr_crypto_state_init (args.key, args.iv, status);
   if (!state) {
      goto fail;
   }

   for (uint32_t i = 0; i < args.in->len; ++i) {
      unsigned char mask;
      if (!_cng_ctr_crypto_next (state, status, &mask)) {
         goto fail;
      }
      args.out->data[i] = args.in->data[i] ^ mask;
   }

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

   ret = true;

fail:
   _cng_ctr_crypto_state_destroy (state);
   return ret;
}

bool
_native_crypto_hmac_sha_256 (const _mongocrypt_buffer_t *key,
                             const _mongocrypt_buffer_t *in,
                             _mongocrypt_buffer_t *out,
                             mongocrypt_status_t *status)
{
   return _hmac_with_algorithm (
      _algo_sha256_hmac, key, in, out, MONGOCRYPT_HMAC_SHA256_LEN, status);
}

#endif /* MONGOCRYPT_ENABLE_CRYPTO_CNG */
