/* genrandom.c - Wrapper around gpg --gen-random
 * Copyright (C) 2025 g10 Code GmbH
 *
 * This file is part of GPGME.
 *
 * GPGME 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.
 *
 * GPGME 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 <https://gnu.org/licenses/>.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#if HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdlib.h>
#include <assert.h>

#include "gpgme.h"
#include "debug.h"
#include "context.h"
#include "ops.h"


static gpgme_error_t
do_genrandom (gpgme_ctx_t ctx, gpgme_data_t dataout, size_t length, int zbase)
{
  gpgme_error_t err;
  const char *argv[4];
  char countbuf[35];

  if (ctx->protocol != GPGME_PROTOCOL_OPENPGP)
    return gpg_error (GPG_ERR_UNSUPPORTED_PROTOCOL);

  err = _gpgme_op_reset (ctx, 1/*synchronous*/);
  if (err)
    return err;

  snprintf (countbuf, sizeof countbuf, "%zu", length);
  argv[0] = "--gen-random";
  argv[1] = zbase? "30" : "2";
  argv[2] = countbuf;
  argv[3] = NULL;

  err = _gpgme_engine_op_getdirect (ctx->engine, argv, dataout, 0);
  if (!err)
    err = _gpgme_wait_one (ctx);

  return err;
}


/* Store a random value into RETVAL or return an error.  */
static gpgme_error_t
getrandom_size_t (gpgme_ctx_t ctx, size_t *retval)
{
  gpgme_error_t err = 0;
  gpgme_data_t data = NULL;
  char *datap = NULL;
  size_t datalen;

  err = gpgme_data_new (&data);
  if (err)
    goto leave;

  err = do_genrandom (ctx, data, sizeof (size_t), 0);
  if (!err)
    err = _gpgme_wait_one (ctx);
  if (err)
    goto leave;

  datap = gpgme_data_release_and_get_mem (data, &datalen);
  data = NULL;
  if (!datap)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }
  if (datalen != sizeof (size_t))
    {
      err = gpg_error (GPG_ERR_INTERNAL);
      goto leave;
    }
  memcpy (retval, datap, datalen);

 leave:
  free (datap);
  gpgme_data_release (data);
  return err;
}


/* Fill BUFFER of size BUFSIZE with random bytes retrieved from gpg.
 * If GPGME_RANDOM_MODE_ZBASE32 is used BUFSIZE needs to be at least
 * 31 and will be filled with a string of 30 ascii characters followed
 * by a Nul; the remainder of the buffer is not changed.  In all other
 * modes the entire buffer will be filled with binary data.  The
 * function has a limit of 1024 bytes to avoid accidental overuse of
 * the random generator. */
gpgme_error_t
gpgme_op_random_bytes (gpgme_ctx_t ctx, gpgme_random_mode_t mode,
                       char *buffer, size_t bufsize)
{
  gpgme_error_t err = 0;
  gpgme_data_t data = NULL;
  char *datap = NULL;
  size_t datalen;

  TRACE_BEG  (DEBUG_CTX, "gpgme_op_random_bytes", ctx, "mode=%d size=%zu",
              mode, bufsize);

  if (!ctx || !buffer || !bufsize)
    err = gpg_error (GPG_ERR_INV_VALUE);
  else if (mode == GPGME_RANDOM_MODE_ZBASE32)
    {
      /* The output is expected to be 30 ascii characters followed by
       * a trailing Nul. */
      if (bufsize < 31)
        err = gpg_error (GPG_ERR_BUFFER_TOO_SHORT);
    }
  else if (mode)
    err = gpg_error (GPG_ERR_INV_VALUE);
  else if (bufsize > 1024) /* More or an less arbitrary limit.  */
    err = gpg_error (GPG_ERR_TOO_LARGE);

  if (err)
    goto leave;

  err = gpgme_data_new (&data);
  if (err)
    goto leave;

  err = do_genrandom (ctx, data, bufsize, (mode == GPGME_RANDOM_MODE_ZBASE32));
  if (!err)
    err = _gpgme_wait_one (ctx);
  if (err)
    goto leave;

  datap = gpgme_data_release_and_get_mem (data, &datalen);
  data = NULL;
  if (!datap)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }
  if (datalen > bufsize)
    {
      err = gpg_error (GPG_ERR_INTERNAL);
      goto leave;
    }
  if (mode == GPGME_RANDOM_MODE_ZBASE32)
    {
      /* Strip trailing LF.  */
      while (datalen
             && (datap[datalen-1] == '\n' || datap[datalen-1] == '\r'))
        datalen--;

      if (datalen != 30)
        {
          /* 30 is the holy count, not 29, not 31 and never 32. */
          err = gpg_error (GPG_ERR_INTERNAL);
          goto leave;
        }
      memcpy (buffer, datap, datalen);
      buffer[datalen] = 0;
    }
  else
    {
      if (datalen != bufsize)
        {
          err = gpg_error (GPG_ERR_INTERNAL);
          goto leave;
        }
      memcpy (buffer, datap, datalen);
    }

 leave:
  free (datap);
  gpgme_data_release (data);
  return TRACE_ERR (err);
}



/* On success the function stores an unbiased random value in the
 * range [0,limit) at RETVAL.  On error the function returns an error
 * code and the value at RETVAL is undefined.  A context must be
 * provided so that the function can get raw random bytes from the gpg
 * enine (i.e. from Libgcrypt). */
gpgme_error_t
gpgme_op_random_value (gpgme_ctx_t ctx, size_t limit, size_t *retval)
{
  gpgme_error_t err;
  size_t t, x;

  TRACE_BEG  (DEBUG_CTX, "gpgme_op_random_value", ctx, "limit=%zu", limit);

  if (!ctx || limit < 2 || !retval)
    {
      err = gpg_error (GPG_ERR_INV_VALUE);
      goto leave;
    }

  /* This is the OpenBSD algorithm as used by arc4random_uniform and
   * described in "Fast Random Integer Generation in an Interval" by
   * Daniel Lemire (arXiv:1805.10941v4, 2018).  */
  t = (-limit) % limit;
  x = 0;  /* Avoid (false) compiler warning.  */
  do
    {
      err = getrandom_size_t (ctx, &x);
      if (err)
        goto leave;
      /* fprintf (stderr, "getrandom returned %zu (limit=%zu, t=%zu)\n", */
      /*          x, limit, t); */
    }
  while (x < t);
  x %= limit;

  *retval = x;

 leave:
  return TRACE_ERR (err);
}
