/* Retrieval of cryptographically random bytes from the operating system.
 *
 * Written by Zack Weinberg <zackw at panix.com> in 2017.
 *
 * No copyright is claimed, and the software is hereby placed in the public
 * domain.  In case this attempt to disclaim copyright and place the software
 * in the public domain is deemed null and void, then the software is
 * Copyright (c) 2017 Zack Weinberg and it is hereby released to the
 * general public under the following terms:
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted.
 *
 * There's ABSOLUTELY NO WARRANTY, express or implied.
 */

#include "crypt-port.h"

#include <errno.h>
#include <stdlib.h>

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_SYS_RANDOM_H
#include <sys/random.h>
#endif
#ifdef HAVE_SYS_SYSCALL_H
#include <sys/syscall.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

/* If we have O_CLOEXEC, we use it, but if we don't, we don't worry
   about it.  */
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif

/* There is no universally portable way to access a system CSPRNG.
   If the C library provides any of the following functions, we try them,
   in order of preference: arc4random_buf, getentropy, getrandom.
   If none of those are available or they don't work, we attempt to
   make direct system calls for getentropy and getrandom.  If *that*
   doesn't work, we try opening and reading /dev/urandom.

   This function returns true if the exact number of requested bytes
   was successfully read, false otherwise; if it returns false, errno
   has been set.  It may block.  It cannot be used to read more than
   256 bytes at a time (this is a limitation inherited from
   getentropy() and enforced regardless of the actual back-end in use).

   If we fall all the way back to /dev/urandom, we open and close it on
   each call.  */

bool
get_random_bytes(void *buf, size_t buflen)
{
  if (buflen == 0)
    return true;

  /* Some, but not all, of the primitives below are limited to
     producing no more than 256 bytes of random data.  Impose this
     constraint on our callers regardless of which primitive is
     actually used.  */
  if (buflen > 256)
    {
      errno = EIO;
      return false;
    }

  /* To eliminate the possibility of one of the primitives below failing
     with EFAULT, force a crash now if the buffer is unwritable.  */
  explicit_bzero (buf, buflen);

#ifdef HAVE_ARC4RANDOM_BUF
  /* arc4random_buf, if it exists, can never fail.  */
  arc4random_buf (buf, buflen);
  return true;

#else /* no arc4random_buf */

#ifdef HAVE_GETENTROPY
  /* getentropy may exist but lack kernel support.  */
  static bool getentropy_doesnt_work;
  if (!getentropy_doesnt_work)
    {
      if (!getentropy (buf, buflen))
        return true;
      getentropy_doesnt_work = true;
    }
#endif

#ifdef HAVE_GETRANDOM
  /* Likewise getrandom.  */
  static bool getrandom_doesnt_work;
  if (!getrandom_doesnt_work)
    {
      if ((size_t)getrandom (buf, buflen, 0) == buflen)
        return true;
      getrandom_doesnt_work = true;
    }
#endif

  /* If we can make arbitrary syscalls, try getentropy and getrandom
     again that way.  */
#ifdef HAVE_SYSCALL
#ifdef SYS_getentropy
  static bool sys_getentropy_doesnt_work;
  if (!sys_getentropy_doesnt_work)
    {
      if (!syscall (SYS_getentropy, buf, buflen))
        return true;
      sys_getentropy_doesnt_work = true;
    }
#endif

#ifdef SYS_getrandom
  static bool sys_getrandom_doesnt_work;
  if (!sys_getrandom_doesnt_work)
    {
      if ((size_t)syscall (SYS_getrandom, buf, buflen, 0) == buflen)
        return true;
      sys_getrandom_doesnt_work = true;
    }
#endif
#endif

#if defined HAVE_SYS_STAT_H && defined HAVE_FCNTL_H && defined HAVE_UNISTD_H
  /* Try reading from /dev/urandom.  */
  static bool dev_urandom_doesnt_work;
  if (!dev_urandom_doesnt_work)
    {
      int fd = open ("/dev/urandom", O_RDONLY|O_CLOEXEC);
      if (fd == -1)
        dev_urandom_doesnt_work = true;
      else
        {
          ssize_t nread = read (fd, buf, buflen);
          if (nread < 0 || (size_t)nread < buflen)
            dev_urandom_doesnt_work = true;

          close(fd);
          return !dev_urandom_doesnt_work;
        }
    }
#endif
#endif /* no arc4random_buf */

  /* if we get here, we're just completely hosed */
  errno = ENOSYS;
  return false;
}
