/* SPDX-License-Identifier: BSD-3-Clause */

#include <errno.h>
#include <stdint.h>
#include <string.h>

#include <openssl/bn.h>
#include <openssl/ecdh.h>

#include <tss2/tss2_tpm2_types.h>
#include <tss2/tss2_mu.h>

#include "log.h"
#include "tpm2_openssl.h"
#include "tpm2_alg_util.h"
#include "tpm2_util.h"

TSS2_RC tpm2_kdfe(
        TPMI_ALG_HASH hash_alg, TPM2B_ECC_PARAMETER *Z,
        unsigned char *label, int label_length,
        TPM2B_ECC_PARAMETER *party_u, TPM2B_ECC_PARAMETER *party_v,
        UINT16 size_in_bits, TPM2B_MAX_BUFFER  *result_key ) {

    TPM2B_MAX_BUFFER hash_input;
    TPM2B_DATA use;
    int bytes = ((size_in_bits + 7) / 8);
    int done;
    UINT32 counter, counter_be;
    UINT16 hash_size = tpm2_alg_util_get_hash_size(hash_alg);
    TSS2_RC rval = TPM2_RC_SUCCESS;

    memcpy(use.buffer, label, label_length);
    use.size = label_length;

    /*
     * Hash[i] := H(hash_input), where otherInfo := Use | PartyUInfo|PartyVInfo
     * hash_input := counter | Z | OtherInfo
     */
    hash_input.size = 4; // room for the counter
    tpm2_util_concat_buffer(&hash_input, (TPM2B *) Z);
    tpm2_util_concat_buffer(&hash_input, (TPM2B *) &use);
    tpm2_util_concat_buffer(&hash_input, (TPM2B *) party_u);
    tpm2_util_concat_buffer(&hash_input, (TPM2B *) party_v);

    digester d = tpm2_openssl_halg_to_digester(hash_alg);

    for (done = 0, counter = 1; done < bytes; done += hash_size, counter++) {
        counter_be = tpm2_util_hton_32(counter);
        memcpy(hash_input.buffer, &counter_be, 4);

        d(hash_input.buffer, hash_input.size, result_key->buffer + done);
    }
    // truncate the result to the desired size
    result_key->size = bytes;

    return rval;
}

static EC_POINT * tpm2_get_EC_public_key(TPM2B_PUBLIC *public) {
    EC_POINT *q = NULL;
    BIGNUM *bn_qx, *bn_qy;
    EC_KEY *key;
    const EC_GROUP *group;
    bool rval;
    TPMS_ECC_PARMS *tpm_ecc   = &public->publicArea.parameters.eccDetail;
    TPMS_ECC_POINT *tpm_point = &public->publicArea.unique.ecc;

    int nid = tpm2_ossl_curve_to_nid(tpm_ecc->curveID);
    if (nid < 0) {
        return NULL;
    }

    key = EC_KEY_new_by_curve_name(nid);
    if (!key) {
        LOG_ERR("Failed to create EC key from nid");
        return NULL;
    }

    bn_qx = BN_bin2bn(tpm_point->x.buffer, tpm_point->x.size, NULL);
    bn_qy = BN_bin2bn(tpm_point->y.buffer, tpm_point->y.size, NULL);
    if ((bn_qx == NULL) || (bn_qy == NULL)) {
        LOG_ERR("Could not convert EC public key to BN");
        goto out;
    }
    group = EC_KEY_get0_group(key);
    if (!group) {
        LOG_ERR("EC key missing group");
        goto out;
    }
    q = EC_POINT_new(group);
    if (q == NULL) {
        LOG_ERR("Could not allocate EC_POINT");
        goto out;
    }

    rval = EC_POINT_set_affine_coordinates_GFp(group, q, bn_qx, bn_qy, NULL);
    if (rval == false) {
        LOG_ERR("Could not set affine_coordinates");
        EC_POINT_free(q);
        q = NULL;
    }

out:
    if (bn_qx) {
        BN_free(bn_qx);
    }
    if (bn_qy) {
        BN_free(bn_qy);
    }
    if (key) {
        EC_KEY_free(key);
    }

    return q;
}


static bool get_public_key_from_ec_key(EC_KEY *key, TPMS_ECC_POINT *point) {
    BIGNUM *x = BN_new();
    BIGNUM *y = BN_new();
    const EC_POINT *pubkey = EC_KEY_get0_public_key(key);
    unsigned int nbx, nby;
    bool result = false;

    if ((x == NULL) || (y == NULL) || (pubkey == NULL)) {
        LOG_ERR("Failed to allocate memory to store EC public key.");
        goto out;
    }

    EC_POINT_get_affine_coordinates_GFp(EC_KEY_get0_group(key),
            pubkey, x, y, NULL);
    nbx = BN_num_bytes(x);
    nby = BN_num_bytes(y);
    if ((nbx > sizeof(point->x.buffer))||
            (nby > sizeof(point->y.buffer))) {
        LOG_ERR("EC public key has too many bits.");
        goto out;
    }

    point->x.size = nbx;
    point->y.size = nby;
    BN_bn2bin(x, point->x.buffer);
    BN_bn2bin(y, point->y.buffer);
    result = true;

out:
    if (x) {
        BN_free(x);
    }
    if (y) {
        BN_free(y);
    }
    return result;
}


static int get_ECDH_shared_secret(EC_KEY *key,
        const EC_POINT *p_pub, TPM2B_ECC_PARAMETER *secret) {

    int shared_secret_length;

    shared_secret_length = EC_GROUP_get_degree(EC_KEY_get0_group(key));
    shared_secret_length = (shared_secret_length + 7) / 8;
    if ((size_t) shared_secret_length > sizeof(secret->buffer)) {
        return -1;
    }
    secret->size = ECDH_compute_key(secret->buffer,
            shared_secret_length, p_pub, key, NULL);
    return secret->size;
}



bool ecdh_derive_seed_and_encrypted_seed(
        TPM2B_PUBLIC *parent_pub,
        unsigned char *label, int label_len,
        TPM2B_DIGEST *seed,
        TPM2B_ENCRYPTED_SECRET *out_sym_seed) {

    TPMS_ECC_PARMS *tpm_ecc = &parent_pub->publicArea.parameters.eccDetail;
    TPMI_ALG_HASH parent_name_alg = parent_pub->publicArea.nameAlg;
    UINT16 parent_hash_size = tpm2_alg_util_get_hash_size(parent_name_alg);
    bool result = false;
    EC_KEY *key = NULL;
    EC_POINT *qsv = NULL;
    TPMS_ECC_POINT qeu;
    bool qeu_is_valid;
    TPM2B_ECC_PARAMETER ecc_secret;

    // generate an ephemeral key
    int nid = tpm2_ossl_curve_to_nid(tpm_ecc->curveID);
    if (nid >= 0) {
        key = EC_KEY_new_by_curve_name(nid);
    }
    if (key == NULL) {
        LOG_ERR("Failed to create EC key from curveID");
        return false;
    }
    EC_KEY_generate_key(key);

    // get public key for the ephemeral key
    qeu_is_valid = get_public_key_from_ec_key(key, &qeu);
    if (qeu_is_valid == false) {
        LOG_ERR("Could not get the ECC public key");
        goto out;
    }

    /* marshal the public key to encrypted seed */
    size_t offset = 0;
    TSS2_RC rval;
    rval = Tss2_MU_TPMS_ECC_POINT_Marshal(&qeu,
            out_sym_seed->secret, sizeof(out_sym_seed->secret), &offset);
    if (rval != TPM2_RC_SUCCESS) {
        LOG_ERR("Error serializing the ECC public key");
        goto out;
    }
    out_sym_seed->size = offset;

    /* get parents public key */
    qsv = tpm2_get_EC_public_key(parent_pub);
    if (qsv == NULL) {
        LOG_ERR("Could not get parent's public key");
        goto out;
    }

    get_ECDH_shared_secret(key, qsv, &ecc_secret);

    /* derive seed using KDFe */
    TPM2B_ECC_PARAMETER *party_u_info = &qeu.x;
    TPM2B_ECC_PARAMETER *party_v_info = &parent_pub->publicArea.unique.ecc.x;
    tpm2_kdfe(parent_name_alg, &ecc_secret, label, label_len,
            party_u_info, party_v_info, parent_hash_size * 8,
            (TPM2B_MAX_BUFFER *) seed);

    result = true;

out:
    if (qsv) {
        EC_POINT_free(qsv);
    }
    if (key) {
        EC_KEY_free(key);
    }
    return result;
}
