diff options
Diffstat (limited to 'random-seed.c')
-rw-r--r-- | random-seed.c | 561 |
1 files changed, 561 insertions, 0 deletions
diff --git a/random-seed.c b/random-seed.c new file mode 100644 index 0000000..7a0527a --- /dev/null +++ b/random-seed.c @@ -0,0 +1,561 @@ +/* Copyright 2018 Alex Xu (aka Hello71, alxu) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* we don't use enough CPU time, better be safe than sorry */ +#ifdef NDEBUG +#warning "NDEBUG is always disabled for security" +#undef NDEBUG +#endif + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/statfs.h> +#include <sys/syscall.h> +#include <unistd.h> + +#ifndef WITHOUT_SYSTEMD +#include <systemd/sd-id128.h> +#endif + +#include "musl-libgen-c.h" +#include "readall.h" +#include "util.h" + +// musl forbids include/linux +#define RNDADDENTROPY _IOW( 'R', 0x03, int [2] ) + +/* 3 hours */ +#define DAEMONIZE_SLEEP_TIME (3*60*60) + +struct rand_pool_info_ { + int entropy_count; + int buf_size; + uint32_t buf[RAND_POOL_SIZE / sizeof(uint32_t)]; +}; + +static const char MAGIC[] = "RANDOM SEED FILE VERSION 1\n"; + +static sig_atomic_t sigint = 0; +static sig_atomic_t sigterm = 0; + +static inline void usage() { + puts("usage: random-seed MODE FILE"); + puts("see random-seed(8) for more information."); +} + +static size_t get_machine_id(unsigned char **machine_id) { +#ifdef HAVE_SYSTEMD + sd_id128_t raw_machine_id; + int r = sd_id128_get_machine(&raw_machine_id); + if (r) { + fprintf(stderr, "error getting machine id: %s\n", strerror(-r)); + *machine_id = NULL; + return 0; + } + *machine_id = malloc(33); + /* convert to string to be compatible with non-hex machine IDs */ + sd_id128_to_string(raw_machine_id, (char *)*machine_id); + /* match /etc/machine-id format */ + (*machine_id)[32] = '\n'; + + return 33; +#else + char *machine_id; + ssize_t machine_id_len; + + const char *etc_machine_id = SYSCONFDIR "/machine-id"; + const char *var_lib_dbus_machine_id = LOCALSTATEDIR "/lib/dbus/machine-id"; + FILE *machine_id_file = fopen(etc_machine_id, "r"); + if (!machine_id_file) { + if (errno != ENOENT) + fprintf(stderr, "error opening %s: %s, trying %s\n", + etc_machine_id, strerror(errno), var_lib_dbus_machine_id); + machine_id_file = fopen(var_lib_dbus_machine_id, "r"); + if (!machine_id_file) { + fprintf(stderr, "couldn't open any machine-id file, last error: " + "%s; tried: %s, %s\n", strerror(errno), etc_machine_id, + var_lib_dbus_machine_id); + *machine_id = NULL; + return 0; + } + } + + if (readall(machine_id_file, machine_id, &machine_id_len) != READALL_OK) { + fputs("error reading machine id file\n", stderr); + *machine_id = NULL; + return 0; + } + + return machine_id_len; +#endif +} + +static bool get_machine_id_hash(const unsigned char salt[static SALT_LEN], unsigned char machine_id_digest[static HASH_LEN]) { + static unsigned char *c_machine_id; + static size_t c_machine_id_len; + if (!c_machine_id) + c_machine_id_len = get_machine_id(&c_machine_id); + if (!c_machine_id_len) { + free(c_machine_id); + c_machine_id = NULL; + return false; + } + unsigned char c_machine_id_digest[HASH_LEN]; + hash(salt, c_machine_id_digest, c_machine_id, c_machine_id_len); + memcpy(machine_id_digest, c_machine_id_digest, HASH_LEN); + return true; +} + +static inline bool get_fs_id_hash(const unsigned char salt[static SALT_LEN], unsigned char fsid_digest[static HASH_LEN], int seed_fd) { + struct statfs statfs_buf; + if (fstatfs(seed_fd, &statfs_buf) == -1) { + fprintf(stderr, "error statfs seed file: %s, " + "disabling entropy credit\n", strerror(errno)); + return false; + } + hash(salt, fsid_digest, &statfs_buf.f_fsid, sizeof(statfs_buf.f_fsid)); + return true; +} + +static bool run_seed_file_cmd(const char *cmd, const unsigned char *salt, FILE *seed_file) { + char *arg; + if (streq(cmd, "machine-id")) { + arg = strtok(NULL, " \t"); + if (!arg) { + fputs("error parsing seed file: expected argument " + "to 'machine-id'\n", stderr); + return false; + } + unsigned char machine_id_hash[HASH_LEN]; + if (!get_machine_id_hash(salt, machine_id_hash)) { + fputs("error getting machine id hash, disabling entropy credit\n", + stderr); + return false; + } + + if (!hash_match(machine_id_hash, arg)) { + fputs("machine-id does not match, disabling entropy credit\n", + stderr); + return false; + } + return true; + } + + if (streq(cmd, "fs-id")) { + arg = strtok(NULL, " \t"); + if (!arg) { + fputs("error parsing seed file: expected argument to 'fs-id'\n", stderr); + return false; + } + unsigned char fs_id_hash[HASH_LEN]; + if (!get_fs_id_hash(salt, fs_id_hash, fileno(seed_file))) { + fputs("error getting fs id hash, disabling entropy credit\n", stderr); + return false; + } + + if (!hash_match(fs_id_hash, arg)) { + fputs("fs id does not match, disabling entropy credit\n", stderr); + return false; + } + return true; + } + + fprintf(stderr, "error parsing seed file: unsupported command: %s\n", cmd); + return false; +} + +static bool load(FILE *seed_file) { + bool credit_entropy = true; + + struct rand_pool_info_ rpi = { + .entropy_count = RAND_POOL_SIZE * 8, + .buf_size = RAND_POOL_SIZE, + .buf = { 0 } + }; + + uint64_t linenum = 0; + char *line = NULL; + size_t n = 0; + bool done = false; + + if (fread(&rpi.buf, 1, RAND_POOL_SIZE, seed_file) != RAND_POOL_SIZE) { + if (feof(seed_file)) { + fputs("premature EOF on seed file\n", stderr); + } else if (ferror(seed_file)) { + fputs("error reading from seed file\n", stderr); + } else { + fputs("short read from seed file\n", stderr); + } + return false; + } + + unsigned char *salt = (unsigned char *)rpi.buf; + + while (1) { + errno = 0; + ssize_t line_length = getline(&line, &n, seed_file); + if (line_length == -1) { + if (errno) { + perror("error reading from seed file"); + credit_entropy = false; + } + break; + } + + linenum++; + + if (linenum == 1) { + if (streq(line, MAGIC)) { + continue; + } else { + fputs("error parsing seed file: invalid magic\n", stderr); + credit_entropy = false; + break; + } + } + + char *nul = memchr(line, '\0', line_length); + if (nul) { + fprintf(stderr, "error parsing seed file: encountered NUL byte in commands line %zu char %zu\n", linenum, nul - line + 1); + credit_entropy = false; + break; + } + + char *cmd = strtok(line, " \t\n"); + if (!cmd) + continue; + +#ifdef DEBUG + fprintf(stderr, "executing command: %s\n", cmd); +#endif + + if (streq(cmd, "done")) { + done = true; + continue; + } + + if (!run_seed_file_cmd(cmd, salt, seed_file)) { + credit_entropy = false; + break; + } + } + + if (credit_entropy && !done) { + fputs("missing done command, random seed file probably truncated. disabling entropy credit\n", stderr); + credit_entropy = false; + } + + int urandom_fd = open("/dev/urandom", O_RDWR, 0); + if (urandom_fd == -1) { + perror("error opening /dev/urandom"); + exit(5); + } + + if (credit_entropy) { + if (ioctl(urandom_fd, RNDADDENTROPY, &rpi) == -1) { + perror("ioctl(RNDADDENTROPY)"); + if (errno == EPERM) { + fputs("Continuing without crediting entropy.\n", stderr); + } + credit_entropy = false; + } + } + + if (!credit_entropy) { + if (write(urandom_fd, &rpi.buf, RAND_POOL_SIZE) != RAND_POOL_SIZE) { + fputs("error writing entropy to /dev/urandom\n", stderr); + exit(5); + } + } + + return credit_entropy; +} + +static bool get_rand_pool(unsigned char *buf) { + size_t rand_bits_gotten = 0; + while (rand_bits_gotten < RAND_POOL_SIZE) { + long this_rand_bits_gotten = random_get(buf + rand_bits_gotten, RAND_POOL_SIZE - rand_bits_gotten, 0); + if (this_rand_bits_gotten < 0) { + switch (errno) { + case EAGAIN: continue; + case EINTR: return false; + default: + perror("getrandom"); + exit(1); + } + } else { + rand_bits_gotten += (size_t)this_rand_bits_gotten; + } + } + return true; +} + +/** + * Save entropy to disk. + * + * \param seed_path the seed file path + * \param random_buf the random buffer. if NULL, get our own entropy. + * \return true means saved successfully, false means received EINTR + */ +static bool save(const char *seed_path, unsigned char *random_buf) { + assert(seed_path); + + bool rv = false; + char *seed_path_tmp = NULL; + char *seed_name_new = NULL; + + unsigned char my_random_buf[RAND_POOL_SIZE]; + if (!random_buf) { + if (!get_rand_pool(my_random_buf)) + return false; + random_buf = my_random_buf; + } + + unsigned char machine_id_digest[HASH_LEN]; + if (!get_machine_id_hash(random_buf, machine_id_digest)) { + fputs("cannot obtain machine id, aborting save\n", stderr); + return false; + } + char machine_id_hash[HASH_LEN*2+1]; + memset(machine_id_hash, 0, HASH_LEN*2); + mem2hex(machine_id_hash, machine_id_digest, HASH_LEN); + machine_id_hash[sizeof(machine_id_hash)-1] = '\0'; + + int seed_dir_fd = -1; + int seed_fd = -1; + FILE *seed_file = NULL; + + seed_path_tmp = strdup(seed_path); + + // TODO: rewrite when AT_REPLACE gets added... eventually... + seed_dir_fd = open(mydirname(seed_path_tmp), O_RDONLY | O_DIRECTORY); + if (seed_dir_fd == -1) { + perror("error opening seed directory"); + goto out; + } + + unsigned char fs_id_digest[HASH_LEN]; + if (!get_fs_id_hash(random_buf, fs_id_digest, seed_dir_fd)) { + fputs("cannot obtain machine id, aborting save\n", stderr); + free(seed_path_tmp); + return false; + } + char fs_id_hash[HASH_LEN*2+1]; + mem2hex(fs_id_hash, fs_id_digest, HASH_LEN); + fs_id_hash[sizeof(fs_id_hash)-1] = '\0'; + + strcpy(seed_path_tmp, seed_path); + char *seed_name = mybasename(seed_path_tmp); + size_t seed_name_new_len = strlen(seed_name) + 6; + seed_name_new = malloc(seed_name_new_len); + if (!seed_name_new) { + fputs("out of memory\n", stderr); + free(seed_path_tmp); + return false; + } + assert(seed_name_new_len < INT_MAX); + if ((size_t)snprintf(seed_name_new, seed_name_new_len, ".%s.new", seed_name) >= seed_name_new_len) + abort(); + + seed_fd = openat(seed_dir_fd, seed_name_new, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (seed_fd == -1) { + perror("error opening new seed file"); + goto err; + } + seed_file = fdopen(seed_fd, "w"); + if (!seed_file) { + perror("error converting seed file fd to stream"); + goto err; + } + + if (fwrite(random_buf, 1, RAND_POOL_SIZE, seed_file) != RAND_POOL_SIZE + || fputs(MAGIC, seed_file) == EOF + || fputs("machine-id ", seed_file) == EOF + || fputs(machine_id_hash, seed_file) == EOF + || fputs("\nfs-id ", seed_file) == EOF + || fputs(fs_id_hash, seed_file) == EOF + || fputs("\ndone\n", seed_file) == EOF) { + fputs("error writing new seed file\n", stderr); + goto err; + } + + if (fflush(seed_file) == EOF) { + perror("error flushing new seed file"); + goto err; + } + if (fsync(seed_fd) == -1) { + perror("error syncing new seed file"); + goto err; + } + if (renameat(seed_dir_fd, seed_name_new, seed_dir_fd, seed_name) == -1) { + perror("error installing new seed file"); + goto err; + } + if (fclose(seed_file) == EOF) { + perror("error closing new seed file"); + goto err; + } + if (fsync(seed_dir_fd) == -1) { + perror("error syncing seed directory"); + goto out; + } + + rv = true; + goto out; + +err: + if (seed_file) { + fclose(seed_file); + if (unlinkat(seed_dir_fd, seed_name_new, 0) == -1) { + perror("error removing temporary seed file"); + rv = false; + } + } + +out: + if (seed_dir_fd != -1) { + if (close(seed_dir_fd) == -1) { + perror("error closing seed directory"); + rv = false; + } + } + + free(seed_path_tmp); + free(seed_name_new); + + return rv; +} + +static void sighandler(int signum) { + switch (signum) { + case SIGHUP: + // do nothing; we just needed to interrupt sleep + break; + case SIGINT: + sigint = 1; + break; + case SIGTERM: + sigterm = 1; + break; + default: + abort(); + } +} + +int main(int argc, char *argv[]) { + const char *seed_path = argv[2]; + FILE *seed_file; + unsigned char random_buf[RAND_POOL_SIZE]; + int exit_status = 0; + + if (argc == 2 && (streq(argv[1], "-h") || streq(argv[1], "--help"))) { + usage(); + exit(0); + } + + if (argc != 3) { + fprintf(stderr, "invalid argument count, expected 2\n"); + usage(); + exit(2); + } + + umask(0077); + + if (streq(argv[1], "load")) { + bool refresh_seed = true; + + if (streq(seed_path, "-")) { + fputs("warning: cannot refresh stdin seed\n", stderr); + seed_file = stdin; + refresh_seed = false; + } else { + seed_file = fopen(seed_path, "r"); + if (!seed_file) { + perror("error opening seed file"); + exit(3); + } + } + if (!load(seed_file)) + exit_status = 1; + if (refresh_seed) { + if (random_get(random_buf, sizeof(random_buf), GRND_NONBLOCK) == -1) { + if (errno != EAGAIN) { + perror("getrandom"); + exit(1); + } + daemon(0, 1); + close(0); + close(1); + save(seed_path, NULL); + } else { + save(seed_path, random_buf); + } + } + exit(exit_status); + } else if (streq(argv[1], "save")) { + exit(!save(seed_path, NULL)); + } else if (streq(argv[1], "daemonize")) { + if (streq(seed_path, "-")) { + fputs("error: seed_path cannot be - for daemonize\n", stderr); + exit(2); + } + seed_file = fopen(seed_path, "r"); + if (!seed_file) { + perror("error opening seed file"); + exit(3); + } + load(seed_file); + + struct sigaction sa; + sa.sa_handler = sighandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGHUP, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + daemon(seed_path[0] != '/', 1); + close(0); + close(1); + // don't close stderr because we log there + while (true) { + if (sigint) + exit(exit_status); + if (!save(seed_path, NULL)) { + exit_status = 1; + fputs("an error occurred while saving, trying again later\n", stderr); + } + if (sigterm) + exit(exit_status); + sleep(DAEMONIZE_SLEEP_TIME); + } + } else if (streq(argv[1], "daemonise")) { + fputs("invalid mode (did you mean `daemonize'?)\n", stderr); + exit(2); + } else { + fputs("invalid mode, expected load, save, or daemonize\n", stderr); + exit(2); + } +} |