/* mz_crypt_openssl.c -- Crypto/hash functions for OpenSSL
   part of the minizip-ng project

   Copyright (C) Nathan Moinvaziri
     https://github.com/zlib-ng/minizip-ng

   This program is distributed under the terms of the same license as zlib.
   See the accompanying LICENSE file for the full text of the license.
*/

#include "mz.h"
#include "mz_crypt.h"

#include <openssl/err.h>
#include <openssl/engine.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/aes.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#  include <openssl/core_names.h>
#endif

/***************************************************************************/

static void mz_crypt_init(void) {
    static int32_t openssl_initialized = 0;
    if (!openssl_initialized) {
#if OPENSSL_VERSION_NUMBER < 0x10100000L
        OpenSSL_add_all_algorithms();

        ERR_load_BIO_strings();
        ERR_load_crypto_strings();

        ENGINE_load_builtin_engines();
        ENGINE_register_all_complete();
#else
        OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_ALL_BUILTIN, NULL);
#endif

        openssl_initialized = 1;
    }
}

int32_t mz_crypt_rand(uint8_t *buf, int32_t size) {
    if (!RAND_bytes(buf, size))
        return MZ_CRYPT_ERROR;

    return size;
}

/***************************************************************************/

typedef struct mz_crypt_sha_s {
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    union {
        SHA512_CTX ctx512;
        SHA256_CTX ctx256;
        SHA_CTX ctx1;
    };
#else
    EVP_MD_CTX *ctx;
#endif
    int32_t initialized;
    unsigned long error;
    uint16_t algorithm;
} mz_crypt_sha;

/***************************************************************************/

static const uint8_t mz_crypt_sha_digest_size[] = {
    MZ_HASH_SHA1_SIZE, 0, MZ_HASH_SHA224_SIZE, MZ_HASH_SHA256_SIZE, MZ_HASH_SHA384_SIZE, MZ_HASH_SHA512_SIZE};

/***************************************************************************/

static void mz_crypt_sha_free(void *handle) {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;
    if (sha->ctx)
        EVP_MD_CTX_free(sha->ctx);
    sha->ctx = NULL;
#else
    MZ_UNUSED(handle);
#endif
}

void mz_crypt_sha_reset(void *handle) {
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;

    mz_crypt_init();
    mz_crypt_sha_free(handle);

    sha->error = 0;
    sha->initialized = 0;
}

int32_t mz_crypt_sha_begin(void *handle) {
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;
    int32_t result = 0;

    if (!sha)
        return MZ_PARAM_ERROR;

    mz_crypt_sha_reset(handle);

#if OPENSSL_VERSION_NUMBER < 0x10100000L
    switch (sha->algorithm) {
    case MZ_HASH_SHA1:
        result = SHA1_Init(&sha->ctx1);
        break;
    case MZ_HASH_SHA224:
        result = SHA224_Init(&sha->ctx256);
        break;
    case MZ_HASH_SHA256:
        result = SHA256_Init(&sha->ctx256);
        break;
    case MZ_HASH_SHA384:
        result = SHA384_Init(&sha->ctx512);
        break;
    case MZ_HASH_SHA512:
        result = SHA512_Init(&sha->ctx512);
        break;
    }
#else
    const EVP_MD *md = NULL;
    switch (sha->algorithm) {
    case MZ_HASH_SHA1:
        md = EVP_sha1();
        break;
    case MZ_HASH_SHA224:
        md = EVP_sha224();
        break;
    case MZ_HASH_SHA256:
        md = EVP_sha256();
        break;
    case MZ_HASH_SHA384:
        md = EVP_sha384();
        break;
    case MZ_HASH_SHA512:
        md = EVP_sha512();
        break;
    }
    if (!md)
        return MZ_PARAM_ERROR;

    sha->ctx = EVP_MD_CTX_new();
    if (!sha->ctx)
        return MZ_MEM_ERROR;
    result = EVP_DigestInit_ex(sha->ctx, md, NULL);
#endif

    if (!result) {
        sha->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    sha->initialized = 1;
    return MZ_OK;
}

int32_t mz_crypt_sha_update(void *handle, const void *buf, int32_t size) {
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;
    int32_t result = 0;

    if (!sha || !buf || !sha->initialized)
        return MZ_PARAM_ERROR;

#if OPENSSL_VERSION_NUMBER < 0x10100000L
    switch (sha->algorithm) {
    case MZ_HASH_SHA1:
        result = SHA1_Update(&sha->ctx1, buf, size);
        break;
    case MZ_HASH_SHA224:
        result = SHA224_Update(&sha->ctx256, buf, size);
        break;
    case MZ_HASH_SHA256:
        result = SHA256_Update(&sha->ctx256, buf, size);
        break;
    case MZ_HASH_SHA384:
        result = SHA384_Update(&sha->ctx512, buf, size);
        break;
    case MZ_HASH_SHA512:
        result = SHA512_Update(&sha->ctx512, buf, size);
        break;
    }
#else
    result = EVP_DigestUpdate(sha->ctx, buf, size);
#endif

    if (!result) {
        sha->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    return size;
}

int32_t mz_crypt_sha_end(void *handle, uint8_t *digest, int32_t digest_size) {
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;
    int32_t result = 0;

    if (!sha || !digest || !sha->initialized)
        return MZ_PARAM_ERROR;
    if (digest_size < mz_crypt_sha_digest_size[sha->algorithm - MZ_HASH_SHA1])
        return MZ_PARAM_ERROR;

#if OPENSSL_VERSION_NUMBER < 0x10100000L
    switch (sha->algorithm) {
    case MZ_HASH_SHA1:
        result = SHA1_Final(digest, &sha->ctx1);
        break;
    case MZ_HASH_SHA224:
        result = SHA224_Final(digest, &sha->ctx256);
        break;
    case MZ_HASH_SHA256:
        result = SHA256_Final(digest, &sha->ctx256);
        break;
    case MZ_HASH_SHA384:
        result = SHA384_Final(digest, &sha->ctx512);
        break;
    case MZ_HASH_SHA512:
        result = SHA512_Final(digest, &sha->ctx512);
        break;
    }
#else
    result = EVP_DigestFinal_ex(sha->ctx, digest, NULL);
#endif

    if (!result) {
        sha->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    return MZ_OK;
}

int32_t mz_crypt_sha_set_algorithm(void *handle, uint16_t algorithm) {
    mz_crypt_sha *sha = (mz_crypt_sha *)handle;
    if (algorithm < MZ_HASH_SHA1 || algorithm > MZ_HASH_SHA512)
        return MZ_PARAM_ERROR;
    sha->algorithm = algorithm;
    return MZ_OK;
}

void *mz_crypt_sha_create(void) {
    mz_crypt_sha *sha = (mz_crypt_sha *)calloc(1, sizeof(mz_crypt_sha));
    if (sha)
        sha->algorithm = MZ_HASH_SHA256;
    return sha;
}

void mz_crypt_sha_delete(void **handle) {
    mz_crypt_sha *sha = NULL;
    if (!handle)
        return;
    sha = (mz_crypt_sha *)*handle;
    if (sha) {
        mz_crypt_sha_free(*handle);
        free(sha);
    }
    *handle = NULL;
}

/***************************************************************************/

typedef struct mz_crypt_aes_s {
    int32_t mode;
    unsigned long error;
    EVP_CIPHER_CTX *ctx;
} mz_crypt_aes;

/***************************************************************************/

static void mz_crypt_aes_free(void *handle) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;
    if (aes->ctx)
        EVP_CIPHER_CTX_free(aes->ctx);
    aes->ctx = NULL;
}

void mz_crypt_aes_reset(void *handle) {
    mz_crypt_init();
    mz_crypt_aes_free(handle);
}

int32_t mz_crypt_aes_encrypt(void *handle, const void *aad, int32_t aad_size, uint8_t *buf, int32_t size) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;

    if (!aes || !buf || size % MZ_AES_BLOCK_SIZE != 0 || !aes->ctx)
        return MZ_PARAM_ERROR;
    if (aes->mode != MZ_AES_MODE_GCM && aad && aad_size > 0)
        return MZ_PARAM_ERROR;

    if (aad && aad_size > 0) {
        int32_t how_many = 0;
        if (!EVP_EncryptUpdate(aes->ctx, NULL, &how_many, aad, aad_size))
            return MZ_CRYPT_ERROR;
    }

    if (!EVP_EncryptUpdate(aes->ctx, buf, &size, buf, size))
        return MZ_CRYPT_ERROR;

    return size;
}

int32_t mz_crypt_aes_encrypt_final(void *handle, uint8_t *buf, int32_t size, uint8_t *tag, int32_t tag_size) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;
    int result = 0;
    int out_len = 0;

    if (!aes || !tag || !tag_size || !aes->ctx || aes->mode != MZ_AES_MODE_GCM)
        return MZ_PARAM_ERROR;

    if (buf && size) {
        if (!EVP_EncryptUpdate(aes->ctx, buf, &size, buf, size))
            return MZ_CRYPT_ERROR;
    }

    /* Must call EncryptFinal for tag to be calculated */
    result = EVP_EncryptFinal_ex(aes->ctx, NULL, &out_len);

    if (result)
        result = EVP_CIPHER_CTX_ctrl(aes->ctx, EVP_CTRL_GCM_GET_TAG, tag_size, tag);

    if (!result) {
        aes->error = ERR_get_error();
        return MZ_CRYPT_ERROR;
    }

    return size;
}

int32_t mz_crypt_aes_decrypt(void *handle, const void *aad, int32_t aad_size, uint8_t *buf, int32_t size) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;

    if (!aes || !buf || size % MZ_AES_BLOCK_SIZE != 0 || !aes->ctx)
        return MZ_PARAM_ERROR;
    if (aes->mode != MZ_AES_MODE_GCM && aad && aad_size > 0)
        return MZ_PARAM_ERROR;

    if (aad && aad_size > 0) {
        int32_t how_many = 0;
        if (!EVP_DecryptUpdate(aes->ctx, NULL, &how_many, aad, aad_size))
            return MZ_CRYPT_ERROR;
    }

    if (!EVP_DecryptUpdate(aes->ctx, buf, &size, buf, size))
        return MZ_CRYPT_ERROR;

    return size;
}

int32_t mz_crypt_aes_decrypt_final(void *handle, uint8_t *buf, int32_t size, const uint8_t *tag, int32_t tag_length) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;
    int out_len = 0;

    if (!aes || !tag || !tag_length || !aes->ctx || aes->mode != MZ_AES_MODE_GCM)
        return MZ_PARAM_ERROR;

    if (buf && size) {
        if (!EVP_DecryptUpdate(aes->ctx, buf, &size, buf, size))
            return MZ_CRYPT_ERROR;
    }

    /* Set expected tag */
    if (!EVP_CIPHER_CTX_ctrl(aes->ctx, EVP_CTRL_GCM_SET_TAG, tag_length, (void *)tag)) {
        aes->error = ERR_get_error();
        return MZ_CRYPT_ERROR;
    }

    /* Must call DecryptFinal for tag verification */
    if (!EVP_DecryptFinal_ex(aes->ctx, NULL, &out_len)) {
        aes->error = ERR_get_error();
        return MZ_CRYPT_ERROR;
    }

    return size;
}

static int32_t mz_crypt_aes_set_key(void *handle, const void *key, int32_t key_length, const void *iv,
                                    int32_t iv_length, int32_t encrypt) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;
    const EVP_CIPHER *type = NULL;

    switch (aes->mode) {
    case MZ_AES_MODE_CBC:
        if (key_length == 16)
            type = EVP_aes_128_cbc();
        else if (key_length == 24)
            type = EVP_aes_192_cbc();
        else if (key_length == 32)
            type = EVP_aes_256_cbc();
        break;
    case MZ_AES_MODE_ECB:
        if (key_length == 16)
            type = EVP_aes_128_ecb();
        else if (key_length == 24)
            type = EVP_aes_192_ecb();
        else if (key_length == 32)
            type = EVP_aes_256_ecb();
        break;
    case MZ_AES_MODE_GCM:
        if (key_length == 16)
            type = EVP_aes_128_gcm();
        else if (key_length == 24)
            type = EVP_aes_192_gcm();
        else if (key_length == 32)
            type = EVP_aes_256_gcm();
        break;
    }
    if (!type)
        return MZ_PARAM_ERROR;

    aes->ctx = EVP_CIPHER_CTX_new();
    if (!aes->ctx)
        return MZ_MEM_ERROR;

    if (!EVP_CipherInit_ex(aes->ctx, type, NULL, key, iv, encrypt)) {
        aes->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    EVP_CIPHER_CTX_set_padding(aes->ctx, aes->mode == MZ_AES_MODE_GCM);

    return MZ_OK;
}

int32_t mz_crypt_aes_set_encrypt_key(void *handle, const void *key, int32_t key_length, const void *iv,
                                     int32_t iv_length) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;

    if (!aes || !key || !key_length)
        return MZ_PARAM_ERROR;
    if (key_length != 16 && key_length != 24 && key_length != 32)
        return MZ_PARAM_ERROR;
    if (iv && iv_length != MZ_AES_BLOCK_SIZE)
        return MZ_PARAM_ERROR;

    mz_crypt_aes_reset(handle);

    return mz_crypt_aes_set_key(handle, key, key_length, iv, iv_length, 1);
}

int32_t mz_crypt_aes_set_decrypt_key(void *handle, const void *key, int32_t key_length, const void *iv,
                                     int32_t iv_length) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;

    if (!aes || !key || !key_length)
        return MZ_PARAM_ERROR;
    if (key_length != 16 && key_length != 24 && key_length != 32)
        return MZ_PARAM_ERROR;
    if (iv && iv_length > MZ_AES_BLOCK_SIZE)
        return MZ_PARAM_ERROR;

    mz_crypt_aes_reset(handle);

    return mz_crypt_aes_set_key(handle, key, key_length, iv, iv_length, 0);
}

void mz_crypt_aes_set_mode(void *handle, int32_t mode) {
    mz_crypt_aes *aes = (mz_crypt_aes *)handle;
    aes->mode = mode;
}

void *mz_crypt_aes_create(void) {
    mz_crypt_aes *aes = (mz_crypt_aes *)calloc(1, sizeof(mz_crypt_aes));
    return aes;
}

void mz_crypt_aes_delete(void **handle) {
    mz_crypt_aes *aes = NULL;
    if (!handle)
        return;
    aes = (mz_crypt_aes *)*handle;
    if (aes) {
        mz_crypt_aes_free(*handle);
        free(aes);
    }
    *handle = NULL;
}

/***************************************************************************/

typedef struct mz_crypt_hmac_s {
#if OPENSSL_VERSION_NUMBER < 0x30000000L
    HMAC_CTX *ctx;
#else
    EVP_MAC *mac;
    EVP_MAC_CTX *ctx;
#endif
    int32_t initialized;
    unsigned long error;
    uint16_t algorithm;
} mz_crypt_hmac;

/***************************************************************************/

#if (OPENSSL_VERSION_NUMBER < 0x10100000L) ||                                                                          \
    (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER < 0x2070000fL))
static HMAC_CTX *HMAC_CTX_new(void) {
    HMAC_CTX *ctx = OPENSSL_malloc(sizeof(HMAC_CTX));
    if (ctx)
        HMAC_CTX_init(ctx);
    return ctx;
}

static void HMAC_CTX_free(HMAC_CTX *ctx) {
    if (ctx) {
        HMAC_CTX_cleanup(ctx);
        OPENSSL_free(ctx);
    }
}
#endif

/***************************************************************************/

static void mz_crypt_hmac_free(void *handle) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;

#if OPENSSL_VERSION_NUMBER < 0x30000000L
    HMAC_CTX_free(hmac->ctx);
#else
    if (hmac->ctx)
        EVP_MAC_CTX_free(hmac->ctx);
    if (hmac->mac)
        EVP_MAC_free(hmac->mac);
    hmac->mac = NULL;
#endif

    hmac->ctx = NULL;
}

void mz_crypt_hmac_reset(void *handle) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;

    mz_crypt_init();
    mz_crypt_hmac_free(handle);

    hmac->error = 0;
}

int32_t mz_crypt_hmac_init(void *handle, const void *key, int32_t key_length) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;
    int32_t result = 0;

    if (!hmac || !key)
        return MZ_PARAM_ERROR;

    mz_crypt_hmac_reset(handle);

#if OPENSSL_VERSION_NUMBER < 0x30000000L
    const EVP_MD *evp_md = NULL;

    if (hmac->algorithm == MZ_HASH_SHA1)
        evp_md = EVP_sha1();
    else
        evp_md = EVP_sha256();

    hmac->ctx = HMAC_CTX_new();
    if (!hmac->ctx)
        return MZ_MEM_ERROR;

    result = HMAC_Init_ex(hmac->ctx, key, key_length, evp_md, NULL);
#else
    char *digest_algorithm = NULL;
    OSSL_PARAM params[2];

    if (hmac->algorithm == MZ_HASH_SHA1)
        digest_algorithm = "sha1";
    else
        digest_algorithm = "sha256";

    params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, digest_algorithm, 0);
    params[1] = OSSL_PARAM_construct_end();

    hmac->mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
    if (!hmac->mac)
        return MZ_MEM_ERROR;
    hmac->ctx = EVP_MAC_CTX_new(hmac->mac);
    if (!hmac->ctx)
        return MZ_MEM_ERROR;
    result = EVP_MAC_init(hmac->ctx, key, key_length, params);
#endif

    if (!result) {
        hmac->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    return MZ_OK;
}

int32_t mz_crypt_hmac_update(void *handle, const void *buf, int32_t size) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;
    int32_t result = 0;

    if (!hmac || !buf)
        return MZ_PARAM_ERROR;

#if OPENSSL_VERSION_NUMBER < 0x30000000L
    result = HMAC_Update(hmac->ctx, buf, size);
#else
    result = EVP_MAC_update(hmac->ctx, buf, size);
#endif
    if (!result) {
        hmac->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    return MZ_OK;
}

int32_t mz_crypt_hmac_end(void *handle, uint8_t *digest, int32_t digest_size) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;
    int32_t result = 0;

    if (!hmac || !digest)
        return MZ_PARAM_ERROR;

#if OPENSSL_VERSION_NUMBER < 0x30000000L
    if (hmac->algorithm == MZ_HASH_SHA1) {
        if (digest_size < MZ_HASH_SHA1_SIZE)
            return MZ_BUF_ERROR;

        result = HMAC_Final(hmac->ctx, digest, (uint32_t *)&digest_size);
    } else {
        if (digest_size < MZ_HASH_SHA256_SIZE)
            return MZ_BUF_ERROR;
        result = HMAC_Final(hmac->ctx, digest, (uint32_t *)&digest_size);
    }
#else
    {
        size_t digest_outsize = digest_size;
        result = EVP_MAC_final(hmac->ctx, digest, &digest_outsize, digest_size);
    }
#endif

    if (!result) {
        hmac->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }

    return MZ_OK;
}

void mz_crypt_hmac_set_algorithm(void *handle, uint16_t algorithm) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)handle;
    hmac->algorithm = algorithm;
}

int32_t mz_crypt_hmac_copy(void *src_handle, void *target_handle) {
    mz_crypt_hmac *source = (mz_crypt_hmac *)src_handle;
    mz_crypt_hmac *target = (mz_crypt_hmac *)target_handle;

    if (!source || !target)
        return MZ_PARAM_ERROR;

    mz_crypt_hmac_reset(target_handle);

#if OPENSSL_VERSION_NUMBER < 0x30000000L
    if (!target->ctx)
        target->ctx = HMAC_CTX_new();

    if (!HMAC_CTX_copy(target->ctx, source->ctx)) {
        target->error = ERR_get_error();
        return MZ_HASH_ERROR;
    }
#else
    if (!target->ctx)
        target->ctx = EVP_MAC_CTX_dup(source->ctx);
    if (!target->ctx)
        return MZ_MEM_ERROR;
#endif

    return MZ_OK;
}

void *mz_crypt_hmac_create(void) {
    mz_crypt_hmac *hmac = (mz_crypt_hmac *)calloc(1, sizeof(mz_crypt_hmac));
    if (hmac)
        hmac->algorithm = MZ_HASH_SHA256;
    return hmac;
}

void mz_crypt_hmac_delete(void **handle) {
    mz_crypt_hmac *hmac = NULL;
    if (!handle)
        return;
    hmac = (mz_crypt_hmac *)*handle;
    if (hmac) {
        mz_crypt_hmac_free(*handle);
        free(hmac);
    }
    *handle = NULL;
}
