/*******************************************************************************
 * Copyright 2017-2018, Fraunhofer SIT sponsored by Infineon Technologies AG
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of tpm2-tss-engine nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/

#include <stdlib.h>
#include <string.h>

#include <openssl/engine.h>
#include <openssl/pem.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>

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

#include "tpm2-tss-engine.h"
#include "tpm2-tss-engine-common.h"

/**
 * The identifier of the engine
 */
static const char *engine_id = "tpm2tss";

/**
 * The full name of the engine
 */
static const char *engine_name = "TPM2-TSS engine for OpenSSL";

TPM2B_DIGEST ownerauth = { .size = 0 };
TPM2B_DIGEST parentauth = { .size = 0 };

char *tcti_nameconf = NULL;

/** Retrieve password
 *
 * Helper function to retreive a password from the user.
 * @param prompt_info [in] The object name to ask the user for
 * @param ui_method [in] The ui method callbacks to be used
 * @param cb_data [in] The callback data for the ui
 * @param auth [out] The user provided password
 * @retval 1 on success
 * @retval 0 on failure
 */
static int
get_auth(const char *prompt_info, UI_METHOD *ui_method, void *cb_data,
         TPM2B_AUTH *auth)
{
    DBG("get_auth called for object %s with ui_method %p\n", prompt_info,
        ui_method);
    char *ui_prompt = NULL;
    UI *ui = NULL;
    if (!ui_method) {
        ERR(get_auth, TPM2TSS_R_UI_ERROR);
        goto error;
    }
    ui = UI_new_method(ui_method);
    if (!ui) {
        ERR(get_auth, TPM2TSS_R_UI_ERROR);
        goto error;
    }
    ui_prompt = UI_construct_prompt(ui, "password", prompt_info);
    if (!ui_prompt) {
        ERR(get_auth, TPM2TSS_R_UI_ERROR);
        goto error;
    }
    if (0 > UI_add_input_string(ui, ui_prompt, UI_INPUT_FLAG_DEFAULT_PWD,
                                (char *)&auth->buffer[0], 0,
                                sizeof(auth->buffer) - 1)) {
        ERR(get_auth, TPM2TSS_R_UI_ERROR);
        goto error;
    }
    UI_add_user_data(ui, cb_data);
    if (0 > UI_process(ui)) {
        ERR(get_auth, TPM2TSS_R_UI_ERROR);
        goto error;
    }
    auth->size = strlen((char *)&auth->buffer[0]);
    OPENSSL_free(ui_prompt);
    UI_free(ui);

    DBG("password is %s\n", (char *)&auth->buffer[0]);

    return 1;
 error:
    if (ui_prompt)
        OPENSSL_free(ui_prompt);
    if (ui)
        UI_free(ui);
    return 0;
}

static const ENGINE_CMD_DEFN cmd_defns[] = {
    { TPM2TSS_SET_OWNERAUTH, "SET_OWNERAUTH",
     "Set the password for the owner hierarchy (default none)",
     ENGINE_CMD_FLAG_STRING },
    { TPM2TSS_SET_TCTI, "SET_TCTI",
     "Set the TCTI module and options (default none)",
     ENGINE_CMD_FLAG_STRING },
    { TPM2TSS_SET_PARENTAUTH, "SET_PARENTAUTH",
     "Set the password for the parent key (default none)",
     ENGINE_CMD_FLAG_STRING },
    {0, NULL, NULL, 0}
};

static int
engine_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) ())
{
    (void)(e);
    (void)(i);
    (void)(f);
    switch (cmd) {
    case TPM2TSS_SET_OWNERAUTH:
        if (!p) {
            DBG("Setting owner auth to empty auth.\n");
            ownerauth.size = 0;
            return 1;
        }
        DBG("Setting owner auth to password.\n");
        if (strlen((char *)p) > sizeof(ownerauth.buffer) - 1) {
            return 0;
        }
        ownerauth.size = strlen((char *)p);
        memcpy(&ownerauth.buffer[0], p, ownerauth.size);
        return 1;
    case TPM2TSS_SET_TCTI:
        OPENSSL_free(tcti_nameconf);
        if (!p) {
            DBG("Setting TCTI to the ESAPI default\n");
        } else {
            tcti_nameconf = OPENSSL_strdup(p);
            DBG("Setting TCTI option to \"%s\"\n", tcti_nameconf);
        }
        return 1;
    case TPM2TSS_SET_PARENTAUTH:
        if (!p) {
            DBG("Setting parent auth to empty auth.\n");
            parentauth.size = 0;
            return 1;
        }
        DBG("Setting parent auth to password.\n");
        if (strlen((char *)p) > sizeof(parentauth.buffer) - 1) {
            return 0;
        }
        parentauth.size = strlen((char *)p);
        memcpy(&parentauth.buffer[0], p, parentauth.size);
        return 1;
    default:
        break;
    }
    ERR(engine_ctrl, TPM2TSS_R_UNKNOWN_CTRL);
    return 0;
}

/** Load a TPM2TSS key
 *
 * This function implements the prototype for loading a key from a file.
 * @param e The engine for this callback (unused).
 * @param key_id The name of the file with the TPM key data.
 * @param ui The ui functions for querying the user.
 * @param cb_data Callback data.
 */
static EVP_PKEY *
loadkey(ENGINE *e, const char *key_id, UI_METHOD *ui, void *cb_data)
{
    (void)(e);
    (void)(ui);
    (void)(cb_data);

    TPM2_DATA *tpm2Data = NULL;
    EVP_PKEY *pkey = NULL;

    DBG("Loading private key %s\n", key_id);
    if (strncmp(key_id, "0x81", 4) == 0) {
        uint32_t handle;
        sscanf(key_id, "0x%x", &handle);
        if (!tpm2tss_tpm2data_readtpm(handle, &tpm2Data)) {
            ERR(loadkey, TPM2TSS_R_TPM2DATA_READ_FAILED);
            goto error;
        }
    } else {
        if (!tpm2tss_tpm2data_read(key_id, &tpm2Data)) {
            ERR(loadkey, TPM2TSS_R_TPM2DATA_READ_FAILED);
            goto error;
        }
    }

    if (tpm2Data->emptyAuth) {
        tpm2Data->userauth.size = 0;
    } else {
        if (!get_auth("user key", ui, cb_data, &tpm2Data->userauth)) {
            goto error;
        }
    }

    DBG("Loaded key uses alg-id %x\n", tpm2Data->pub.publicArea.type);

    switch (tpm2Data->pub.publicArea.type) {
    case TPM2_ALG_RSA:
        pkey = tpm2tss_rsa_makekey(tpm2Data);
        break;
    case TPM2_ALG_ECC:
        pkey = tpm2tss_ecc_makekey(tpm2Data);
        break;
    default:
        ERR(loadkey, TPM2TSS_R_UNKNOWN_ALG);
        goto error;
    }
    if (!pkey) {
        ERR(loadkey, TPM2TSS_R_CANNOT_MAKE_KEY);
        goto error;
    }

    DBG("TPM2 Key loaded\n");

    return pkey;
error:
    if (tpm2Data)
        OPENSSL_free(tpm2Data);
    return NULL;
}

/** Initialize the tpm2tss engine
 *
 * Initialize the tpm2tss engine by calling each of the submodules' init
 * functions for setting function pointer.
 * @param e The engine context.
 * @retval 1 on success
 * @retval 0 on failure
 */
static int
init_engine(ENGINE *e) {
    static int initialized = 0;

    DBG("Initializing\n");

    if (initialized) {
        DBG("Already initialized\n");
        return 1;
    }

    int rc;

#ifdef ENABLE_TCTIENVVAR
    /*  Set the default TCTI option from the environment */
    OPENSSL_free(tcti_nameconf);
    tcti_nameconf = OPENSSL_strdup(getenv("TPM2TSSENGINE_TCTI"));
#endif

    rc = init_rand(e);
    if (rc != 1) {
        ERR(init_engine, TPM2TSS_R_SUBINIT_FAILED);
        return rc;
    }

    rc = init_rsa(e);
    if (rc != 1) {
        ERR(init_engine, TPM2TSS_R_SUBINIT_FAILED);
        return rc;
    }

    rc = init_ecc(e);
    if (rc != 1) {
        ERR(init_engine, TPM2TSS_R_SUBINIT_FAILED);
        return rc;
    }

    initialized = 1;
    return 1;
}

/** Destroys the engine context
 *
 * Unloads the strings of the tpm2tss engine.
 * @param e The engine context (unused).
 * @retval 1 for success
 */
static int
destroy_engine(ENGINE *e)
{
    (void)(e);
    OPENSSL_free(tcti_nameconf);
    ERR_unload_TPM2TSS_strings();
    return 1;
}

/** OpenSSL's method to bind an engine.
 *
 * This initializes the name, id and function pointers of the engine.
 * @param e The TPM engine to initialize
 * @param id The identifier of the engine
 * @retval 0 if binding failed
 * @retval 1 on success
 */
static int
bind(ENGINE *e, const char *id)
{
    (void)(id);

    if (!ENGINE_set_id(e, engine_id)) {
        DBG("ENGINE_set_id failed\n");
        goto end;
    }
    if (!ENGINE_set_name(e, engine_name)) {
        DBG("ENGINE_set_name failed\n");
        goto end;
    }

    /* The init function is not allways called so we initialize crypto methods
       directly from bind. */
    if (!init_engine(e)) {
        DBG("tpm2tss enigne initialization failed\n");
        goto end;
    }

    if (!ENGINE_set_load_privkey_function(e, loadkey)) {
        DBG("ENGINE_set_load_privkey_function failed\n");
        goto end;
    }

    if (!ENGINE_set_destroy_function(e, destroy_engine)) {
        DBG("ENGINE_set_destroy_function failed\n");
        goto end;
    }

    if (!ENGINE_set_ctrl_function(e, engine_ctrl)) {
        DBG("ENGINE_set_ctrl_function failed\n");
        goto end;
    }

    if (!ENGINE_set_cmd_defns(e, cmd_defns)) {
        DBG("ENGINE_set_cmd_defns failed\n");
        goto end;
    }

    ERR_load_TPM2TSS_strings();
    return 1;
 end:
    return 0;
}

IMPLEMENT_DYNAMIC_BIND_FN(bind)
IMPLEMENT_DYNAMIC_CHECK_FN()
