/* random.c - part of the Libgcrypt test suite.
   Copyright (C) 2005 Free Software Foundation, Inc.

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   This program 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
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
   USA.  */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#ifndef HAVE_W32_SYSTEM
# include <signal.h>
# include <unistd.h>
# include <sys/wait.h>
#endif

#include "../src/gcrypt-int.h"

#define PGM "random"

#ifndef DIM
# define DIM(v)		     (sizeof(v)/sizeof((v)[0]))
#endif


static int verbose;
static int debug;
static int with_progress;

/* If we have a decent libgpg-error we can use some gcc attributes.  */
#ifdef GPGRT_ATTR_NORETURN
static void die (const char *format, ...) GPGRT_ATTR_NR_PRINTF(1,2);
static void inf (const char *format, ...) GPGRT_ATTR_PRINTF(1,2);
#endif /*GPGRT_ATTR_NORETURN*/


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

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


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

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


static void
print_hex (const char *text, const void *buf, size_t n)
{
  const unsigned char *p = buf;

  inf ("%s", text);
  for (; n; n--, p++)
    fprintf (stderr, "%02X", *p);
  putc ('\n', stderr);
}


static void
progress_cb (void *cb_data, const char *what, int printchar,
             int current, int total)
{
  (void)cb_data;

  inf ("progress (%s %c %d %d)\n", what, printchar, current, total);
  fflush (stderr);
}


#ifndef HAVE_W32_SYSTEM
static int
writen (int fd, const void *buf, size_t nbytes)
{
  size_t nleft = nbytes;
  int nwritten;

  while (nleft > 0)
    {
      nwritten = write (fd, buf, nleft);
      if (nwritten < 0)
        {
          if (errno == EINTR)
            nwritten = 0;
          else
            return -1;
        }
      nleft -= nwritten;
      buf = (const char*)buf + nwritten;
    }

  return 0;
}
#endif /*!HAVE_W32_SYSTEM*/


#ifndef HAVE_W32_SYSTEM
static int
readn (int fd, void *buf, size_t buflen, size_t *ret_nread)
{
  size_t nleft = buflen;
  int nread;

  while ( nleft > 0 )
    {
      nread = read ( fd, buf, nleft );
      if (nread < 0)
        {
          if (nread == EINTR)
            nread = 0;
          else
            return -1;
        }
      else if (!nread)
        break; /* EOF */
      nleft -= nread;
      buf = (char*)buf + nread;
    }
  if (ret_nread)
    *ret_nread = buflen - nleft;
  return 0;
}
#endif /*!HAVE_W32_SYSTEM*/


/* Check that forking won't return the same random. */
static void
check_forking (void)
{
#ifdef HAVE_W32_SYSTEM
  if (verbose)
    inf ("check_forking skipped: not applicable on Windows\n");
#else /*!HAVE_W32_SYSTEM*/
  pid_t pid;
  int rp[2];
  int i, status;
  size_t nread;
  char tmp1[16], tmp1c[16], tmp1p[16];

  if (verbose)
    inf ("checking that a fork won't cause the same random output\n");

  /* We better make sure that the RNG has been initialzied. */
  gcry_randomize (tmp1, sizeof tmp1, GCRY_STRONG_RANDOM);
  if (verbose)
    print_hex ("initial random: ", tmp1, sizeof tmp1);

  if (pipe (rp) == -1)
    die ("pipe failed: %s\n", strerror (errno));

  pid = fork ();
  if (pid == (pid_t)(-1))
    die ("fork failed: %s\n", strerror (errno));
  if (!pid)
    {
      gcry_randomize (tmp1c, sizeof tmp1c, GCRY_STRONG_RANDOM);
      if (writen (rp[1], tmp1c, sizeof tmp1c))
        die ("write failed: %s\n", strerror (errno));
      if (verbose)
        {
          print_hex ("  child random: ", tmp1c, sizeof tmp1c);
          fflush (stdout);
        }
      _exit (0);
    }
  gcry_randomize (tmp1p, sizeof tmp1p, GCRY_STRONG_RANDOM);
  if (verbose)
    print_hex (" parent random: ", tmp1p, sizeof tmp1p);

  close (rp[1]);
  if (readn (rp[0], tmp1c, sizeof tmp1c, &nread))
    die ("read failed: %s\n", strerror (errno));
  if (nread != sizeof tmp1c)
    die ("read too short\n");

  while ( (i=waitpid (pid, &status, 0)) == -1 && errno == EINTR)
    ;
  if (i != (pid_t)(-1)
      && WIFEXITED (status) && !WEXITSTATUS (status))
    ;
  else
    die ("child failed\n");

  if (!memcmp (tmp1p, tmp1c, sizeof tmp1c))
    die ("parent and child got the same random number\n");
#endif  /*!HAVE_W32_SYSTEM*/
}



/* Check that forking won't return the same nonce. */
static void
check_nonce_forking (void)
{
#ifdef HAVE_W32_SYSTEM
  if (verbose)
    inf ("check_nonce_forking skipped: not applicable on Windows\n");
#else /*!HAVE_W32_SYSTEM*/
  pid_t pid;
  int rp[2];
  int i, status;
  size_t nread;
  char nonce1[10], nonce1c[10], nonce1p[10];

  if (verbose)
    inf ("checking that a fork won't cause the same nonce output\n");

  /* We won't get the same nonce back if we never initialized the
     nonce subsystem, thus we get one nonce here and forget about
     it. */
  gcry_create_nonce (nonce1, sizeof nonce1);
  if (verbose)
    print_hex ("initial nonce: ", nonce1, sizeof nonce1);

  if (pipe (rp) == -1)
    die ("pipe failed: %s\n", strerror (errno));

  pid = fork ();
  if (pid == (pid_t)(-1))
    die ("fork failed: %s\n", strerror (errno));
  if (!pid)
    {
      gcry_create_nonce (nonce1c, sizeof nonce1c);
      if (writen (rp[1], nonce1c, sizeof nonce1c))
        die ("write failed: %s\n", strerror (errno));
      if (verbose)
        {
          print_hex ("  child nonce: ", nonce1c, sizeof nonce1c);
          fflush (stdout);
        }
      _exit (0);
    }
  gcry_create_nonce (nonce1p, sizeof nonce1p);
  if (verbose)
    print_hex (" parent nonce: ", nonce1p, sizeof nonce1p);

  close (rp[1]);
  if (readn (rp[0], nonce1c, sizeof nonce1c, &nread))
    die ("read failed: %s\n", strerror (errno));
  if (nread != sizeof nonce1c)
    die ("read too short\n");

  while ( (i=waitpid (pid, &status, 0)) == -1 && errno == EINTR)
    ;
  if (i != (pid_t)(-1)
      && WIFEXITED (status) && !WEXITSTATUS (status))
    ;
  else
    die ("child failed\n");

  if (!memcmp (nonce1p, nonce1c, sizeof nonce1c))
    die ("parent and child got the same nonce\n");
#endif  /*!HAVE_W32_SYSTEM*/
}


/* Check that a closed random device os re-opened if needed. */
static void
check_close_random_device (void)
{
#ifdef HAVE_W32_SYSTEM
  if (verbose)
    inf ("check_close_random_device skipped: not applicable on Windows\n");
#else /*!HAVE_W32_SYSTEM*/
  pid_t pid;
  int i, status;
  char buf[4];

  if (verbose)
    inf ("checking that close_random_device works\n");

  gcry_randomize (buf, sizeof buf, GCRY_STRONG_RANDOM);
  if (verbose)
    print_hex ("parent random: ", buf, sizeof buf);

  pid = fork ();
  if (pid == (pid_t)(-1))
    die ("fork failed: %s\n", strerror (errno));
  if (!pid)
    {
      gcry_control (GCRYCTL_CLOSE_RANDOM_DEVICE, 0);

      /* The next call will re-open the device.  */
      gcry_randomize (buf, sizeof buf, GCRY_STRONG_RANDOM);
      if (verbose)
        {
          print_hex ("child random : ", buf, sizeof buf);
          fflush (stdout);
        }
      _exit (0);
    }

  while ( (i=waitpid (pid, &status, 0)) == -1 && errno == EINTR)
    ;
  if (i != (pid_t)(-1)
      && WIFEXITED (status) && !WEXITSTATUS (status))
    ;
  else
    die ("child failed\n");

#endif  /*!HAVE_W32_SYSTEM*/
}


static int
rng_type (void)
{
  int rngtype;
  if (gcry_control (GCRYCTL_GET_CURRENT_RNG_TYPE, &rngtype))
    die ("retrieving RNG type failed\n");
  return rngtype;
}


static void
check_rng_type_switching (void)
{
  int rngtype, initial;
  char tmp1[4];

  if (verbose)
    inf ("checking whether RNG type switching works\n");

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  initial = rngtype;
  gcry_randomize (tmp1, sizeof tmp1, GCRY_STRONG_RANDOM);
  if (debug)
    print_hex ("  sample: ", tmp1, sizeof tmp1);
  if (rngtype != rng_type ())
    die ("RNG type unexpectedly changed\n");

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_SYSTEM);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (rngtype != initial)
    die ("switching to System RNG unexpectedly succeeded\n");
  gcry_randomize (tmp1, sizeof tmp1, GCRY_STRONG_RANDOM);
  if (debug)
    print_hex ("  sample: ", tmp1, sizeof tmp1);
  if (rngtype != rng_type ())
    die ("RNG type unexpectedly changed\n");

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_FIPS);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (rngtype != initial)
    die ("switching to FIPS RNG unexpectedly succeeded\n");
  gcry_randomize (tmp1, sizeof tmp1, GCRY_STRONG_RANDOM);
  if (debug)
    print_hex ("  sample: ", tmp1, sizeof tmp1);
  if (rngtype != rng_type ())
    die ("RNG type unexpectedly changed\n");

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_STANDARD);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (rngtype != GCRY_RNG_TYPE_STANDARD)
    die ("switching to standard RNG failed\n");
  gcry_randomize (tmp1, sizeof tmp1, GCRY_STRONG_RANDOM);
  if (debug)
    print_hex ("  sample: ", tmp1, sizeof tmp1);
  if (rngtype != rng_type ())
    die ("RNG type unexpectedly changed\n");
}


static void
check_early_rng_type_switching (void)
{
  int rngtype, initial;

  if (verbose)
    inf ("checking whether RNG type switching works in the early stage\n");

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  initial = rngtype;

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_SYSTEM);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (initial >= GCRY_RNG_TYPE_SYSTEM && rngtype != GCRY_RNG_TYPE_SYSTEM)
    die ("switching to System RNG failed\n");

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_FIPS);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (initial >= GCRY_RNG_TYPE_FIPS && rngtype != GCRY_RNG_TYPE_FIPS)
    die ("switching to FIPS RNG failed\n");

  gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_STANDARD);

  rngtype = rng_type ();
  if (debug)
    inf ("rng type: %d\n", rngtype);
  if (rngtype != GCRY_RNG_TYPE_STANDARD)
    die ("switching to standard RNG failed\n");
}


static void
check_drbg_reinit (void)
{
  static struct { const char *flags; } tv[] = {
    { NULL },
    { "" },
    { "sha1" },
    { "sha1 pr" },
    { "sha256" },
    { "sha256 pr" },
    { "sha512" },
    { "sha512 pr" },
    { "hmac sha1" },
    { "hmac sha1 pr" },
    { "hmac sha256" },
    { "hmac sha256 pr" },
    { "hmac sha512" },
    { "hmac sha512 pr" },
    { "aes sym128" },
    { "aes sym128 pr" },
    { "aes sym192" },
    { "aes sym192 pr" },
    { "aes sym256" },
    { "aes sym256 pr" }
  };
  int tidx;
  gpg_error_t err;
  char pers_string[] = "I'm a doctor, not an engineer.";
  gcry_buffer_t pers[1];

  if (verbose)
    inf ("checking DRBG_REINIT\n");

  memset (pers, 0, sizeof pers);
  pers[0].data = pers_string;
  pers[0].len = strlen (pers_string);

  err = gcry_control (GCRYCTL_DRBG_REINIT, "", NULL, 0, &err);
  if (gpg_err_code (err) != GPG_ERR_INV_ARG)
    die ("gcry_control(DRBG_REINIT) guard value did not work\n");

  err = gcry_control (GCRYCTL_DRBG_REINIT, "", NULL, -1, NULL);
  if (gpg_err_code (err) != GPG_ERR_INV_ARG)
    die ("gcry_control(DRBG_REINIT) npers negative detection failed\n");

  if (rng_type () != GCRY_RNG_TYPE_FIPS)
    {
      err = gcry_control (GCRYCTL_DRBG_REINIT, "", NULL, 0, NULL);
      if (gpg_err_code (err) != GPG_ERR_NOT_SUPPORTED)
        die ("DRBG_REINIT worked despite that DRBG is not active\n");
      return;
    }

  err = gcry_control (GCRYCTL_DRBG_REINIT, "", NULL, 1, NULL);
  if (gpg_err_code (err) != GPG_ERR_INV_ARG)
    die ("_gcry_rngdrbg_reinit failed to detact: (!pers && npers)\n");
  err = gcry_control (GCRYCTL_DRBG_REINIT, "", pers, 2, NULL);
  if (gpg_err_code (err) != GPG_ERR_INV_ARG)
    die ("_gcry_rngdrbg_reinit failed to detect: (pers && npers != 1)\n");

  err = gcry_control (GCRYCTL_DRBG_REINIT, "aes sym128 bad pr ", pers, 1, NULL);
  if (gpg_err_code (err) != GPG_ERR_INV_FLAG)
    die ("_gcry_rngdrbg_reinit failed to detect a bad flag\n");

  for (tidx=0; tidx < DIM(tv); tidx++)
    {
      err = gcry_control (GCRYCTL_DRBG_REINIT, tv[tidx].flags, NULL, 0, NULL);
      if (err)
        die ("_gcry_rngdrbg_reinit failed for \"%s\" w/o pers: %s\n",

             tv[tidx].flags, gpg_strerror (err));
      err = gcry_control (GCRYCTL_DRBG_REINIT, tv[tidx].flags, pers, 1, NULL);
      if (err)
        die ("_gcry_rngdrbg_reinit failed for \"%s\" with pers: %s\n",
             tv[tidx].flags, gpg_strerror (err));
      /* fixme: We should extract some random after each test.  */
    }
}


/* Because we want to check initialization behaviour, we need to
   fork/exec this program with several command line arguments.  We use
   system, so that these tests work also on Windows.  */
static void
run_all_rng_tests (const char *program)
{
  static const char *options[] = {
    "--early-rng-check",
    "--early-rng-check --prefer-standard-rng",
    "--early-rng-check --prefer-fips-rng",
    "--early-rng-check --prefer-system-rng",
    "--prefer-standard-rng",
    "--prefer-fips-rng",
    "--prefer-system-rng",
    NULL
  };
  int idx;
  size_t len, maxlen;
  char *cmdline;

  maxlen = 0;
  for (idx=0; options[idx]; idx++)
    {
      len = strlen (options[idx]);
      if (len > maxlen)
        maxlen = len;
    }
  maxlen += strlen (program);
  maxlen += strlen (" --in-recursion --verbose --debug --progress");
  maxlen++;
  cmdline = malloc (maxlen + 1);
  if (!cmdline)
    die ("out of core\n");

  for (idx=0; options[idx]; idx++)
    {
      if (verbose)
        inf ("now running with options '%s'\n", options[idx]);
      strcpy (cmdline, program);
      strcat (cmdline, " --in-recursion");
      if (verbose)
        strcat (cmdline, " --verbose");
      if (debug)
        strcat (cmdline, " --debug");
      if (with_progress)
        strcat (cmdline, " --progress");
      strcat (cmdline, " ");
      strcat (cmdline, options[idx]);
      if (system (cmdline))
        die ("running '%s' failed\n", cmdline);
    }

  free (cmdline);
}

int
main (int argc, char **argv)
{
  int last_argc = -1;
  int early_rng = 0;
  int in_recursion = 0;
  const char *program = NULL;

  if (argc)
    {
      program = *argv;
      argc--; argv++;
    }
  else
    die ("argv[0] missing\n");

  while (argc && last_argc != argc )
    {
      last_argc = argc;
      if (!strcmp (*argv, "--"))
        {
          argc--; argv++;
          break;
        }
      else if (!strcmp (*argv, "--help"))
        {
          fputs ("usage: random [options]\n", stdout);
          exit (0);
        }
      else if (!strcmp (*argv, "--verbose"))
        {
          verbose = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--debug"))
        {
          debug = verbose = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--progress"))
        {
          argc--; argv++;
          with_progress = 1;
        }
      else if (!strcmp (*argv, "--in-recursion"))
        {
          in_recursion = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--early-rng-check"))
        {
          early_rng = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--prefer-standard-rng"))
        {
          /* This is anyway the default, but we may want to use it for
             debugging. */
          gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_STANDARD);
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--prefer-fips-rng"))
        {
          gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_FIPS);
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--prefer-system-rng"))
        {
          gcry_control (GCRYCTL_SET_PREFERRED_RNG_TYPE, GCRY_RNG_TYPE_SYSTEM);
          argc--; argv++;
        }
    }

#ifndef HAVE_W32_SYSTEM
  signal (SIGPIPE, SIG_IGN);
#endif

  if (early_rng)
    {
      /* Don't switch RNG in fips mode. */
      if (!gcry_fips_mode_active())
        check_early_rng_type_switching ();
    }

  gcry_control (GCRYCTL_DISABLE_SECMEM, 0);
  if (!gcry_check_version (GCRYPT_VERSION))
    die ("version mismatch\n");

  if (with_progress)
    gcry_set_progress_handler (progress_cb, NULL);

  gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
  if (debug)
    gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1u, 0);

  if (!in_recursion)
    {
      check_forking ();
      check_nonce_forking ();
      check_close_random_device ();
    }
  /* For now we do not run the drgb_reinit check from "make check" due
     to its high requirement for entropy.  */
  if (!getenv ("GCRYPT_IN_REGRESSION_TEST"))
    check_drbg_reinit ();

  /* Don't switch RNG in fips mode.  */
  if (!gcry_fips_mode_active())
    check_rng_type_switching ();

  if (!in_recursion)
    run_all_rng_tests (program);

  return 0;
}
