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

#include <libcryptsetup.h>

#include "alloc-util.h"
#include "cryptsetup-token-util.h"
#include "hexdecoct.h"
#include "json-util.h"
#include "luks2-fido2.h"
#include "memory-util.h"
#include "strv.h"

int acquire_luks2_key(
                struct crypt_device *cd,
                const char *json,
                const char *device,
                const char *pin,
                char **ret_keyslot_passphrase,
                size_t *ret_keyslot_passphrase_size) {

        int r;
        Fido2EnrollFlags required;
        size_t cid_size, salt_size, decrypted_key_size;
        _cleanup_free_ void *cid = NULL, *salt = NULL;
        _cleanup_free_ char *rp_id = NULL;
        _cleanup_(erase_and_freep) void *decrypted_key = NULL;
        _cleanup_(erase_and_freep) char *base64_encoded = NULL;
        _cleanup_strv_free_erase_ char **pins = NULL;
        ssize_t base64_encoded_size;

        assert(ret_keyslot_passphrase);
        assert(ret_keyslot_passphrase_size);

        r = parse_luks2_fido2_data(cd, json, &rp_id, &salt, &salt_size, &cid, &cid_size, &required);
        if (r < 0)
                return r;

        if (pin) {
                pins = strv_new(pin);
                if (!pins)
                        return crypt_log_oom(cd);
        }

        /* configured to use pin but none was provided */
        if ((required & FIDO2ENROLL_PIN) && strv_isempty(pins))
                return -ENOANO;

        r = fido2_use_hmac_hash(
                        device,
                        rp_id ?: "io.systemd.cryptsetup",
                        salt, salt_size,
                        cid, cid_size,
                        pins,
                        required,
                        &decrypted_key,
                        &decrypted_key_size);
        if (r == -ENOLCK) /* libcryptsetup returns -ENOANO also on wrong PIN */
                r = -ENOANO;
        if (r < 0)
                return r;

        /* Before using this key as passphrase we base64 encode it, for compat with homed */
        base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
        if (base64_encoded_size < 0)
                return crypt_log_error_errno(cd, (int) base64_encoded_size, "Failed to base64 encode key: %m");

        *ret_keyslot_passphrase = TAKE_PTR(base64_encoded);
        *ret_keyslot_passphrase_size = base64_encoded_size;

        return 0;
}

/* this function expects valid "systemd-fido2" in json */
int parse_luks2_fido2_data(
                struct crypt_device *cd,
                const char *json,
                char **ret_rp_id,
                void **ret_salt,
                size_t *ret_salt_size,
                void **ret_cid,
                size_t *ret_cid_size,
                Fido2EnrollFlags *ret_required) {

        _cleanup_free_ void *cid = NULL, *salt = NULL;
        size_t cid_size = 0, salt_size = 0;
        _cleanup_free_ char *rp = NULL;
        int r;
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
        sd_json_variant *w;
        Fido2EnrollFlags required = 0;

        assert(json);
        assert(ret_rp_id);
        assert(ret_salt);
        assert(ret_salt_size);
        assert(ret_cid);
        assert(ret_cid_size);
        assert(ret_required);

        r = sd_json_parse(json, 0, &v, NULL, NULL);
        if (r < 0)
                return crypt_log_error_errno(cd, r, "Failed to parse JSON token data: %m");

        w = sd_json_variant_by_key(v, "fido2-credential");
        if (!w)
                return -EINVAL;

        r = sd_json_variant_unbase64(w, &cid, &cid_size);
        if (r < 0)
                return crypt_log_error_errno(cd, r, "Failed to parse 'fido2-credentials' field: %m");

        w = sd_json_variant_by_key(v, "fido2-salt");
        if (!w)
                return -EINVAL;

        r = sd_json_variant_unbase64(w, &salt, &salt_size);
        if (r < 0)
                return crypt_log_error_errno(cd, r, "Failed to parse 'fido2-salt' field: %m");

        w = sd_json_variant_by_key(v, "fido2-rp");
        if (w) {
                /* The "rp" field is optional. */
                rp = strdup(sd_json_variant_string(w));
                if (!rp) {
                        crypt_log_error(cd, "Not enough memory.");
                        return -ENOMEM;
                }
        }

        w = sd_json_variant_by_key(v, "fido2-clientPin-required");
        if (w)
                /* The "fido2-clientPin-required" field is optional. */
                SET_FLAG(required, FIDO2ENROLL_PIN, sd_json_variant_boolean(w));
        else
                required |= FIDO2ENROLL_PIN_IF_NEEDED; /* compat with 248, where the field was unset */

        w = sd_json_variant_by_key(v, "fido2-up-required");
        if (w)
                /* The "fido2-up-required" field is optional. */
                SET_FLAG(required, FIDO2ENROLL_UP, sd_json_variant_boolean(w));
        else
                required |= FIDO2ENROLL_UP_IF_NEEDED; /* compat with 248 */

        w = sd_json_variant_by_key(v, "fido2-uv-required");
        if (w)
                /* The "fido2-uv-required" field is optional. */
                SET_FLAG(required, FIDO2ENROLL_UV, sd_json_variant_boolean(w));
        else
                required |= FIDO2ENROLL_UV_OMIT; /* compat with 248 */

        *ret_rp_id = TAKE_PTR(rp);
        *ret_cid = TAKE_PTR(cid);
        *ret_cid_size = cid_size;
        *ret_salt = TAKE_PTR(salt);
        *ret_salt_size = salt_size;
        *ret_required = required;

        return 0;
}
