NetworkManager/src/libnm-glib-aux/nm-random-utils.c
Corentin Noël 5d28a0dd89
doc: replace all (allow-none) annotations by (optional) and/or (nullable)
The (allow-none) annotation is deprecated since a long time now, it is better to
use (nullable) and/or (optional) which clarifies what it means with the (out)
annotation.

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/1551
2023-03-27 11:49:43 +02:00

501 lines
14 KiB
C

/* 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>
#include <poll.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
static ssize_t
getrandom(void *buf, size_t buflen, unsigned flags)
{
#if defined(SYS_getrandom)
return syscall(SYS_getrandom, buf, buflen, flags);
#else
errno = ENOSYS;
return -1;
#endif
}
#endif
/*****************************************************************************/
static ssize_t
_getrandom(void *buf, size_t buflen, unsigned flags)
{
static int have_getrandom = TRUE;
ssize_t l;
int errsv;
nm_assert(buflen > 0);
/* This calls getrandom() and either returns the positive
* success or an negative errno. ENOSYS means getrandom()
* call is not supported. That result is cached and we don't retry. */
if (!have_getrandom)
return -ENOSYS;
l = getrandom(buf, buflen, flags);
if (l > 0)
return l;
if (l == 0)
return -EIO;
errsv = errno;
if (errsv == ENOSYS)
have_getrandom = FALSE;
return -errsv;
}
static ssize_t
_getrandom_insecure(void *buf, size_t buflen)
{
static int have_grnd_insecure = TRUE;
ssize_t l;
/* GRND_INSECURE was added recently. We catch EINVAL
* if kernel does not support the flag (and cache it). */
if (!have_grnd_insecure)
return -EINVAL;
l = _getrandom(buf, buflen, GRND_INSECURE);
if (l == -EINVAL)
have_grnd_insecure = FALSE;
return l;
}
static ssize_t
_getrandom_best_effort(void *buf, size_t buflen)
{
ssize_t l;
/* To get best-effort bytes, we would use GRND_INSECURE (and we try that
* first). However, not all kernel versions support that, so we fallback
* to GRND_NONBLOCK.
*
* Granted, this is called from a fallback path where we have no entropy
* already, it's unlikely that GRND_NONBLOCK would succeed. Still... */
l = _getrandom_insecure(buf, buflen);
if (l != -EINVAL)
return l;
return _getrandom(buf, buflen, GRND_NONBLOCK);
}
static int
_random_check_entropy(gboolean block)
{
static gboolean seen_high_quality = FALSE;
nm_auto_close int fd = -1;
int r;
/* We come here because getrandom() gave ENOSYS. We will fallback to /dev/urandom,
* but the caller wants to know whether we have high quality numbers. Poll
* /dev/random to find out. */
if (seen_high_quality) {
/* We cache the positive result. Once kernel has entropy, we will get
* good random numbers. */
return 1;
}
fd = open("/dev/random", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0)
return -errno;
r = nm_utils_fd_wait_for_event(fd, POLLIN, block ? -1 : 0);
if (r <= 0) {
nm_assert(r < 0 || !block);
return r;
}
nm_assert(r == 1);
seen_high_quality = TRUE;
return 1;
}
/*****************************************************************************/
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;
guint8 rand_vals_getrandom[16];
gint64 rand_vals_timestamp;
} 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 too, but we already know that
* /dev/urandom fails to give us good randomness (which is why
* we hit the "bad random" 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);
}
_getrandom_best_effort(seed->getrandom_buf, sizeof(seed->getrandom_buf));
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 be bad randomness.
*
* 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* bad randomness. And with the inability to get a good
* seed, any CPRNG can only give us bad 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);
}
_getrandom_best_effort(gl_state.rand_vals_getrandom, sizeof(gl_state.rand_vals_getrandom));
gl_state.rand_vals_timestamp = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
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
* bad 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 bad randomness. That
* is suitable to initialize 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_random_get_bytes_full:
* @p: the buffer to fill
* @n: the number of bytes to write to @p.
* @out_high_quality: (out) (optional): whether the returned
* random bytes are of high quality.
*
* - will never block
* - will always produce some numbers, but they may not
* be of high quality.
* - Whether they are of high quality, you can know via @out_high_quality.
* - will always try hard to produce high quality numbers, and on success
* they are as good as nm_random_get_crypto_bytes().
*/
void
nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality)
{
int fd;
int r;
gboolean has_high_quality;
ssize_t l;
if (n == 0) {
NM_SET_OUT(out_high_quality, TRUE);
return;
}
g_return_if_fail(p);
again_getrandom:
l = _getrandom(p, n, GRND_NONBLOCK);
if (l > 0) {
if ((size_t) l == n) {
NM_SET_OUT(out_high_quality, TRUE);
return;
}
p = ((uint8_t *) p) + l;
n -= l;
goto again_getrandom;
}
/* getrandom() failed. Fallback to read /dev/urandom. */
if (l == -ENOSYS) {
/* no support for getrandom(). */
if (out_high_quality) {
/* The caller wants to know whether we have high quality. Poll /dev/random
* to find out. */
has_high_quality = (_random_check_entropy(FALSE) > 0);
} else {
/* The value doesn't matter in this case. It will be unused. */
has_high_quality = FALSE;
}
} else {
/* Any other failure of getrandom() means we don't have high quality. */
has_high_quality = FALSE;
if (l == -EAGAIN) {
/* getrandom(GRND_NONBLOCK) failed because lack of entropy. Retry with GRND_INSECURE. */
for (;;) {
l = _getrandom_insecure(p, n);
if (l > 0) {
if ((size_t) l == n) {
NM_SET_OUT(out_high_quality, FALSE);
return;
}
p = ((uint8_t *) p) + l;
n -= l;
continue;
}
/* Any error. Fallback to /dev/urandom. */
break;
}
}
}
again_open:
fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0) {
if (errno == EINTR)
goto again_open;
} else {
r = nm_utils_fd_read_loop_exact(fd, p, n, TRUE);
nm_close(fd);
if (r >= 0) {
NM_SET_OUT(out_high_quality, has_high_quality);
return;
}
}
/* we failed to fill the bytes reading from /dev/urandom.
* Fill the bits using our fallback approach (which obviously
* cannot give high quality random).
*/
_bad_random_bytes(p, n);
NM_SET_OUT(out_high_quality, FALSE);
}
/*****************************************************************************/
/**
* nm_random_get_crypto_bytes:
* @p: the buffer to fill
* @n: the number of bytes to fill
*
* - can fail (in which case a negative number is returned
* and the output buffer is undefined).
* - will block trying to get high quality random numbers.
*/
int
nm_random_get_crypto_bytes(void *p, size_t n)
{
nm_auto_close int fd = -1;
ssize_t l;
int r;
if (n == 0)
return 0;
nm_assert(p);
again_getrandom:
l = _getrandom(p, n, 0);
if (l > 0) {
if ((size_t) l == n)
return 0;
p = (uint8_t *) p + l;
n -= l;
goto again_getrandom;
}
if (l != -ENOSYS) {
/* We got a failure, but getrandom seems to be working in principle. We
* won't get good numbers. Fail. */
return l;
}
/* getrandom() failed with ENOSYS. Fallback to reading /dev/urandom. */
r = _random_check_entropy(TRUE);
if (r < 0)
return r;
if (r == 0)
return nm_assert_unreachable_val(-EIO);
fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0)
return -errno;
return nm_utils_fd_read_loop_exact(fd, p, n, FALSE);
}
/*****************************************************************************/
guint64
nm_random_u64_range_full(guint64 begin, guint64 end, gboolean crypto_bytes)
{
gboolean bad_crypto_bytes = FALSE;
guint64 remainder;
guint64 maxvalue;
guint64 x;
guint64 m;
/* Returns a random #guint64 equally distributed in the range [@begin..@end-1].
*
* The function always set errno. It either sets it to zero or to EAGAIN
* (if crypto_bytes were requested but not obtained). In any case, the function
* will always return a random number in the requested range (worst case, it's
* not crypto_bytes despite being requested). Check errno if you care. */
if (begin >= end) {
/* systemd's random_u64_range(0) is an alias for nm_random_u64().
* Not for us. It's a caller error to request an element from an empty range. */
return nm_assert_unreachable_val(begin);
}
m = end - begin;
if (m == 1) {
x = 0;
goto out;
}
remainder = G_MAXUINT64 % m;
maxvalue = G_MAXUINT64 - remainder;
do
if (crypto_bytes) {
if (nm_random_get_crypto_bytes(&x, sizeof(x)) < 0) {
/* Cannot get good crypto numbers. We will try our best, but fail
* and set errno below. */
crypto_bytes = FALSE;
bad_crypto_bytes = TRUE;
continue;
}
} else
nm_random_get_bytes(&x, sizeof(x));
while (x >= maxvalue);
out:
errno = bad_crypto_bytes ? EAGAIN : 0;
return begin + (x % m);
}