/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "boot-secret.h"
#include "efi-efivars.h"
#include "efi-log.h"
#include "random-seed.h"
#include "sha256.h"
#include "util.h"

#define BOOT_SECRET_MIXIN_PATH u"\\loader\\boot-secret-mixin"

/* This maintains a per-system secret that is stored in an EFI variable that is only accessible during EFI
 * boot, and becomes inaccessible afterwards, once ExitBootServices() is called. The variable is
 * automatically initialized if missing. A secret derived by hashing from this EFI variable secret is then
 * passed to the OS, in an initrd file inaccessible to unprivileged userspace. To make things a bit more
 * robust while hashing two more pieces of information are mixed in: a random "mixin" that is stored in the
 * ESP and is supposed to ensure that the passed boot secrets are distinct for each disk used on the system;
 * moreover an OS identifier derived from the UKI's .osrel field (ideally IMAGE_ID=, but if not defined ID=
 * will do, with a final fallback to "linux"). Note that these two additions are not supposed to enhance the
 * cryptographic quality of the secret, they are just supposed to make things more robust on systems with
 * multiple disks and OSes.
 *
 * The boot secret passed to the OS can be used to protect resources during OS runtime, from earliest boot
 * phases on, as a fallback for the usual TPM based protections.
 *
 * Note that this secret comes with much weaker protection than TPM backed secrets: there's no physical
 * isolation, there are no cryptographic access policies, there's just the hope the firmware reasonably
 * correctly implements boot-time-only EFI variable mechanism. (But then again, this is what mok/shim's
 * security also relies on, and hence this all is not too bad?) */

static EFI_STATUS random_seed_find_table(struct linux_efi_random_seed **ret) {
        assert(ret);

        /* We use the Linux random seed EFI table as our source of randomness, since there's reason to
         * believe it is as good as it possibly would get. Note that we ourselves might be the ones
         * initializing it, based on EFI RNG APIs, the monotonic boot counter, a random seed file on disk and
         * the clock. */

        struct linux_efi_random_seed *seed_table =
                find_configuration_table(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE));
        if (!seed_table)
                return log_debug_status(EFI_NOT_FOUND, "No random seed available, not creating a boot secret.");
        if (seed_table->size < BOOT_SECRET_SIZE)
                return log_debug_status(EFI_NOT_FOUND, "Random seed is available, but too short.");

        *ret = seed_table;
        return EFI_SUCCESS;
}

static void random_seed_evolve(struct linux_efi_random_seed *seed_table) {
        static const char label[] = "systemd-stub random seed evolve label v1";

        assert(seed_table);

        /* Whenever we derived something from the Linux random seed EFI table we evolve the secret in it, so
         * that the seed is never reused. */

        struct sha256_ctx hash;
        CLEANUP_ERASE(hash);
        sha256_init_ctx(&hash);
        sha256_process_bytes(label, sizeof(label) - 1, &hash);
        sha256_process_bytes(&seed_table->size, sizeof(seed_table->size), &hash);
        sha256_process_bytes(seed_table->seed, seed_table->size, &hash);
        assert(seed_table->size >= SHA256_DIGEST_SIZE);
        sha256_finish_ctx(&hash, seed_table->seed);
}

static void random_seed_make_secret(
                struct linux_efi_random_seed *seed_table,
                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {

        static const char label[] = "systemd-stub random seed make secret label v1";

        assert(seed_table);
        assert(ret_secret);

        /* Derive a new secret from the Linux random seed EFI table data */

        struct sha256_ctx hash;
        CLEANUP_ERASE(hash);
        sha256_init_ctx(&hash);
        sha256_process_bytes(label, sizeof(label) - 1, &hash);
        sha256_process_bytes(&seed_table->size, sizeof(seed_table->size), &hash);
        sha256_process_bytes(seed_table->seed, seed_table->size, &hash);
        sha256_finish_ctx(&hash, ret_secret);

        random_seed_evolve(seed_table); /* ← ensure the same seed is not reused */
}

static EFI_STATUS read_efivar_secret(uint8_t ret_secret[static BOOT_SECRET_SIZE]) {
        EFI_STATUS err;

        assert(ret_secret);

        /* Reads the boot secret from the EFI variable, ensuring it's properly protected from the OS, as per
         * the attribute flags */

        _cleanup_free_ void* data = NULL;
        uint32_t attributes;
        size_t size = 0;
        err = efivar_get_raw_full(MAKE_GUID_PTR(LOADER), u"LoaderBootSecret", &attributes, &data, &size);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to read LoaderBootSecret EFI variable: %m");

        if (size != BOOT_SECRET_SIZE) {
                err = log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected size of BootSecret EFI variable, ignoring.");
                goto finish;
        }

        if ((attributes & (EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS|EFI_VARIABLE_RUNTIME_ACCESS)) !=
            (EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS)) {
                err = log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected attributes of BootSecret EFI variable, ignoring.");
                goto finish;
        }

        memcpy(ret_secret, data, size);
        err = EFI_SUCCESS;
finish:
        explicit_bzero_safe(data, size);
        return err;
}

static EFI_STATUS setup_efivar_secret(
                struct linux_efi_random_seed *seed_table,
                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {

        EFI_STATUS err;

        assert(seed_table);
        assert(ret_secret);

        /* Generates a new EFI variable secret, and stores it in an EFI variable. */

        uint8_t secret[BOOT_SECRET_SIZE];
        CLEANUP_ERASE(secret);
        random_seed_make_secret(seed_table, secret);

        /* Set the variable with the EFI_VARIABLE_RUNTIME_ACCESS flag off (!), so that it's invisible after
         * ExitBootServices()! */
        err = RT->SetVariable(
                        (char16_t*) u"LoaderBootSecret",
                        MAKE_GUID_PTR(LOADER),
                        EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS, /* ← No EFI_VARIABLE_RUNTIME_ACCESS here */
                        sizeof(secret),
                        secret);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to set boot secret EFI variable: %m");

        memcpy(ret_secret, secret, sizeof(secret));
        return EFI_SUCCESS;
}

static EFI_STATUS acquire_efivar_secret(
                struct linux_efi_random_seed *seed_table,
                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {

        EFI_STATUS err;

        assert(seed_table);
        assert(ret_secret);

        /* Try to read the boot secret EFI variable, but if it doesn't exist create a new one */

        err = read_efivar_secret(ret_secret);
        if (err != EFI_NOT_FOUND)
                return err;

        return setup_efivar_secret(seed_table, ret_secret);
}

static EFI_STATUS setup_secret_mixin(
                EFI_FILE *handle,
                struct linux_efi_random_seed *seed_table,
                uint8_t ret_mixin[static BOOT_SECRET_SIZE]) {

        EFI_STATUS err;

        assert(handle);
        assert(seed_table);
        assert(ret_mixin);

        /* This writes a new 'mixin' to the ESP, in case the ESP so far had none */

        uint8_t mixin[BOOT_SECRET_SIZE];
        random_seed_make_secret(seed_table, mixin);

        size_t wsize = sizeof(mixin);
        err = handle->Write(handle, &wsize, mixin);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to write secret mixin file: %m");
        if (wsize != sizeof(mixin))
                return log_debug_status(EFI_LOAD_ERROR, "Short write while writing secret mixin file: %m");

        err = handle->Flush(handle);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to flush secret mixin file: %m");

        memcpy(ret_mixin, mixin, sizeof(mixin));
        return EFI_SUCCESS;
}

static EFI_STATUS acquire_secret_mixin(
                EFI_FILE *root_dir,
                struct linux_efi_random_seed *seed_table,
                uint8_t ret_mixin[static BOOT_SECRET_SIZE]) {

        EFI_STATUS err;

        assert(seed_table);
        assert(ret_mixin);

        if (!root_dir)
                return EFI_NOT_FOUND;

        /* Acquires the mixin for the boot secret stored in the ESP. If it already exists we'll read it. If
         * it doesn't we'll initialize it */

        bool writable;
        _cleanup_file_close_ EFI_FILE *handle = NULL;
        err = root_dir->Open(
                        root_dir,
                        &handle,
                        (char16_t *) BOOT_SECRET_MIXIN_PATH,
                        EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,
                        /* Attributes= */ 0);
        if (err == EFI_WRITE_PROTECTED) {
                err = root_dir->Open(
                                root_dir,
                                &handle,
                                (char16_t *) BOOT_SECRET_MIXIN_PATH,
                                EFI_FILE_MODE_READ,
                                /* Attributes= */ 0);
                if (err != EFI_SUCCESS)
                        return log_debug_status(err, "Failed to read the boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);

                writable = false;
        } else if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to access the boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);
        else
                writable = true;

        _cleanup_free_ EFI_FILE_INFO *info = NULL;
        err = get_file_info(handle, &info, /* ret_size= */ NULL);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to get boot secret mixin file '%ls' info: %m", BOOT_SECRET_MIXIN_PATH);
        if (info->FileSize == 0 && writable) /* New file? Fill it. */
                return setup_secret_mixin(handle, seed_table, ret_mixin);

        /* If the mixin file is too small we won't overwrite it (in order to not destroy some potentially
         * load bearing key), but we won't use it either. */
        if (info->FileSize < BOOT_SECRET_SIZE)
                return log_debug_status(EFI_PROTOCOL_ERROR, "Boot secret mixin file '%ls' is too short %" PRIu64 " < %u", BOOT_SECRET_MIXIN_PATH, info->FileSize, BOOT_SECRET_SIZE);

        uint8_t mixin[BOOT_SECRET_SIZE];
        size_t rsize = sizeof(mixin);
        err = handle->Read(handle, &rsize, mixin);
        if (err != EFI_SUCCESS)
                return log_debug_status(err, "Failed to read boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);
        if (rsize != BOOT_SECRET_SIZE)
                return log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected size from Read(): %zu != %zu", rsize, sizeof(mixin));

        memcpy(ret_mixin, mixin, BOOT_SECRET_SIZE);
        return EFI_SUCCESS;
}

static char* pick_id(const char *_osrel, size_t osrel_size) {
        assert(_osrel || osrel_size == 0);

        /* Make a NUL terminated copy we can chop into pieces */
        _cleanup_free_ char *osrel = NULL;
        osrel = xmalloc(osrel_size + 1);
        if (osrel_size > 0)
                memcpy(osrel, _osrel, osrel_size);
        osrel[osrel_size] = 0;

        /* Find an OS ID. Preferably the IMAGE_ID. */
        _cleanup_free_ char *os_id = NULL;
        char *line, *key, *value;
        size_t pos = 0;
        while ((line = line_get_key_value(osrel, "=", &pos, &key, &value))) {
                if (streq8(key, "IMAGE_ID"))
                        return xstrdup8(value);

                if (streq8(key, "ID")) {
                        free(os_id);
                        os_id = xstrdup8(value);
                }
        }

        /* If the IMAGE_ID= wasn't set, use the OS ID=. If that one isn't set either fall back to "linux". */
        return TAKE_PTR(os_id) ?: xstrdup8("linux");
}

static void derive_secret(
                uint8_t efivar_secret[static BOOT_SECRET_SIZE],
                uint8_t secret_mixin[static BOOT_SECRET_SIZE],
                const char *id,
                uint8_t ret[static BOOT_SECRET_SIZE]) {

        static const char hash_label[] = "systemd-stub derive secret label v1";

        assert(efivar_secret);
        assert(secret_mixin);
        assert(id);
        assert(ret);

        /* Now combine the EFI variable secret, the mixin from the ESP and the OS id to generate the secret
         * to pass to the OS */

        struct sha256_ctx hash;
        CLEANUP_ERASE(hash);
        sha256_init_ctx(&hash);
        sha256_process_bytes(hash_label, sizeof(hash_label) - 1, &hash);
        sha256_process_bytes(efivar_secret, BOOT_SECRET_SIZE, &hash);
        sha256_process_bytes(secret_mixin, BOOT_SECRET_SIZE, &hash);

        /* Include an OS id in the hash, so that every OS gets a different derived secret */
        size_t size = strlen8(id);
        sha256_process_bytes(&size, sizeof(size), &hash);
        sha256_process_bytes(id, size, &hash);

        assert_cc(SHA256_DIGEST_SIZE == BOOT_SECRET_SIZE);
        sha256_finish_ctx(&hash, ret);
}

EFI_STATUS prepare_boot_secret(
                EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
                const PeSectionVector *osrel_section,
                uint8_t ret[static BOOT_SECRET_SIZE]) {

        EFI_STATUS err;

        assert(loaded_image);
        assert(ret);

        /* Prepares the boot secret to pass to the OS */

        if (!loaded_image->DeviceHandle)
                return EFI_SUCCESS;

        _cleanup_file_close_ EFI_FILE *root = NULL;
        err = open_volume(loaded_image->DeviceHandle, &root);
        if (err != EFI_SUCCESS)
                return err;

        /* We need the Linux random seed EFI table, so that we can initialize the EFI variable secret and
         * generate the secret mixin. */
        struct linux_efi_random_seed *seed_table = NULL;
        err = random_seed_find_table(&seed_table);
        if (err != EFI_SUCCESS)
                return err;

        uint8_t efivar_secret[BOOT_SECRET_SIZE];
        CLEANUP_ERASE(efivar_secret);
        err = acquire_efivar_secret(seed_table, efivar_secret);
        if (err != EFI_SUCCESS)
                return err;

        uint8_t secret_mixin[BOOT_SECRET_SIZE];
        err = acquire_secret_mixin(root, seed_table, secret_mixin);
        if (err != EFI_SUCCESS)
                return err;

        const char *osrel = NULL;
        size_t osrel_size = 0;
        if (PE_SECTION_VECTOR_IS_SET(osrel_section)) {
                osrel = (const char*) loaded_image->ImageBase + osrel_section->memory_offset;
                osrel_size = osrel_section->memory_size;
        }
        _cleanup_free_ char *id = pick_id(osrel, osrel_size);

        derive_secret(efivar_secret, secret_mixin, id, ret);
        return EFI_SUCCESS;
}
