/* pkbench.c - Pubkey menchmarking
 * Copyright (C) 2004, 2005, 2008 Free Software Foundation, Inc.
 *
 * This file is part of Libgcrypt.
 *
 * Libgcrypt is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * Libgcrypt is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <gcrypt.h>
#include <assert.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/stat.h>
#ifndef HAVE_W32_SYSTEM
# include <sys/times.h>
#endif /*HAVE_W32_SYSTEM*/
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>

#define PGM "pkbench"


static int verbose;
static int debug;
static int error_count;


typedef struct context
{
  gcry_sexp_t key_secret;
  gcry_sexp_t key_public;
  gcry_sexp_t data;
  gcry_sexp_t data_encrypted;
  gcry_sexp_t data_signed;
} *context_t;

typedef int (*work_t) (context_t context, unsigned int final);


static void
fail (const char *format, ...)
{
  va_list arg_ptr;

  fputs ( PGM ": ", stderr);
  va_start (arg_ptr, format);
  vfprintf (stderr, format, arg_ptr);
  va_end (arg_ptr);
  error_count++;
}

static void
die (const char *format, ...)
{
  va_list arg_ptr;

  putchar ('\n');
  fputs ( PGM ": ", stderr);
  va_start (arg_ptr, format);
  vfprintf (stderr, format, arg_ptr);
  va_end (arg_ptr);
  exit (1);
}

static void
show_sexp (const char *prefix, gcry_sexp_t a)
{
  char *buf;
  size_t size;

  fputs (prefix, stderr);
  size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0);
  buf = gcry_xmalloc (size);

  gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size);
  fprintf (stderr, "%.*s", (int)size, buf);
  gcry_free (buf);
}


static void *
read_file (const char *fname, size_t *r_length)
{
  FILE *fp;
  struct stat st;
  char *buf;
  size_t buflen;

  fp = fopen (fname, "rb");
  if (!fp)
    {
      fail ("can't open `%s': %s\n", fname, strerror (errno));
      return NULL;
    }

  if (fstat (fileno(fp), &st))
    {
      fail ("can't stat `%s': %s\n", fname, strerror (errno));
      fclose (fp);
      return NULL;
    }

  buflen = st.st_size;
  buf = gcry_xmalloc (buflen+1);
  if (fread (buf, buflen, 1, fp) != 1)
    {
      fail ("error reading `%s': %s\n", fname, strerror (errno));
      fclose (fp);
      gcry_free (buf);
      return NULL;
    }
  fclose (fp);

  if (r_length)
    *r_length = buflen;
  return buf;
}



static void
benchmark (work_t worker, context_t context)
{
  clock_t timer_start, timer_stop;
  unsigned int loop = 10;
  unsigned int i = 0;
  struct tms timer;
  int ret = 0;

#ifdef HAVE_W32_SYSTEM
  timer_start = clock ();
#else
  times (&timer);
  timer_start = timer.tms_utime;
#endif
  for (i = 0; i < loop; i++)
    {
      ret = (*worker) (context, (i + 1) == loop);
      if (! ret)
	break;
    }
#ifdef HAVE_W32_SYSTEM
  timer_stop = clock ();
#else
  times (&timer);
  timer_stop = timer.tms_utime;
#endif

  if (ret)
    printf ("%.0f ms\n",
	    (((double) ((timer_stop - timer_start) / loop)) / CLOCKS_PER_SEC)
	    * 10000000);
  else
    printf ("[skipped]\n");
}

static int
work_encrypt (context_t context, unsigned int final)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  gcry_sexp_t data_encrypted = NULL;
  int ret = 1;

  err = gcry_pk_encrypt (&data_encrypted,
			 context->data, context->key_public);
  if (gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED)
    {
      err = GPG_ERR_NO_ERROR;
      ret = 0;
    }
  else
    {
      assert (! err);

      if (final)
	context->data_encrypted = data_encrypted;
      else
	gcry_sexp_release (data_encrypted);
    }

  return ret;
}

static int
work_decrypt (context_t context, unsigned int final)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  int ret = 1;

  if (! context->data_encrypted)
    ret = 0;
  else
    {
      gcry_sexp_t data_decrypted = NULL;

      err = gcry_pk_decrypt (&data_decrypted,
			     context->data_encrypted,
			     context->key_secret);
      assert (! err);
      if (final)
	{
	  gcry_sexp_release (context->data_encrypted);
	  context->data_encrypted = NULL;
	}
      gcry_sexp_release (data_decrypted);
    }

  return ret;
}

static int
work_sign (context_t context, unsigned int final)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  gcry_sexp_t data_signed = NULL;
  int ret = 1;

  err = gcry_pk_sign (&data_signed,
		      context->data, context->key_secret);
  if (gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED)
    {
      err = GPG_ERR_NO_ERROR;
      ret = 0;
    }
  else if (err)
    {
      fail ("pk_sign failed: %s\n", gpg_strerror (err));
      ret = 0;
    }
  else
    {
      if (final)
	context->data_signed = data_signed;
      else
	gcry_sexp_release (data_signed);
    }

  return ret;
}

static int
work_verify (context_t context, unsigned int final)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  int ret = 1;

  if (!context->data_signed)
    return 0;

  err = gcry_pk_verify (context->data_signed,
                        context->data,
                        context->key_public);
  if (err)
    {
      show_sexp ("data_signed:\n", context->data_signed);
      show_sexp ("data:\n", context->data);
      fail ("pk_verify failed: %s\n", gpg_strerror (err));
      ret = 0;
    }
  else if (final)
    {
      gcry_sexp_release (context->data_signed);
      context->data_signed = NULL;
    }

  return ret;
}

static void
process_key_pair (context_t context)
{
  struct
  {
    work_t worker;
    const char *identifier;
  } worker_functions[] = { { work_encrypt, "encrypt" },
			   { work_decrypt, "decrypt" },
			   { work_sign,    "sign"    },
			   { work_verify,  "verify"  } };
  unsigned int i = 0;

  for (i = 0; i < (sizeof (worker_functions) / sizeof (*worker_functions)); i++)
    {
      printf ("%s: ", worker_functions[i].identifier);
      benchmark (worker_functions[i].worker, context);
    }
}

static void
context_init (context_t context, gcry_sexp_t key_secret, gcry_sexp_t key_public)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  unsigned int key_size = 0;
  gcry_mpi_t data = NULL;
  gcry_sexp_t data_sexp = NULL;

  key_size = gcry_pk_get_nbits (key_secret);
  assert (key_size);

  data = gcry_mpi_new (key_size);
  assert (data);

  gcry_mpi_randomize (data, key_size, GCRY_STRONG_RANDOM);
  gcry_mpi_clear_bit (data, key_size - 1);
  err = gcry_sexp_build (&data_sexp, NULL,
			 "(data (flags raw) (value %m))",
			 data);
  assert (! err);
  gcry_mpi_release (data);

  context->key_secret = key_secret;
  context->key_public = key_public;
  context->data = data_sexp;
  context->data_encrypted = NULL;
  context->data_signed = NULL;
}

static void
context_destroy (context_t context)
{
  gcry_sexp_release (context->key_secret);
  gcry_sexp_release (context->key_public);
  gcry_sexp_release (context->data);
}

static void
process_key_pair_file (const char *key_pair_file)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  void *key_pair_buffer = NULL;
  gcry_sexp_t key_pair_sexp = NULL;
  gcry_sexp_t key_secret_sexp = NULL;
  gcry_sexp_t key_public_sexp = NULL;
  struct context context = { NULL };
  size_t file_length;

  key_pair_buffer = read_file (key_pair_file, &file_length);
  if (!key_pair_buffer)
    die ("failed to open `%s'\n", key_pair_file);

  err = gcry_sexp_sscan (&key_pair_sexp, NULL,
			 key_pair_buffer, file_length);
  if (err)
    die ("gcry_sexp_sscan failed\n");

  key_secret_sexp = gcry_sexp_find_token (key_pair_sexp, "private-key", 0);
  assert (key_secret_sexp);
  key_public_sexp = gcry_sexp_find_token (key_pair_sexp, "public-key", 0);
  assert (key_public_sexp);

  gcry_sexp_release (key_pair_sexp);

  context_init (&context, key_secret_sexp, key_public_sexp);

  printf ("Key file: %s\n", key_pair_file);
  process_key_pair (&context);
  printf ("\n");

  context_destroy (&context);
  gcry_free (key_pair_buffer);
}


static void
generate_key (const char *algorithm, const char *key_size)
{
  gcry_error_t err = GPG_ERR_NO_ERROR;
  size_t key_pair_buffer_size = 0;
  char *key_pair_buffer = NULL;
  gcry_sexp_t key_spec = NULL;
  gcry_sexp_t key_pair = NULL;

  if (isdigit ((unsigned int)*key_size))
    err = gcry_sexp_build (&key_spec, NULL,
                           "(genkey (%s (nbits %s)))",
                           algorithm, key_size);
  else
    err = gcry_sexp_build (&key_spec, NULL,
                           "(genkey (%s (curve %s)))",
                           algorithm, key_size);
  if (err)
    die ("sexp_build failed: %s\n", gpg_strerror (err));

  err = gcry_pk_genkey (&key_pair, key_spec);
  if (err)
    {
      show_sexp ("request:\n", key_spec);
      die ("pk_genkey failed: %s\n", gpg_strerror (err));
    }

  key_pair_buffer_size = gcry_sexp_sprint (key_pair, GCRYSEXP_FMT_ADVANCED,
					   NULL, 0);
  key_pair_buffer = gcry_xmalloc (key_pair_buffer_size);

  gcry_sexp_sprint (key_pair, GCRYSEXP_FMT_ADVANCED,
		    key_pair_buffer, key_pair_buffer_size);

  printf ("%.*s", (int)key_pair_buffer_size, key_pair_buffer);
  gcry_free (key_pair_buffer);
}



int
main (int argc, char **argv)
{
  int last_argc = -1;
  int genkey_mode = 0;
  int fips_mode = 0;

  if (argc)
    { argc--; argv++; }

  while (argc && last_argc != argc )
    {
      last_argc = argc;
      if (!strcmp (*argv, "--"))
        {
          argc--; argv++;
          break;
        }
      else if (!strcmp (*argv, "--help"))
        {
          puts ("Usage: " PGM " [OPTIONS] [FILES]\n"
                "Various public key tests:\n\n"
                "  Default is to process all given key files\n\n"
                "  --genkey ALGONAME SIZE  Generate a public key\n"
                "\n"
                "  --verbose    enable extra informational output\n"
                "  --debug      enable additional debug output\n"
                "  --help       display this help and exit\n\n");
          exit (0);
        }
      else if (!strcmp (*argv, "--verbose"))
        {
          verbose++;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--debug"))
        {
          verbose = debug = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--genkey"))
        {
          genkey_mode = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--fips"))
        {
          fips_mode = 1;
          argc--; argv++;
        }
    }

  gcry_control (GCRYCTL_SET_VERBOSITY, (int)verbose);

  if (fips_mode)
    gcry_control (GCRYCTL_FORCE_FIPS_MODE, 0);

  gcry_control (GCRYCTL_DISABLE_SECMEM);
  if (!gcry_check_version (GCRYPT_VERSION))
    {
      fprintf (stderr, PGM ": version mismatch\n");
      exit (1);
    }

  if (genkey_mode)
    {
      /* No valuable keys are create, so we can speed up our RNG. */
      gcry_control (GCRYCTL_ENABLE_QUICK_RANDOM, 0);
    }
  if (debug)
    gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1u, 0);
  gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);


  if (genkey_mode && argc == 2)
    {
      generate_key (argv[0], argv[1]);
    }
  else if (!genkey_mode && argc)
    {
      int i;

      for (i = 0; i < argc; i++)
	process_key_pair_file (argv[i]);
    }
  else
    {
      fprintf (stderr, "usage: " PGM
               " [OPTIONS] [FILES] (try --help for more information)\n");
      exit (1);
    }

  return error_count ? 1 : 0;
}
