/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "blapi.h"
#include "ec.h"
#include "ecl-curve.h"
#include "prprf.h"
#include "basicutil.h"
#include "pkcs11.h"
#include "nspr.h"
#include <stdio.h>

#define __PASTE(x, y) x##y

/*
 * Get the NSS specific PKCS #11 function names.
 */
#undef CK_PKCS11_FUNCTION_INFO
#undef CK_NEED_ARG_LIST

#define CK_EXTERN extern
#define CK_PKCS11_FUNCTION_INFO(func) \
    CK_RV __PASTE(NS, func)
#define CK_NEED_ARG_LIST 1

#include "pkcs11f.h"

typedef SECStatus (*op_func)(void *, void *, void *);
typedef SECStatus (*pk11_op_func)(CK_SESSION_HANDLE, void *, void *, void *);

typedef struct ThreadDataStr {
    op_func op;
    void *p1;
    void *p2;
    void *p3;
    int iters;
    PRLock *lock;
    int count;
    SECStatus status;
    int isSign;
} ThreadData;

typedef SECItem SECKEYECParams;

void
PKCS11Thread(void *data)
{
    ThreadData *threadData = (ThreadData *)data;
    pk11_op_func op = (pk11_op_func)threadData->op;
    int iters = threadData->iters;
    unsigned char sigData[256];
    SECItem sig;
    CK_SESSION_HANDLE session;
    CK_RV crv;
    void *tmp = NULL;

    threadData->status = SECSuccess;
    threadData->count = 0;

    /* get our thread's session */
    PR_Lock(threadData->lock);
    crv = NSC_OpenSession(1, CKF_SERIAL_SESSION, NULL, 0, &session);
    PR_Unlock(threadData->lock);
    if (crv != CKR_OK) {
        return;
    }

    if (threadData->isSign) {
        sig.data = sigData;
        sig.len = sizeof(sigData);
        tmp = threadData->p2;
        threadData->p2 = (void *)&sig;
    }

    while (iters--) {
        threadData->status = (*op)(session, threadData->p1,
                                   threadData->p2, threadData->p3);
        if (threadData->status != SECSuccess) {
            break;
        }
        threadData->count++;
    }

    if (threadData->isSign) {
        threadData->p2 = tmp;
    }
    return;
}

void
genericThread(void *data)
{
    ThreadData *threadData = (ThreadData *)data;
    int iters = threadData->iters;
    unsigned char sigData[256];
    SECItem sig;
    void *tmp = NULL;

    threadData->status = SECSuccess;
    threadData->count = 0;

    if (threadData->isSign) {
        sig.data = sigData;
        sig.len = sizeof(sigData);
        tmp = threadData->p2;
        threadData->p2 = (void *)&sig;
    }

    while (iters--) {
        threadData->status = (*threadData->op)(threadData->p1,
                                               threadData->p2, threadData->p3);
        if (threadData->status != SECSuccess) {
            break;
        }
        threadData->count++;
    }

    if (threadData->isSign) {
        threadData->p2 = tmp;
    }
    return;
}

/* Time iter repetitions of operation op. */
SECStatus
M_TimeOperation(void (*threadFunc)(void *),
                op_func opfunc, char *op, void *param1, void *param2,
                void *param3, int iters, int numThreads, PRLock *lock,
                CK_SESSION_HANDLE session, int isSign, double *rate)
{
    double dUserTime;
    int i, total;
    PRIntervalTime startTime, totalTime;
    PRThread **threadIDs;
    ThreadData *threadData;
    pk11_op_func pk11_op = (pk11_op_func)opfunc;
    SECStatus rv;

    /* verify operation works before testing performance */
    if (session) {
        rv = (*pk11_op)(session, param1, param2, param3);
    } else {
        rv = (*opfunc)(param1, param2, param3);
    }
    if (rv != SECSuccess) {
        SECU_PrintError("Error:", op);
        return rv;
    }

    /* get Data structures */
    threadIDs = (PRThread **)PORT_Alloc(numThreads * sizeof(PRThread *));
    threadData = (ThreadData *)PORT_Alloc(numThreads * sizeof(ThreadData));

    startTime = PR_Now();
    if (numThreads == 1) {
        for (i = 0; i < iters; i++) {
            if (session) {
                rv = (*pk11_op)(session, param1, param2, param3);
            } else {
                rv = (*opfunc)(param1, param2, param3);
            }
            if (rv != SECSuccess) {
                PORT_Free(threadIDs);
                PORT_Free(threadData);
                SECU_PrintError("Error:", op);
                return rv;
            }
        }
        total = iters;
    } else {
        for (i = 0; i < numThreads; i++) {
            threadData[i].op = opfunc;
            threadData[i].p1 = (void *)param1;
            threadData[i].p2 = (void *)param2;
            threadData[i].p3 = (void *)param3;
            threadData[i].iters = iters;
            threadData[i].lock = lock;
            threadData[i].isSign = isSign;
            threadIDs[i] = PR_CreateThread(PR_USER_THREAD, threadFunc,
                                           (void *)&threadData[i], PR_PRIORITY_NORMAL,
                                           PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0);
        }

        total = 0;
        for (i = 0; i < numThreads; i++) {
            PR_JoinThread(threadIDs[i]);
            /* check the status */
            total += threadData[i].count;
        }
    }

    totalTime = PR_Now() - startTime;
    /* SecondsToInterval seems to be broken here ... */
    dUserTime = (double)totalTime / (double)1000000;
    if (dUserTime) {
        printf("    %-15s count:%4d sec: %3.2f op/sec: %6.2f\n",
               op, total, dUserTime, (double)total / dUserTime);
        if (rate) {
            *rate = ((double)total) / dUserTime;
        }
    }
    PORT_Free(threadIDs);
    PORT_Free(threadData);

    return SECSuccess;
}

/* Test curve using specific field arithmetic. */
#define ECTEST_NAMED(name_c, name_v)                                   \
    if (usefreebl) {                                                   \
        printf("Testing %s using freebl implementation...\n", name_c); \
        rv = ectest_curve_freebl(name_v, iterations, numThreads);      \
        if (rv != SECSuccess)                                          \
            goto cleanup;                                              \
        printf("... okay.\n");                                         \
    }                                                                  \
    if (usepkcs11) {                                                   \
        printf("Testing %s using pkcs11 implementation...\n", name_c); \
        rv = ectest_curve_pkcs11(name_v, iterations, numThreads);      \
        if (rv != SECSuccess)                                          \
            goto cleanup;                                              \
        printf("... okay.\n");                                         \
    }

#define PK11_SETATTRS(x, id, v, l) \
    (x)->type = (id);              \
    (x)->pValue = (v);             \
    (x)->ulValueLen = (l);

SECStatus
PKCS11_Derive(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE *hKey,
              CK_MECHANISM *pMech, int *dummy)
{
    CK_RV crv;
    CK_OBJECT_HANDLE newKey;
    CK_BBOOL cktrue = CK_TRUE;
    CK_OBJECT_CLASS keyClass = CKO_SECRET_KEY;
    CK_KEY_TYPE keyType = CKK_GENERIC_SECRET;
    CK_ATTRIBUTE keyTemplate[3];
    CK_ATTRIBUTE *attrs = keyTemplate;

    PK11_SETATTRS(attrs, CKA_CLASS, &keyClass, sizeof(keyClass));
    attrs++;
    PK11_SETATTRS(attrs, CKA_KEY_TYPE, &keyType, sizeof(keyType));
    attrs++;
    PK11_SETATTRS(attrs, CKA_DERIVE, &cktrue, 1);
    attrs++;

    crv = NSC_DeriveKey(session, pMech, *hKey, keyTemplate, 3, &newKey);
    if (crv != CKR_OK) {
        printf("Derive Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }
    return SECSuccess;
}

SECStatus
PKCS11_Sign(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE *hKey,
            SECItem *sig, SECItem *digest)
{
    CK_RV crv;
    CK_MECHANISM mech;
    CK_ULONG sigLen = sig->len;

    mech.mechanism = CKM_ECDSA;
    mech.pParameter = NULL;
    mech.ulParameterLen = 0;

    crv = NSC_SignInit(session, &mech, *hKey);
    if (crv != CKR_OK) {
        printf("Sign Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }
    crv = NSC_Sign(session, digest->data, digest->len, sig->data, &sigLen);
    if (crv != CKR_OK) {
        printf("Sign Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }
    sig->len = (unsigned int)sigLen;
    return SECSuccess;
}

SECStatus
PKCS11_Verify(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE *hKey,
              SECItem *sig, SECItem *digest)
{
    CK_RV crv;
    CK_MECHANISM mech;

    mech.mechanism = CKM_ECDSA;
    mech.pParameter = NULL;
    mech.ulParameterLen = 0;

    crv = NSC_VerifyInit(session, &mech, *hKey);
    if (crv != CKR_OK) {
        printf("Verify Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }
    crv = NSC_Verify(session, digest->data, digest->len, sig->data, sig->len);
    if (crv != CKR_OK) {
        printf("Verify Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }
    return SECSuccess;
}

/* Performs basic tests of elliptic curve cryptography over prime fields.
 * If tests fail, then it prints an error message, aborts, and returns an
 * error code. Otherwise, returns 0. */
SECStatus
ectest_curve_pkcs11(ECCurveName curve, int iterations, int numThreads)
{
    CK_OBJECT_HANDLE ecPriv;
    CK_OBJECT_HANDLE ecPub;
    CK_SESSION_HANDLE session;
    SECItem sig;
    SECItem digest;
    SECKEYECParams ecParams;
    CK_MECHANISM mech;
    CK_ECDH1_DERIVE_PARAMS ecdh_params;
    unsigned char sigData[256];
    unsigned char digestData[20];
    unsigned char pubKeyData[256];
    PRLock *lock = NULL;
    double signRate, deriveRate = 0;
    CK_ATTRIBUTE template;
    SECStatus rv;
    CK_RV crv;

    ecParams.data = NULL;
    ecParams.len = 0;
    rv = SECU_ecName2params(curve, &ecParams);
    if (rv != SECSuccess) {
        goto cleanup;
    }

    crv = NSC_OpenSession(1, CKF_SERIAL_SESSION, NULL, 0, &session);
    if (crv != CKR_OK) {
        printf("OpenSession Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }

    PORT_Memset(digestData, 0xa5, sizeof(digestData));
    digest.data = digestData;
    digest.len = sizeof(digestData);
    sig.data = sigData;
    sig.len = sizeof(sigData);

    template.type = CKA_EC_PARAMS;
    template.pValue = ecParams.data;
    template.ulValueLen = ecParams.len;
    mech.mechanism = CKM_EC_KEY_PAIR_GEN;
    mech.pParameter = NULL;
    mech.ulParameterLen = 0;
    crv = NSC_GenerateKeyPair(session, &mech,
                              &template, 1, NULL, 0, &ecPub, &ecPriv);
    if (crv != CKR_OK) {
        printf("GenerateKeyPair Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }

    template.type = CKA_EC_POINT;
    template.pValue = pubKeyData;
    template.ulValueLen = sizeof(pubKeyData);
    crv = NSC_GetAttributeValue(session, ecPub, &template, 1);
    if (crv != CKR_OK) {
        printf("GenerateKeyPair Failed CK_RV=0x%x\n", (int)crv);
        return SECFailure;
    }

    ecdh_params.kdf = CKD_NULL;
    ecdh_params.ulSharedDataLen = 0;
    ecdh_params.pSharedData = NULL;
    ecdh_params.ulPublicDataLen = template.ulValueLen;
    ecdh_params.pPublicData = template.pValue;

    mech.mechanism = CKM_ECDH1_DERIVE;
    mech.pParameter = (void *)&ecdh_params;
    mech.ulParameterLen = sizeof(ecdh_params);

    lock = PR_NewLock();

    if (ecCurve_map[curve]->usage & KU_KEY_AGREEMENT) {
        rv = M_TimeOperation(PKCS11Thread, (op_func)PKCS11_Derive, "ECDH_Derive",
                             &ecPriv, &mech, NULL, iterations, numThreads,
                             lock, session, 0, &deriveRate);
        if (rv != SECSuccess) {
            goto cleanup;
        }
    }

    if (ecCurve_map[curve]->usage & KU_DIGITAL_SIGNATURE) {
        rv = M_TimeOperation(PKCS11Thread, (op_func)PKCS11_Sign, "ECDSA_Sign",
                             (void *)&ecPriv, &sig, &digest, iterations, numThreads,
                             lock, session, 1, &signRate);
        if (rv != SECSuccess) {
            goto cleanup;
        }
        printf("        ECDHE max rate = %.2f\n", (deriveRate + signRate) / 4.0);
        /* get a signature */
        rv = PKCS11_Sign(session, &ecPriv, &sig, &digest);
        if (rv != SECSuccess) {
            goto cleanup;
        }
        rv = M_TimeOperation(PKCS11Thread, (op_func)PKCS11_Verify, "ECDSA_Verify",
                             (void *)&ecPub, &sig, &digest, iterations, numThreads,
                             lock, session, 0, NULL);
        if (rv != SECSuccess) {
            goto cleanup;
        }
    }

cleanup:
    if (lock) {
        PR_DestroyLock(lock);
    }
    return rv;
}

SECStatus
ECDH_DeriveWrap(ECPrivateKey *priv, ECPublicKey *pub, int *dummy)
{
    SECItem secret;
    unsigned char secretData[256];
    SECStatus rv;

    secret.data = secretData;
    secret.len = sizeof(secretData);

    rv = ECDH_Derive(&pub->publicValue, &pub->ecParams,
                     &priv->privateValue, 0, &secret);
    SECITEM_FreeItem(&secret, PR_FALSE);
    return rv;
}

/* Performs basic tests of elliptic curve cryptography over prime fields.
 * If tests fail, then it prints an error message, aborts, and returns an
 * error code. Otherwise, returns 0. */
SECStatus
ectest_curve_freebl(ECCurveName curve, int iterations, int numThreads)
{
    ECParams ecParams = { 0 };
    ECPrivateKey *ecPriv = NULL;
    ECPublicKey ecPub;
    SECItem sig;
    SECItem digest;
    unsigned char sigData[256];
    unsigned char digestData[20];
    double signRate, deriveRate = 0;
    SECStatus rv = SECFailure;
    PLArenaPool *arena;
    SECItem ecEncodedParams = { siBuffer, NULL, 0 };

    arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE);
    if (!arena) {
        return SECFailure;
    }

    if ((curve < ECCurve_noName) || (curve > ECCurve_pastLastCurve)) {
        PORT_FreeArena(arena, PR_FALSE);
        return SECFailure;
    }

    rv = SECU_ecName2params(curve, &ecEncodedParams);
    if (rv != SECSuccess) {
        goto cleanup;
    }
    EC_FillParams(arena, &ecEncodedParams, &ecParams);

    PORT_Memset(digestData, 0xa5, sizeof(digestData));
    digest.data = digestData;
    digest.len = sizeof(digestData);
    sig.data = sigData;
    sig.len = sizeof(sigData);

    rv = EC_NewKey(&ecParams, &ecPriv);
    if (rv != SECSuccess) {
        goto cleanup;
    }
    ecPub.ecParams = ecParams;
    ecPub.publicValue = ecPriv->publicValue;

    if (ecCurve_map[curve]->usage & KU_KEY_AGREEMENT) {
        rv = M_TimeOperation(genericThread, (op_func)ECDH_DeriveWrap, "ECDH_Derive",
                             ecPriv, &ecPub, NULL, iterations, numThreads, 0, 0, 0, &deriveRate);
        if (rv != SECSuccess) {
            goto cleanup;
        }
    }

    if (ecCurve_map[curve]->usage & KU_DIGITAL_SIGNATURE) {
        rv = M_TimeOperation(genericThread, (op_func)ECDSA_SignDigest, "ECDSA_Sign",
                             ecPriv, &sig, &digest, iterations, numThreads, 0, 0, 1, &signRate);
        if (rv != SECSuccess)
            goto cleanup;
        printf("        ECDHE max rate = %.2f\n", (deriveRate + signRate) / 4.0);
        rv = ECDSA_SignDigest(ecPriv, &sig, &digest);
        if (rv != SECSuccess) {
            goto cleanup;
        }
        rv = M_TimeOperation(genericThread, (op_func)ECDSA_VerifyDigest, "ECDSA_Verify",
                             &ecPub, &sig, &digest, iterations, numThreads, 0, 0, 0, NULL);
        if (rv != SECSuccess) {
            goto cleanup;
        }
    }

cleanup:
    SECITEM_FreeItem(&ecEncodedParams, PR_FALSE);
    PORT_FreeArena(arena, PR_FALSE);
    if (ecPriv) {
        PORT_FreeArena(ecPriv->ecParams.arena, PR_FALSE);
    }
    return rv;
}

/* Prints help information. */
void
printUsage(char *prog)
{
    printf("Usage: %s [-i iterations] [-t threads ] [-ans] [-fp] [-Al]\n"
           "-a: ansi\n-n: nist\n-s: secp\n-f: usefreebl\n-p: usepkcs11\n-A: all\n",
           prog);
}

/* Performs tests of elliptic curve cryptography over prime fields If
 * tests fail, then it prints an error message, aborts, and returns an
 * error code. Otherwise, returns 0. */
int
main(int argv, char **argc)
{
    int ansi = 0;
    int nist = 0;
    int secp = 0;
    int usefreebl = 0;
    int usepkcs11 = 0;
    int i;
    SECStatus rv = SECSuccess;
    int iterations = 100;
    int numThreads = 1;

    const CK_C_INITIALIZE_ARGS pk11args = {
        NULL, NULL, NULL, NULL, CKF_LIBRARY_CANT_CREATE_OS_THREADS,
        (void *)"flags=readOnly,noCertDB,noModDB", NULL
    };

    /* read command-line arguments */
    for (i = 1; i < argv; i++) {
        if (PL_strcasecmp(argc[i], "-i") == 0) {
            i++;
            iterations = atoi(argc[i]);
        } else if (PL_strcasecmp(argc[i], "-t") == 0) {
            i++;
            numThreads = atoi(argc[i]);
        } else if (PL_strcasecmp(argc[i], "-A") == 0) {
            ansi = nist = secp = 1;
            usepkcs11 = usefreebl = 1;
        } else if (PL_strcasecmp(argc[i], "-a") == 0) {
            ansi = 1;
        } else if (PL_strcasecmp(argc[i], "-n") == 0) {
            nist = 1;
        } else if (PL_strcasecmp(argc[i], "-s") == 0) {
            secp = 1;
        } else if (PL_strcasecmp(argc[i], "-p") == 0) {
            usepkcs11 = 1;
        } else if (PL_strcasecmp(argc[i], "-f") == 0) {
            usefreebl = 1;
        } else {
            printUsage(argc[0]);
            return 0;
        }
    }

    if ((ansi | nist | secp) == 0) {
        nist = 1;
    }
    if ((usepkcs11 | usefreebl) == 0) {
        usefreebl = 1;
    }

    rv = RNG_RNGInit();
    if (rv != SECSuccess) {
        SECU_PrintError("Error:", "RNG_RNGInit");
        return -1;
    }
    RNG_SystemInfoForRNG();

    rv = SECOID_Init();
    if (rv != SECSuccess) {
        SECU_PrintError("Error:", "SECOID_Init");
        goto cleanup;
    }

    if (usepkcs11) {
        CK_RV crv = NSC_Initialize((CK_VOID_PTR)&pk11args);
        if (crv != CKR_OK) {
            fprintf(stderr, "NSC_Initialize failed crv=0x%x\n", (unsigned int)crv);
            return SECFailure;
        }
    }

    /* specific arithmetic tests */
    if (nist) {
        ECTEST_NAMED("NIST-P256", ECCurve_NIST_P256);
        ECTEST_NAMED("NIST-P384", ECCurve_NIST_P384);
        ECTEST_NAMED("NIST-P521", ECCurve_NIST_P521);
        ECTEST_NAMED("Curve25519", ECCurve25519);
    }

cleanup:
    rv |= SECOID_Shutdown();
    RNG_RNGShutdown();

    if (rv != SECSuccess) {
        printf("Error: exiting with error value\n");
    }
    return rv;
}
