/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2017 Red Hat, Inc.
 */

#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-random-utils.h"

#include <fcntl.h>
#include <sys/auxv.h>
#include <sys/syscall.h>

#if USE_SYS_RANDOM_H
#include <sys/random.h>
#else
#include <linux/random.h>
#endif

#include "nm-shared-utils.h"
#include "nm-time-utils.h"

/*****************************************************************************/

#if !defined(SYS_getrandom) && defined(__NR_getrandom)
#define SYS_getrandom __NR_getrandom
#endif

#ifndef GRND_NONBLOCK
#define GRND_NONBLOCK 0x01
#endif

#ifndef GRND_INSECURE
#define GRND_INSECURE 0x04
#endif

#if !HAVE_GETRANDOM && defined(SYS_getrandom)
static int
getrandom(void *buf, size_t buflen, unsigned flags)
{
    return syscall(SYS_getrandom, buf, buflen, flags);
}
#undef HAVE_GETRANDOM
#define HAVE_GETRANDOM 1
#endif

/*****************************************************************************/

typedef struct _nm_packed {
    uintptr_t heap_ptr;
    uintptr_t stack_ptr;
    gint64    now_bootime;
    gint64    now_real;
    pid_t     pid;
    pid_t     ppid;
    pid_t     tid;
    guint32   grand[16];
    guint8    auxval[16];
    guint8    getrandom_buf[20];
} BadRandSeed;

typedef struct _nm_packed {
    guint64 counter;
    union {
        guint8 full[NM_UTILS_CHECKSUM_LENGTH_SHA256];
        struct {
            guint8 half_1[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
            guint8 half_2[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
        };
    } sha_digest;
    union {
        guint8  u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
        guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
    } rand_vals;
    GRand *rand;
} BadRandState;

static void
_bad_random_init_seed(BadRandSeed *seed)
{
    const guint8 *p_at_random;
    int           seed_idx;
    GRand        *rand;

    /* g_rand_new() reads /dev/urandom, but we already noticed that
     * /dev/urandom fails to give us good randomness (which is why
     * we hit the "bad randomness" code path). So this may not be as
     * good as we wish, but let's hope that it it does something smart
     * to give some extra entropy... */
    rand = g_rand_new();

    /* Get some seed material from a GRand. */
    for (seed_idx = 0; seed_idx < (int) G_N_ELEMENTS(seed->grand); seed_idx++)
        seed->grand[seed_idx] = g_rand_int(rand);

    /* Add an address from the heap and stack, maybe ASLR helps a bit? */
    seed->heap_ptr  = (uintptr_t) ((gpointer) rand);
    seed->stack_ptr = (uintptr_t) ((gpointer) &rand);

    g_rand_free(rand);

    /* Add the per-process, random number. */
    p_at_random = ((gpointer) getauxval(AT_RANDOM));
    if (p_at_random) {
        G_STATIC_ASSERT(sizeof(seed->auxval) == 16);
        memcpy(&seed->auxval, p_at_random, 16);
    }

#if HAVE_GETRANDOM
    {
        ssize_t r;

        /* This is likely to fail, because we already failed a moment earlier. Still, give
         * it a try. */
        r = getrandom(seed->getrandom_buf,
                      sizeof(seed->getrandom_buf),
                      GRND_INSECURE | GRND_NONBLOCK);
        (void) r;
    }
#endif

    seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
    seed->now_real    = g_get_real_time();
    seed->pid         = getpid();
    seed->ppid        = getppid();
    seed->tid         = nm_utils_gettid();
}

static void
_bad_random_bytes(guint8 *buf, gsize n)
{
    nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256);

    nm_assert(n > 0);

    /* We are in the fallback code path, where getrandom() (and /dev/urandom) failed
     * to give us good randomness. Try our best.
     *
     * Our ability to get entropy for the CPRNG is very limited and thus the overall
     * result will not be good randomness. See _bad_random_init_seed().
     *
     * Once we have some seed material, we combine GRand (which is not a cryptographically
     * secure PRNG) with some iterative sha256 hashing. It would be nice if we had
     * easy access to chacha20, but it's probably more cumbersome to fork those
     * implementations than hack a bad CPRNG by using sha256 hashing. After all, this
     * is fallback code to get *some* randomness. And with the inability to get a good
     * seed, the CPRNG is not going to give us truly good randomness. */

    {
        static BadRandState gl_state;
        static GRand       *gl_rand;
        static GMutex       gl_mutex;
        NM_G_MUTEX_LOCKED(&gl_mutex);

        if (G_UNLIKELY(!gl_rand)) {
            union {
                BadRandSeed d_seed;
                guint32     d_u32[(sizeof(BadRandSeed) + 3) / 4];
            } data = {
                .d_u32 = {0},
            };

            _bad_random_init_seed(&data.d_seed);

            gl_rand = g_rand_new_with_seed_array(data.d_u32, G_N_ELEMENTS(data.d_u32));

            g_checksum_update(sum, (const guchar *) &data, sizeof(data));
            nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
        }

        while (TRUE) {
            int i;

            gl_state.counter++;
            for (i = 0; i < G_N_ELEMENTS(gl_state.rand_vals.u32); i++)
                gl_state.rand_vals.u32[i] = g_rand_int(gl_rand);
            g_checksum_reset(sum);
            g_checksum_update(sum, (const guchar *) &gl_state, sizeof(gl_state));
            nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);

            /* gl_state.sha_digest.full and gl_state.rand_vals contain now our
             * random values, but they are also the state for the next iteration.
             * We must not directly expose that state to the caller, so XOR the values.
             *
             * That means, per iteration we can generate 16 bytes of randomness. That
             * is for example required to generate a random UUID. */
            for (i = 0; i < (int) (NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2); i++) {
                nm_assert(n > 0);
                buf[0] = gl_state.sha_digest.half_1[i] ^ gl_state.sha_digest.half_2[i]
                         ^ gl_state.rand_vals.u8[i];
                buf++;
                n--;
                if (n == 0)
                    return;
            }
        }
    }
}

/**
 * nm_utils_random_bytes:
 * @p: the buffer to fill
 * @n: the number of bytes to write to @p.
 *
 * Uses getrandom() or reads /dev/urandom to fill the buffer
 * with random data. If all fails, as last fallback it uses
 * GRand to fill the buffer with pseudo random numbers.
 * The function always succeeds in writing some random numbers
 * to the buffer. The return value of FALSE indicates that the
 * obtained bytes are probably not of good randomness.
 *
 * Returns: whether the written bytes are good. If you
 * don't require good randomness, you can ignore the return
 * value.
 *
 * Note that if calling getrandom() fails because there is not enough
 * entropy (at early boot), the function will read /dev/urandom.
 * Which of course, still has low entropy, and cause kernel to log
 * a warning.
 */
gboolean
nm_utils_random_bytes(void *p, size_t n)
{
    int      fd;
    int      r;
    gboolean has_high_quality = TRUE;
    guint8  *buf              = p;

    g_return_val_if_fail(p, FALSE);
    g_return_val_if_fail(n > 0, FALSE);

#if HAVE_GETRANDOM
    {
        static gboolean have_syscall = TRUE;

        if (have_syscall) {
            ssize_t r2;
            int     errsv;

            r2 = getrandom(buf, n, GRND_NONBLOCK);
            if (r2 >= 0) {
                if ((size_t) r2 == n)
                    return TRUE;

                /* no or partial read. There is not enough entropy.
                 * Fill the rest reading with the fallback code and remember
                 * that some bits are not high quality. */
                nm_assert((size_t) r2 < n);
                buf += r2;
                n -= r2;

                /* At this point, we don't want to read /dev/urandom, because
                 * the entropy pool is low (early boot?), and asking for more
                 * entropy causes kernel messages to be logged.
                 *
                 * Note that we fall back to _bad_random_bytes(), which (among others) seeds
                 * itself with g_rand_new(). That also will read /dev/urandom, but as
                 * we do that only once, we don't care. But in general, we are here in
                 * a situation where we want to avoid reading /dev/urandom too much. */
                goto out_bad_random;
            }
            errsv = errno;
            if (errsv == ENOSYS) {
                /* no support for getrandom(). We don't know whether
                 * we /dev/urandom will give us good quality. Assume yes. */
                have_syscall = FALSE;
            } else if (errsv == EAGAIN) {
                /* No entropy. We avoid reading /dev/urandom. */
                goto out_bad_random;
            } else {
                /* Unknown error, likely no entropy. We'll read /dev/urandom below, but we don't
                 * have high-quality randomness. */
                has_high_quality = FALSE;
            }
        }
    }
#endif

fd_open:
    fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
    if (fd < 0) {
        if (errno == EINTR)
            goto fd_open;
        goto out_bad_random;
    }
    r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE);
    nm_close(fd);
    if (r >= 0)
        return has_high_quality;

out_bad_random:
    /* we failed to fill the bytes reading from /dev/urandom.
     * Fill the bits using our pseudo random numbers.
     *
     * We don't have good quality.
     */
    _bad_random_bytes(buf, n);
    return FALSE;
}
