#include <stdio.h>
#include <md5.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#ifndef EXCLUDE_SPENT_DBM
#include <gdbm.h>
char spentfile[256] = "spent.db";  /* CHANGE TO FULL PATH !!! */

#endif

#include <bn.h>
#include <bio.h>
#include <evp.h>
#include <pem.h>

/*------------------------------------------------------------------------*/
/* coin certificate i/o */
FILE *certin, *certout, *certalt;

int BN_to_mpi_write(BIGNUM * bnval, char *certtype, char *header)
{
  int t, len;
  unsigned char dbuf[2048];
  BIO *bi;

  t = BN_num_bits(bnval);
  dbuf[0] = t >> 8;
  dbuf[1] = t & 0xff;
  len = BN_bn2bin(bnval, &dbuf[2]);

  bi = BIO_new(BIO_s_file());
  BIO_set_fp(bi, certout, 0);
  PEM_write_bio(bi, certtype, header, dbuf, len + 2);
  BIO_free(bi);

  fflush(certout);
  return t;
}

/* returned so we can tell which type of cert */
char certname[256], certheader[256];

void mpi_to_BN_read(BIGNUM ** N)
{
  long length, i;
  unsigned char *p;
  char *cn, *ch;
  BIO *bi;

  *N = NULL;
  bi = BIO_new(BIO_s_file());
  BIO_set_fp(bi, certin, 0);
  if (!PEM_read_bio(bi, &cn, &ch, &p, &length))
    return;

  strncpy(certname, cn ? cn : "(NULL)", 255);
  strncpy(certheader, ch ? ch : "(NULL)", 255);
  free(cn), free(ch);
  BIO_free(bi);

  i = p[0] * 256 + p[1];
  *N = BN_bin2bn(&p[2], (i + 7) / 8, NULL);  /* allocates space and convert */
  free(p);
}

/*------------------------------------------------------------------------*/
/* coin manipulation */

/* cert strings */
char unblinder[] = "UNBLINDER";
char blindcoin[] = "BLINDED COIN";
char signblind[] = "SIGNED BLINDED COIN";
char signdcoin[] = "SIGNED COIN";
char signtitle[] = "SIGNED TITLE";
char coinvalid[20] = "XcHeQueRgOoDCaSh";
char sernobuf[80];

BN_CTX *ctx;
RSA *key, *key2, *key3;
BIGNUM *T;                      /* preread to here when finding cert type */
char hashbuf[16];

#if 0
#define RSA_mod_exp(X, Y, key) BN_mod_exp(X, Y, key->d, key->n, ctx)
#define KBN_mod_exp(X, Y, key) BN_mod_exp(X, Y, key->e, key->n, ctx)
#else
#define RSA_mod_exp(X, Y, key) ((*key->meth->rsa_mod_exp) (X, Y, key) )
#define KBN_mod_exp(X, Y, key) \
((*key->meth->bn_mod_exp) (X, Y, key->e, key->n, ctx))
#endif

/*------------------------------------*/
/* given E,N,p,q calculate D and the rest for title certs */
void rsaEtoD(RSA * rsa)
{
  BIGNUM *pm1 = BN_new(), *qm1 = BN_new(), *pm1qm1 = BN_new(), *gcd = BN_new();

  BN_sub(pm1, rsa->p, BN_value_one());
  BN_sub(qm1, rsa->q, BN_value_one());

  BN_mul(pm1qm1, pm1, qm1);
  /* fiddle with E until it is relatively prime to (p-1)(q-1) */
  for (;;) {
    BN_gcd(gcd, pm1qm1, rsa->e, ctx);
    if (BN_is_one(gcd))
      break;
    BN_add_word(rsa->e, 2L);
  }
  if (!(rsa->d = BN_mod_inverse(rsa->e, pm1qm1, ctx))) {
    fprintf(stderr, "Error calculating D\n");
    exit(-1);
  }
  BN_free(pm1qm1), BN_free(gcd);

  BN_mod(pm1, rsa->d, pm1, ctx);
  BN_mod(qm1, rsa->d, qm1, ctx);
  rsa->dmp1 = pm1;
  rsa->dmq1 = qm1;
}

/*------------------------------------*/
/* extract LSBs of bignum for keyid */
unsigned long long BN_to_id(BIGNUM * N)
{
  int i = 64;
  unsigned long long ret = 0;

  while (i)
    ret = (ret << 1) + BN_is_bit_set(N, --i);
  return ret;
}

/*------------------------------------*/
/* insert/extract identifier string */
#define BRANDBIT 384

void brand(BIGNUM * N, unsigned char *s, int startbit)
{
  int j;

  while (startbit) {
    for (j = 0; j < 8 && startbit; j++) {
      if ((*s >> j) & 1)
        BN_set_bit(N, startbit--);
      else
        BN_clear_bit(N, startbit--);
    }
    if (!*s++)
      break;
  }
}

/*----------*/
void getbrand(BIGNUM * N, unsigned char *s, int startbit)
{
  int j;
  unsigned char *s0 = s;

  while (startbit) {
    *s = 0;
    for (j = 0; j < 8 && startbit; j++)
      if (BN_is_bit_set(N, startbit--))
        *s |= 1 << j;
    if (!*s++)
      break;
  }
  if (!startbit)
    *s0 = 0;
}

/*------------------------------------*/
#define TTAG 0xAA

/*------------------------------------*/
/* generate new E for a title cert */
void newtitle(char *ident)
{
  int j, cks;
  RSA *title = RSA_new();

  for (j = 0, cks = 0; j < strlen(ident); j++)
    cks += ident[j];
  ident[j] = 256 - cks;         /* checksum to zero byte */
  ident[j + 1] = TTAG;          /* tag */
  ident[j + 2] = 0;

  title->n = BN_dup(key->n);
  title->p = BN_dup(key->p);
  title->q = BN_dup(key->q);
  title->e = BN_new();

  BN_rand(title->e, BN_num_bits(key->n) - 8, 1, 1);  /* create title E */
  brand(title->e, ident, BRANDBIT);
  rsaEtoD(title);               /* adjust E to find D and fill in */
  sprintf(sernobuf, "%016qX %016qX %s\n",
          BN_to_id(title->n), BN_to_id(title->e), ident);
  T = BN_new();
  RSA_mod_exp(T, title->e, key);  /* sign new E */
  BN_to_mpi_write(T, signtitle, sernobuf);
  RSA_free(title);
  BN_free(T);
}

/*------------------------------------*/
/* swap title rsa for key rsa */
void decodetitle(BIGNUM * S)
{
  unsigned char ident[(BRANDBIT + 7) / 8 + 4];
  int j, cks;

  KBN_mod_exp(S, S, key);       /* design */
  getbrand(S, ident, BRANDBIT);
  for (j = 0, cks = 0; j < strlen(ident); j++)
    cks += ident[j];
  if (!j || (ident[j - 1] != TTAG && (cks & 0xff) != TTAG)) {
    fprintf(stderr, "Error in title\n");
    exit(-1);
  }
  ident[j - 2] = 0;
  fprintf(stderr, "Title: [%s]\n", ident);
  BN_free(key->e);
  key->e = S;
  if (key->d) {                 /* secret key, rederive D factors */
    BN_free(key->d), BN_free(key->dmp1), BN_free(key->dmq1);
    rsaEtoD(key);
  }
}

/*------------------------------------*/
void mintcoin()
{
  T = BN_new();
  BN_rand(T, BN_num_bits(key->n) - 8, 1, 1);  /* create coin */
  brand(T, coinvalid, BRANDBIT);
  RSA_mod_exp(T, T, key);       /* sign coin */
  sprintf(sernobuf, "%016qX %qX %016qX\n", BN_to_id(key->n),
          BN_to_id(key->e), BN_to_id(T));
  BN_to_mpi_write(T, signdcoin, sernobuf);
  BN_free(T);
}

/*------------------------------------*/
void signcoin()
{
  /* needed for title cert usage */
  RSA_mod_exp(T, T, key);
  BN_to_mpi_write(T, signblind, certheader);
  BN_free(T);
}

/*------------------------------------*/
void protocoin()
{
  BIGNUM *PC = BN_new(), *C = BN_new(), *B = BN_new(), *IB = NULL, *IBB = NULL;
  char unbltype[80];
  FILE *fsave;

  T = BN_new();
  BN_rand(PC, BN_num_bits(key->n) - 8, 1, 1);  /* create coin */
  brand(PC, coinvalid, BRANDBIT);
  /* get good unblinding factor */
  while (!IB) {
    BN_rand(B, BN_num_bits(key->n) - 16, 0, 1);  /* create blinding factor */
    IB = BN_mod_inverse(B, key->n, ctx);  /* calculate unblinding factor */
    strcpy(unbltype, unblinder);
    IBB = BN_new(), BN_copy(IBB, IB);
    if (key3) {                 /* sign too */
      if (BN_ucmp(key3->n, IBB) <= 0) {
        BN_free(IBB), BN_free(IB);
        IB = NULL;
        continue;
      }
      RSA_mod_exp(IBB, IBB, key3);  /* sign with sender private key */
    }
    if (key2) {                 /* encrypt unblinding factor */
      if (BN_ucmp(key2->n, IBB) <= 0) {
        BN_free(IBB), BN_free(IB);
        IB = NULL;
        continue;
      }
      KBN_mod_exp(IBB, IBB, key2);  /* encrypt with recip public key */
    }
  }
  KBN_mod_exp(T, B, key);       /* mint-encrypt blinding factor */
  BN_mod_mul(C, T, PC, key->n, ctx);  /* multiply by protocoin */
  sprintf(sernobuf, "%016qX %qX %016qX\n", BN_to_id(key->n),
          BN_to_id(key->e), BN_to_id(C));
  BN_to_mpi_write(C, blindcoin, sernobuf);
  if (key2 || key3)
    BN_to_mpi_write(IBB, unbltype, sernobuf);

  fsave = certout;
  certout = certalt;
  BN_to_mpi_write(IB, unblinder, sernobuf);
  certout = fsave;

  BN_free(T), BN_free(IB), IB = NULL;
  BN_free(IBB);
  BN_free(PC), BN_free(B), BN_free(C);
}

/*------------------------------------*/
int unblindcoin(int keepcrypt)
{
  BIGNUM *SC = NULL, *IB = NULL, *XIB = NULL, *XSC = NULL;
  char chsav[256];
  int ended;

  strcpy(chsav, certheader);    /* match headers (serial numbers) */
  ended = 0;
  while (!SC || !IB) {
    if (!strcmp(certheader, chsav)
        && !strncmp(certname, unblinder, strlen(unblinder)))
      IB = BN_dup(T);
    else if (!strcmp(certheader, chsav)
             && !strncmp(certname, signblind, strlen(signblind)))
      SC = BN_dup(T);
    BN_free(T);
    T = BN_new();
    if (!SC || !IB)
      mpi_to_BN_read(&T);
  }

  if (keepcrypt) {              /* save copies to print out later */
    XIB = BN_new(), XSC = BN_new();
    BN_copy(XIB, IB), BN_copy(XSC, SC);
  }
  /* Decrypt as needed - should also check for E and S in the certname */
  if (key2)
    RSA_mod_exp(IB, IB, key2);  /* decrypt unblinding factor */
  if (key3)
    KBN_mod_exp(IB, IB, key3);  /* design unblinding factor */

  BN_mod_mul(T, SC, IB, key->n, ctx);  /* unblind */
  BN_free(IB), IB = NULL;

  KBN_mod_exp(SC, T, key);      /* unsign from mint to expose check string */
  getbrand(SC, chsav, BRANDBIT);
  if (!strncmp(coinvalid, chsav, strlen(coinvalid))) {
    if (keepcrypt) {
      BN_to_mpi_write(XIB, unblinder, certheader);
      BN_to_mpi_write(XSC, signblind, certheader);
      BN_free(XIB), BN_free(XSC);
    } else
      BN_to_mpi_write(T, signdcoin, certheader);
  } else {
    fprintf(stderr, "Invalid Coin or Unblinder is (not) Encrypted/Signed\n");
    BN_free(SC), BN_free(T);
    return (-1);
  }
  BN_free(SC), BN_free(T);
  return 0;
}

/*------------------------------------*/
int verifycoin(int verfunc)
{
  BIGNUM *SC = BN_new();
  int ret = 0;
  char coinbrand[50];

#ifndef EXCLUDE_SPENT_DBM
  GDBM_FILE dbf = NULL;
  datum dbkey, cont;
  int coinlen;
  unsigned char *p;

  if (verfunc && NULL == (dbf =
                          gdbm_open(spentfile, 512, verfunc == 1 ?
                                    GDBM_READER : GDBM_WRCREAT, 0600, NULL)))
    exit(-1);
#endif

  SC = BN_new();
  KBN_mod_exp(SC, T, key);      /* validate signature */
  getbrand(SC, coinbrand, BRANDBIT);
  if (strncmp(coinvalid, coinbrand, strlen(coinvalid))) {
    fprintf(stderr, "Invalid Coin\n");
    BN_free(SC), BN_free(T);
    return (-1);
  }
  if (verfunc == 0)
    fprintf(stderr, "Good Coin Signature\n");

#ifndef EXCLUDE_SPENT_DBM
  else {                        /* check or insert in database */
    cont.dptr = "";
    cont.dsize = 0;
    /* convert SC to database key entry */
    p = (unsigned char *) malloc(((BN_num_bits(SC) + 7) / 8) + 4);
    coinlen = BN_bn2bin(SC, p);
    dbkey.dptr = p;
    dbkey.dsize = coinlen;

    if (verfunc == 1)
      ret = gdbm_exists(dbf, dbkey);
    else if (verfunc == 2)
      ret = gdbm_store(dbf, dbkey, cont, GDBM_INSERT);

    if (ret != 0)
      fprintf(stderr, "Coin is Spent\n");
  }
  if (verfunc)
    gdbm_close(dbf);
#endif
  BN_free(SC), BN_free(T);
  return ret;
}

/*------------------------------------------------------------------------*/
#include <libpgp2.h>

/*------------------------------------------------------------------------*/
int main(int argc, char *argv[])
{
  int ch;
  extern char *optarg;
  FILE *keyring = NULL, *keyring2 = NULL, *keyring3 = NULL;
  unsigned long long keyid = 0LL, keyid2 = 0LL, keyid3 = 0LL;
  unsigned char passwdhash[16];
  enum {
    PROTO, SIGN, UNBL, VERF, MINT
  } action = PROTO;
  char ident[(BRANDBIT + 7) / 8];
  int titleflag = 0;
  int vertype = 0;
  int leavecrypt = 0;
  BIGNUM *S;

  memset(passwdhash, 0, 16);
  memset(ident, 0, 20);
  certin = stdin, certout = stdout, certalt = stderr;

  while ((ch = getopt(argc, argv, "li:K:k:mp:r:S:s:Tt:vXxI:O:E:B:")) != -1) {
    if (ch == 'm')
      action = MINT;
    else if (ch == 'l')
      leavecrypt++;
    else if (ch == 'T')
      titleflag++;
    else if (ch == 't') {
      if (strlen(optarg) > (BRANDBIT + 7) / 8 - 10) {
        fprintf(stderr, "Title String too long\n");
        exit(-1);
      }
      strcpy(ident, optarg);

    } else if (ch == 'I') {
      certin = fopen(optarg, "rb");
      if (certin == NULL)
        exit(-1);
    } else if (ch == 'O') {
      certout = fopen(optarg, "wb");
      if (certout == NULL)
        exit(-1);
    } else if (ch == 'E') {
      certalt = fopen(optarg, "wb");
      if (certalt == NULL)
        exit(-1);
    }
#ifndef EXCLUDE_SPENT_DBM
    else if (ch == 'B')
      strncpy(spentfile, optarg, 255);
    else if (ch == 'v')
      vertype = 1;
    else if (ch == 'x')
      vertype = 2;
    else if (ch == 'X')
      vertype = 3;
#endif
    else if (ch == 'i')         /* mint userid */
      sscanf(optarg, "%qX", &keyid);
    else if (ch == 'r')         /* recipient userid */
      sscanf(optarg, "%qX", &keyid2);
    else if (ch == 's')         /* signers keyring */
      sscanf(optarg, "%qX", &keyid3);
    else if (ch == 'k') {       /* mint keyring */
      if (!(keyring = fopen(optarg, "rb")))
        fprintf(stderr, "Warning -k keyring not found, defaulting\n");
    } else if (ch == 'K') {     /* recipient keyring */
      if (!(keyring2 = fopen(optarg, "rb")))
        fprintf(stderr, "Warning -K keyring not found, defaulting\n");
    } else if (ch == 'S') {     /* signers keyring */
      if (!(keyring3 = fopen(optarg, "rb")))
        fprintf(stderr, "Warning -S keyring not found, defaulting\n");
    } else if (ch == 'p')       /* password */
      MD5(optarg, strlen(optarg), passwdhash);
    else {
      fprintf(stderr, "Bad Option - see xchequer.doc\n");
      exit(-1);
    }
  }

  if (titleflag) {
    S = BN_new();
    mpi_to_BN_read(&S);
  }
  if (!keyid) {
    mpi_to_BN_read(&T);
    if (!strncmp(certname, signtitle, strlen(signtitle))) {
      S = T;
      T = BN_new();
      titleflag = 1;
      mpi_to_BN_read(&T);
    }
    sscanf(certheader, "%qX", &keyid);
    if (!strncmp(certname, blindcoin, strlen(blindcoin)))
      action = SIGN;
    else if (!strncmp(certname, unblinder, strlen(unblinder))
             || !strncmp(certname, signblind, strlen(signblind)))
      action = UNBL;
    else if (!strncmp(certname, signdcoin, strlen(signdcoin)))
      action = VERF;
  }
  if (vertype && action != VERF)
    return -1;

  ctx = BN_CTX_new();
  setkeyring_fp(keyring);
  if (getkey2(&key, NULL, (action == SIGN || action == MINT
                           || strlen(ident) || vertype == 3) ?
              passwdhash : NULL, &keyid))
    exit(-1);
  if (titleflag)                /* modify main key with title? */
    decodetitle(S);
  setkeyring_fp(keyring2);
  if (keyid2)                   /* recipient key */
    getkey2(&key2, NULL, action == UNBL ? passwdhash : NULL, &keyid2);
  setkeyring_fp(keyring3);
  if (keyid3)                   /* signers key */
    getkey2(&key3, NULL, action == PROTO ? passwdhash : NULL, &keyid3);

  if (strlen(ident))
    newtitle(ident);
  else if (action == MINT)
    mintcoin();
  else if (action == PROTO)
    protocoin();
  else if (action == SIGN)
    signcoin();
  else if (action == UNBL)
    return (unblindcoin(leavecrypt));
  else if (action == VERF) {
    if (vertype != 3)
      return (verifycoin(vertype));
    strncpy(sernobuf, certheader, 17);
    certheader[0] = 0;          /* insure we get a new one */
    S = BN_new();
    mpi_to_BN_read(&S);
    if (strncmp(sernobuf, certheader, 17)) {
      fprintf(stderr, "Signed and Blinded coin are of different series\n");
      exit(-1);
    }
    if (verifycoin(2))
      return (-1);
    T = S;
    signcoin();
  }
  return 0;
}
