/* 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 "seccomon.h"
#include "cert.h"
#include "secutil.h"
#include "nspr.h"
#include "nss.h"
#include "blapi.h"
#include "plgetopt.h"
#include "lowkeyi.h"
#include "pk11pub.h"


#define DEFAULT_ITERS           10
#define DEFAULT_DURATION        10
#define DEFAULT_KEY_BITS        1024
#define MIN_KEY_BITS            512
#define MAX_KEY_BITS            65536
#define BUFFER_BYTES            MAX_KEY_BITS / 8
#define DEFAULT_THREADS         1
#define DEFAULT_EXPONENT        0x10001

extern NSSLOWKEYPrivateKey * getDefaultRSAPrivateKey(void);
extern NSSLOWKEYPublicKey  * getDefaultRSAPublicKey(void);

secuPWData pwData = { PW_NONE, NULL };

typedef struct TimingContextStr TimingContext;

struct TimingContextStr {
    PRTime start;
    PRTime end;
    PRTime interval;

    long  days;     
    int   hours;    
    int   minutes;  
    int   seconds;  
    int   millisecs;
};

TimingContext *CreateTimingContext(void) {
    return PORT_Alloc(sizeof(TimingContext));
}

void DestroyTimingContext(TimingContext *ctx) {
    PORT_Free(ctx);
}

void TimingBegin(TimingContext *ctx, PRTime begin) {
    ctx->start = begin;
}

static void timingUpdate(TimingContext *ctx) {
    PRInt64 tmp, remaining;
    PRInt64 L1000,L60,L24;

    LL_I2L(L1000,1000);
    LL_I2L(L60,60);
    LL_I2L(L24,24);

    LL_DIV(remaining, ctx->interval, L1000);
    LL_MOD(tmp, remaining, L1000);
    LL_L2I(ctx->millisecs, tmp);
    LL_DIV(remaining, remaining, L1000);
    LL_MOD(tmp, remaining, L60);
    LL_L2I(ctx->seconds, tmp);
    LL_DIV(remaining, remaining, L60);
    LL_MOD(tmp, remaining, L60);
    LL_L2I(ctx->minutes, tmp);
    LL_DIV(remaining, remaining, L60);
    LL_MOD(tmp, remaining, L24);
    LL_L2I(ctx->hours, tmp);
    LL_DIV(remaining, remaining, L24);
    LL_L2I(ctx->days, remaining);
}

void TimingEnd(TimingContext *ctx, PRTime end) {
    ctx->end = end;
    LL_SUB(ctx->interval, ctx->end, ctx->start);
    PORT_Assert(LL_GE_ZERO(ctx->interval));
    timingUpdate(ctx);
}

void TimingDivide(TimingContext *ctx, int divisor) {
    PRInt64 tmp;

    LL_I2L(tmp, divisor);
    LL_DIV(ctx->interval, ctx->interval, tmp);

    timingUpdate(ctx);
}

char *TimingGenerateString(TimingContext *ctx) {
    char *buf = NULL;

    if (ctx->days != 0) {
	buf = PR_sprintf_append(buf, "%d days", ctx->days);
    }
    if (ctx->hours != 0) {
	if (buf != NULL) buf = PR_sprintf_append(buf, ", ");
	buf = PR_sprintf_append(buf, "%d hours", ctx->hours);
    }
    if (ctx->minutes != 0) {
	if (buf != NULL) buf = PR_sprintf_append(buf, ", ");
	buf = PR_sprintf_append(buf, "%d minutes", ctx->minutes);
    }
    if (buf != NULL) buf = PR_sprintf_append(buf, ", and ");
    if (!buf && ctx->seconds == 0) {
	int interval;
	LL_L2I(interval, ctx->interval);
    	if (ctx->millisecs < 100) 
	    buf = PR_sprintf_append(buf, "%d microseconds", interval);
	else
	    buf = PR_sprintf_append(buf, "%d milliseconds", ctx->millisecs);
    } else if (ctx->millisecs == 0) {
	buf = PR_sprintf_append(buf, "%d seconds", ctx->seconds);
    } else {
	buf = PR_sprintf_append(buf, "%d.%03d seconds",
				ctx->seconds, ctx->millisecs);
    }
    return buf;
}

void
Usage(char *progName)
{
    fprintf(stderr, "Usage: %s [-s | -e] [-i iterations | -p period] "
            "[-t threads]\n[-n none [-k keylength] [ [-g] -x exponent] |\n"
            " -n token:nickname [-d certdir] [-w password] |\n"
            " -h token [-d certdir] [-w password] [-g] [-k keylength] "
            "[-x exponent] [-f pwfile]\n",
	    progName);
    fprintf(stderr, "%-20s Cert database directory (default is ~/.netscape)\n",
	    "-d certdir");
    fprintf(stderr, "%-20s How many operations to perform\n", "-i iterations");
    fprintf(stderr, "%-20s How many seconds to run\n", "-p period");
    fprintf(stderr, "%-20s Perform signing (private key) operations\n", "-s");
    fprintf(stderr, "%-20s Perform encryption (public key) operations\n","-e");
    fprintf(stderr, "%-20s Nickname of certificate or key, prefixed "
            "by optional token name\n", "-n nickname");
    fprintf(stderr, "%-20s PKCS#11 token to perform operation with.\n",
            "-h token");
    fprintf(stderr, "%-20s key size in bits, from %d to %d\n", "-k keylength",
            MIN_KEY_BITS, MAX_KEY_BITS);
    fprintf(stderr, "%-20s token password\n", "-w password");
    fprintf(stderr, "%-20s temporary key generation. Not for token keys.\n",
            "-g");
    fprintf(stderr, "%-20s set public exponent for keygen\n", "-x");
    fprintf(stderr, "%-20s Number of execution threads (default 1)\n",
            "-t threads");
    exit(-1);
}

static void
dumpBytes( unsigned char * b, int l)
{
    int i;
    if (l <= 0)
        return;
    for (i = 0; i < l; ++i) {
        if (i % 16 == 0)
            printf("\t");
        printf(" %02x", b[i]);
        if (i % 16 == 15)
            printf("\n");
    }
    if ((i % 16) != 0)
        printf("\n");
}

static void
dumpItem( SECItem * item, const char * description)
{
    if (item->len & 1 && item->data[0] == 0) {
	printf("%s: (%d bytes)\n", description, item->len - 1);
	dumpBytes(item->data + 1, item->len - 1);
    } else {
	printf("%s: (%d bytes)\n", description, item->len);
	dumpBytes(item->data, item->len);
    }
}

void
printPrivKey(NSSLOWKEYPrivateKey * privKey)
{
    RSAPrivateKey *rsa = &privKey->u.rsa;

    dumpItem( &rsa->modulus, 		"n");
    dumpItem( &rsa->publicExponent, 	"e");
    dumpItem( &rsa->privateExponent, 	"d");
    dumpItem( &rsa->prime1, 		"P");
    dumpItem( &rsa->prime2, 		"Q");
    dumpItem( &rsa->exponent1, 	        "d % (P-1)");
    dumpItem( &rsa->exponent2, 	        "d % (Q-1)");
    dumpItem( &rsa->coefficient, 	"(Q ** -1) % P");
    puts("");
}

typedef SECStatus (* RSAOp)(void *               key, 
			    unsigned char *      output,
		            unsigned char *      input);

typedef struct {
    SECKEYPublicKey* pubKey;
    SECKEYPrivateKey* privKey;
} PK11Keys;


SECStatus PK11_PublicKeyOp (SECKEYPublicKey*     key,
			    unsigned char *      output,
		            unsigned char *      input)
{
    return PK11_PubEncryptRaw(key, output, input, key->u.rsa.modulus.len,
                              NULL);
}

SECStatus PK11_PrivateKeyOp (PK11Keys*            keys,
			     unsigned char *      output,
		             unsigned char *      input)
{
    unsigned outLen = 0;
    return PK11_PrivDecryptRaw(keys->privKey,
                               output, &outLen,
                               keys->pubKey->u.rsa.modulus.len, input,
                               keys->pubKey->u.rsa.modulus.len);
}
typedef struct ThreadRunDataStr ThreadRunData;

struct ThreadRunDataStr {
    const PRBool        *doIters;
    const void          *rsaKey;
    const unsigned char *buf;
    RSAOp                fn;
    int                  seconds;
    long                 iters;
    long                 iterRes;
    PRErrorCode          errNum;
    SECStatus            status;
};


void ThreadExecFunction(void *data)
{
    ThreadRunData *tdata = (ThreadRunData*)data;
    unsigned char buf2[BUFFER_BYTES];

    tdata->status = SECSuccess;
    if (*tdata->doIters) {
        long i = tdata->iters;
        tdata->iterRes = 0;
        while (i--) {
            SECStatus rv = tdata->fn((void*)tdata->rsaKey, buf2,
                                     (unsigned char*)tdata->buf);
            if (rv != SECSuccess) {
                tdata->errNum = PORT_GetError();
                tdata->status = rv;
                break;
            }
            tdata->iterRes++;
        }
    } else {
        PRIntervalTime total = PR_SecondsToInterval(tdata->seconds);
        PRIntervalTime start = PR_IntervalNow();
        tdata->iterRes = 0;
        while (PR_IntervalNow() - start < total) {
            SECStatus rv = tdata->fn((void*)tdata->rsaKey, buf2,
                                     (unsigned char*)tdata->buf);
            if (rv != SECSuccess) {
                tdata->errNum = PORT_GetError();
                tdata->status = rv;
                break;
            }
            tdata->iterRes++;
        }
    }
}

#define INT_ARG(arg,def) atol(arg)>0?atol(arg):def

int
main(int argc, char **argv)
{
    TimingContext *	  timeCtx       = NULL;
    SECKEYPublicKey *	  pubHighKey    = NULL;
    SECKEYPrivateKey *    privHighKey   = NULL;
    NSSLOWKEYPrivateKey * privKey       = NULL;
    NSSLOWKEYPublicKey *  pubKey        = NULL;
    CERTCertificate *	  cert          = NULL;
    char *		  progName      = NULL;
    char *		  secDir 	= NULL;
    char *		  nickname 	= NULL;
    char *                slotname      = NULL;
    long                  keybits     = 0;
    RSAOp                 fn;
    void *                rsaKey        = NULL;
    PLOptState *          optstate;
    PLOptStatus           optstatus;
    long 		  iters 	= DEFAULT_ITERS;
    int		 	  i;
    PRBool 		  doPriv 	= PR_FALSE;
    PRBool 		  doPub 	= PR_FALSE;
    int 		  rv;
    unsigned char 	  buf[BUFFER_BYTES];
    unsigned char 	  buf2[BUFFER_BYTES];
    int                   seconds       = DEFAULT_DURATION;
    PRBool                doIters       = PR_FALSE;
    PRBool                doTime        = PR_FALSE;
    PRBool                useTokenKey   = PR_FALSE; /* use PKCS#11 token
                                                       object key */
    PRBool                useSessionKey = PR_FALSE; /* use PKCS#11 session
                                                       object key */
    PRBool                useBLKey = PR_FALSE;      /* use freebl */
    PK11SlotInfo*         slot          = NULL;     /* slot for session
                                                       object key operations */
    PRBool                doKeyGen      = PR_FALSE;
    int                   publicExponent  = DEFAULT_EXPONENT;
    PK11Keys keys;
    int peCount = 0;
    CK_BYTE pubEx[4];
    SECItem pe;
    RSAPublicKey          pubKeyStr;
    int                   threadNum     = DEFAULT_THREADS;
    ThreadRunData      ** runDataArr = NULL;
    PRThread           ** threadsArr = NULL;
    int                   calcThreads = 0;

    progName = strrchr(argv[0], '/');
    if (!progName)
	progName = strrchr(argv[0], '\\');
    progName = progName ? progName+1 : argv[0];

    optstate = PL_CreateOptState(argc, argv, "d:ef:gh:i:k:n:p:st:w:x:");
    while ((optstatus = PL_GetNextOpt(optstate)) == PL_OPT_OK) {
	switch (optstate->option) {
	case '?':
	    Usage(progName);
	    break;
	case 'd':
	    secDir = PORT_Strdup(optstate->value);
	    break;
	case 'i':
	    iters = INT_ARG(optstate->value, DEFAULT_ITERS);
	    doIters = PR_TRUE;
	    break;
	case 's':
	    doPriv = PR_TRUE;
	    break;
	case 'e':
	    doPub = PR_TRUE;
	    break;
	case 'g':
	    doKeyGen = PR_TRUE;
	    break;
	case 'n':
	    nickname = PORT_Strdup(optstate->value);
            /* for compatibility, nickname of "none" means go to freebl */
            if (nickname && strcmp(nickname, "none")) {
	        useTokenKey = PR_TRUE;
            } else {
                useBLKey = PR_TRUE;
            }
	    break;
	case 'p':
	    seconds = INT_ARG(optstate->value, DEFAULT_DURATION);
	    doTime = PR_TRUE;
            break;
	case 'h':
	    slotname = PORT_Strdup(optstate->value);
	    useSessionKey = PR_TRUE;
	    break;
	case 'k':
	    keybits = INT_ARG(optstate->value, DEFAULT_KEY_BITS);
	    break;
	case 'w':
	    pwData.data = PORT_Strdup(optstate->value);;
	    pwData.source = PW_PLAINTEXT;
	    break;
        case 'f':
            pwData.data = PORT_Strdup(optstate->value);
            pwData.source = PW_FROMFILE;
            break;
	case 'x':
	    /*  -x public exponent (for RSA keygen)  */
	    publicExponent = INT_ARG(optstate->value, DEFAULT_EXPONENT);
	    break;
	case 't':
	    threadNum = INT_ARG(optstate->value, DEFAULT_THREADS);
	    break;
	}
    }
    if (optstatus == PL_OPT_BAD)
	Usage(progName);

    if ((doPriv && doPub) || (doIters && doTime) ||
        ((useTokenKey + useSessionKey + useBLKey) != PR_TRUE) ||
        (useTokenKey && keybits) || (useTokenKey && doKeyGen) ||
        (keybits && (keybits<MIN_KEY_BITS || keybits>MAX_KEY_BITS))) {
        Usage(progName);
    }

    if (doIters && doTime) Usage(progName);

    if (!doTime) {
        doIters = PR_TRUE;
    }

    PR_Init( PR_SYSTEM_THREAD, PR_PRIORITY_NORMAL, 1);

    PK11_SetPasswordFunc(SECU_GetModulePassword);
    secDir = SECU_ConfigDirectory(secDir);

    if (useTokenKey || useSessionKey) {
	rv = NSS_Init(secDir);
	if (rv != SECSuccess) {
	    fprintf(stderr, "NSS_Init failed.\n");
	    exit(1);
	}
    } else {
	rv = NSS_NoDB_Init(NULL);
	if (rv != SECSuccess) {
	    fprintf(stderr, "NSS_NoDB_Init failed.\n");
	    exit(1);
	}
    }

    if (useTokenKey) {
        CK_OBJECT_HANDLE kh = CK_INVALID_HANDLE;

        cert = PK11_FindCertFromNickname(nickname, &pwData);
        if (cert == NULL) {
            fprintf(stderr,
                    "Can't find certificate by name \"%s\"\n", nickname);
            exit(1);
        }
        pubHighKey = CERT_ExtractPublicKey(cert);
        if (pubHighKey == NULL) {
            fprintf(stderr, "Can't extract public key from certificate");
            exit(1);
        }

        if (doPub) {
            /* do public key ops */
            fn = (RSAOp)PK11_PublicKeyOp;
            rsaKey = (void *) pubHighKey;
                
            kh = PK11_ImportPublicKey(cert->slot, pubHighKey, PR_FALSE);
            if (CK_INVALID_HANDLE == kh) {
                fprintf(stderr,
                        "Unable to import public key to certificate slot.");
                exit(1);
            }
            pubHighKey->pkcs11Slot = PK11_ReferenceSlot(cert->slot);
            pubHighKey->pkcs11ID = kh;
            printf("Using PKCS#11 for RSA encryption with token %s.\n",
                   PK11_GetTokenName(cert->slot));
        } else {
            /* do private key ops */
            privHighKey = PK11_FindKeyByAnyCert(cert, &pwData);
            if (privHighKey == NULL) {
                fprintf(stderr,
                        "Can't find private key by name \"%s\"\n", nickname);
                exit(1);
            }
    
            SECKEY_CacheStaticFlags(privHighKey);
            fn = (RSAOp)PK11_PrivateKeyOp;
            keys.privKey = privHighKey;
            keys.pubKey = pubHighKey;
            rsaKey = (void *) &keys;
            printf("Using PKCS#11 for RSA decryption with token %s.\n",
                   PK11_GetTokenName(privHighKey->pkcs11Slot));
        }        
    } else

    if (useSessionKey) {
        /* use PKCS#11 session key objects */
        PK11RSAGenParams   rsaparams;
        void             * params;

        slot = PK11_FindSlotByName(slotname); /* locate target slot */
        if (!slot) {
            fprintf(stderr, "Can't find slot \"%s\"\n", slotname);
            exit(1);
        }

        /* do a temporary keygen in selected slot */
        if (!keybits) {
            keybits = DEFAULT_KEY_BITS;
        }

        printf("Using PKCS#11 with %ld bits session key in token %s.\n",
               keybits, PK11_GetTokenName(slot));

        rsaparams.keySizeInBits = keybits;
        rsaparams.pe = publicExponent;
        params = &rsaparams;

        fprintf(stderr,"\nGenerating RSA key. This may take a few moments.\n");

        privHighKey = PK11_GenerateKeyPair(slot, CKM_RSA_PKCS_KEY_PAIR_GEN,
                                           params, &pubHighKey, PR_FALSE,
                                           PR_FALSE, (void*)&pwData);
        if (!privHighKey) {
            fprintf(stderr,
                    "Key generation failed in token \"%s\"\n",
                    PK11_GetTokenName(slot));
            exit(1);
        }

        SECKEY_CacheStaticFlags(privHighKey);
        
        fprintf(stderr,"Keygen completed.\n");

        if (doPub) {
            /* do public key operations */
            fn = (RSAOp)PK11_PublicKeyOp;
            rsaKey = (void *) pubHighKey;
        } else {
            /* do private key operations */
            fn = (RSAOp)PK11_PrivateKeyOp;
            keys.privKey = privHighKey;
            keys.pubKey = pubHighKey;
            rsaKey = (void *) &keys;
        }        
    } else
        
    {
        /* use freebl directly */
        if (!keybits) {
            keybits = DEFAULT_KEY_BITS;
        }
        if (!doKeyGen) {
            if (keybits != DEFAULT_KEY_BITS) {
                doKeyGen = PR_TRUE;
            }
        }
        printf("Using freebl with %ld bits key.\n", keybits);
        if (doKeyGen) {
            fprintf(stderr,"\nGenerating RSA key. "
                    "This may take a few moments.\n");
            for (i=0; i < 4; i++) {
                if (peCount || (publicExponent & ((unsigned long)0xff000000L >>
                                                  (i*8)))) {
                    pubEx[peCount] =  (CK_BYTE)((publicExponent >>
                                                 (3-i)*8) & 0xff);
                    peCount++;
                }
            }
            pe.len = peCount;
            pe.data = &pubEx[0];
            pe.type = siBuffer;

            rsaKey = RSA_NewKey(keybits, &pe);
            fprintf(stderr,"Keygen completed.\n");
        } else {
            /* use a hardcoded key */
            printf("Using hardcoded %ld bits key.\n", keybits);
            if (doPub) {
                pubKey = getDefaultRSAPublicKey();
            } else {
                privKey = getDefaultRSAPrivateKey();
            }
        }

        if (doPub) {
            /* do public key operations */
            fn = (RSAOp)RSA_PublicKeyOp;
            if (rsaKey) {
                /* convert the RSAPrivateKey to RSAPublicKey */
                pubKeyStr.arena = NULL;
                pubKeyStr.modulus = ((RSAPrivateKey*)rsaKey)->modulus;
                pubKeyStr.publicExponent =
                    ((RSAPrivateKey*)rsaKey)->publicExponent;
                rsaKey = &pubKeyStr;
            } else {
                /* convert NSSLOWKeyPublicKey to RSAPublicKey */
                rsaKey = (void *)(&pubKey->u.rsa);
            }
            PORT_Assert(rsaKey);
        } else {
            /* do private key operations */
            fn = (RSAOp)RSA_PrivateKeyOp;
            if (privKey) {
                /* convert NSSLOWKeyPrivateKey to RSAPrivateKey */
                rsaKey = (void *)(&privKey->u.rsa);
            }
            PORT_Assert(rsaKey);
        }
    }

    memset(buf, 1, sizeof buf);
    rv = fn(rsaKey, buf2, buf);
    if (rv != SECSuccess) {
	PRErrorCode errNum;
	const char * errStr = NULL;

	errNum = PORT_GetError();
	if (errNum)
	    errStr = SECU_Strerror(errNum);
	else
	    errNum = rv;
	if (!errStr)
	    errStr = "(null)";
	fprintf(stderr, "Error in RSA operation: %d : %s\n", errNum, errStr);
	exit(1);
    }

    threadsArr = (PRThread**)PORT_Alloc(threadNum*sizeof(PRThread*));
    runDataArr = (ThreadRunData**)PORT_Alloc(threadNum*sizeof(ThreadRunData*));
    timeCtx = CreateTimingContext();
    TimingBegin(timeCtx, PR_Now());
    for (i = 0;i < threadNum;i++) {
        runDataArr[i] = (ThreadRunData*)PORT_Alloc(sizeof(ThreadRunData));
        runDataArr[i]->fn = fn;
        runDataArr[i]->buf = buf;
        runDataArr[i]->doIters = &doIters;
        runDataArr[i]->rsaKey = rsaKey;
        runDataArr[i]->seconds = seconds;
        runDataArr[i]->iters = iters;
        threadsArr[i] = 
            PR_CreateThread(PR_USER_THREAD,
                 ThreadExecFunction,
                 (void*) runDataArr[i],
                 PR_PRIORITY_NORMAL,
                 PR_GLOBAL_THREAD,
                 PR_JOINABLE_THREAD,
                 0);
    }
    iters = 0;
    calcThreads = 0;
    for (i = 0;i < threadNum;i++, calcThreads++)
    {
        PR_JoinThread(threadsArr[i]);
        if (runDataArr[i]->status != SECSuccess) {
            const char * errStr = SECU_Strerror(runDataArr[i]->errNum);
            fprintf(stderr, "Thread %d: Error in RSA operation: %d : %s\n",
                    i, runDataArr[i]->errNum, errStr);
            calcThreads -= 1;
        } else {
            iters += runDataArr[i]->iterRes;
        }
        PORT_Free((void*)runDataArr[i]);
    }
    PORT_Free(runDataArr);
    PORT_Free(threadsArr);

    TimingEnd(timeCtx, PR_Now());
    
    printf("%ld iterations in %s\n",
	   iters, TimingGenerateString(timeCtx));
    printf("%.2f operations/s .\n", ((double)(iters)*(double)1000000.0) /
           (double)timeCtx->interval );
    TimingDivide(timeCtx, iters);
    printf("one operation every %s\n", TimingGenerateString(timeCtx));

    if (pubHighKey) {
        SECKEY_DestroyPublicKey(pubHighKey);
    }

    if (privHighKey) {
         SECKEY_DestroyPrivateKey(privHighKey);
    }

    if (cert) {
        CERT_DestroyCertificate(cert);
    }

    if (NSS_Shutdown() != SECSuccess) {
        exit(1);
    }

    return 0;
}
