// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2015, Trustees of Indiana University
 *
 * Author: Jeremy Filizetti <jfilizet@iu.edu>
 */

#include <limits.h>
#include <string.h>
#include <openssl/dh.h>
#include <openssl/err.h>

#include "sk_utils.h"
#include "lgss_utils.h"

/**
 * Create the initial shared key credentials
 */
static int lgss_sk_prepare_cred(struct lgss_cred *cred)
{
	uint32_t flags = cred->lc_root_flags;

	switch (cred->lc_svc_type) {
	case 'n':
		flags |= LGSS_SVC_NULL;
		break;
	case 'a':
		flags |= LGSS_SVC_AUTH;
		break;
	case 'i':
		flags |= LGSS_SVC_INTG;
		break;
	case 'p':
		flags |= LGSS_SVC_PRIV;
		break;
	default:
		break;
	}

	cred->lc_mech_cred = sk_create_cred(cred->lc_tgt_uuid, NULL, flags);
	if (cred->lc_mech_cred == NULL) {
		printerr(0, "sk: cannot create credential: %s\n",
			 cred->lc_tgt_uuid);
		return -ENOKEY;
	}

	return 0;
}

/* Free all the sk_cred resources */
static void lgss_sk_release_cred(struct lgss_cred *cred)
{
	struct sk_cred *skc = cred->lc_mech_cred;

	sk_free_cred(skc);
	cred->lc_mech_cred = NULL;
	free(cred->lc_mech_token.value);
}

/**
 * Session key parameter generation is deferred until here because if privacy
 * mode is enabled the session key parameter generation can take a while
 * depending on the key size used and prepare is called before returning
 * from the request_key upcall by lgss_keyring
 */
static int lgss_sk_using_cred(struct lgss_cred *cred)
{
	struct sk_cred *skc = cred->lc_mech_cred;
	gss_buffer_desc bufs[SK_INIT_BUFFERS];
	uint32_t version;
	uint32_t flags;
	int rc;

	rc = sk_gen_params(skc, 0);
	if (rc)
		return rc;

	/* HMAC is generated in this order */
	version = htobe32(SK_MSG_VERSION);
	bufs[SK_INIT_VERSION].value = &version;
	bufs[SK_INIT_VERSION].length = sizeof(version);
	bufs[SK_INIT_RANDOM].value = &skc->sc_kctx.skc_host_random;
	bufs[SK_INIT_RANDOM].length = sizeof(skc->sc_kctx.skc_host_random);
	bufs[SK_INIT_PUB_KEY] = skc->sc_pub_key;
	bufs[SK_INIT_P] = skc->sc_p;
	bufs[SK_INIT_TARGET] = skc->sc_tgt;
	bufs[SK_INIT_NODEMAP] = skc->sc_nodemap_hash;
	flags = htobe32(skc->sc_flags);
	bufs[SK_INIT_FLAGS].value = &flags;
	bufs[SK_INIT_FLAGS].length = sizeof(flags);

	/* sign all the bufs except HMAC */
	rc = sk_sign_bufs(&skc->sc_kctx.skc_shared_key, bufs,
			  SK_INIT_BUFFERS - 1, EVP_sha256(),
			  &skc->sc_hmac);
	if (rc)
		return rc;

	bufs[SK_INIT_HMAC] = skc->sc_hmac;
	rc = sk_encode_netstring(bufs, SK_INIT_BUFFERS, &cred->lc_mech_token);
	if (rc)
		return rc;

	printerr(2, "Created netstring of %zd bytes\n",
		 cred->lc_mech_token.length);

	return 0;
}

static int lgss_sk_validate_cred(struct lgss_cred *cred, gss_buffer_desc *token,
				 gss_buffer_desc *ctx_token)
{
	struct sk_cred *skc = cred->lc_mech_cred;
	gss_buffer_desc bufs[SK_RESP_BUFFERS];
	uint32_t version;
	int i;
	uint32_t rc;

	/* Decode responder buffers and validate */
	i = sk_decode_netstring(bufs, SK_RESP_BUFFERS, token);
	if (i != SK_RESP_BUFFERS) {
		printerr(0, "Invalid token received\n");
		return -EINVAL;
	}

	rc = sk_verify_hmac(skc, bufs, SK_RESP_BUFFERS - 1, EVP_sha256(),
			    &bufs[SK_RESP_HMAC]);
	if (rc != GSS_S_COMPLETE) {
		printerr(0, "Invalid HMAC receieved: 0x%x\n", rc);
		return -EINVAL;
	}

	if (bufs[SK_RESP_VERSION].length != sizeof(version)) {
		printerr(0, "Invalid version received (wrong size)\n");
		return -EINVAL;
	}
	memcpy(&version, bufs[SK_RESP_VERSION].value, sizeof(version));
	version = be32toh(version);
	if (version != SK_MSG_VERSION) {
		printerr(0, "Invalid version received: %d\n", version);
		return -EINVAL;
	}

	/* In the rare event that both the random values are equal the
	 * client has the responsability to retry the connection attempt
	 * otherwise we would leak information about the plain text by
	 * reuusing IVs as both peer and host use the same values other
	 * than the nonce. */
	memcpy(&skc->sc_kctx.skc_peer_random, bufs[SK_RESP_RANDOM].value,
	       sizeof(skc->sc_kctx.skc_peer_random));
	if (skc->sc_kctx.skc_host_random == skc->sc_kctx.skc_peer_random) {
		printerr(0, "Host and peer randoms are equal, must retry to "
			 "ensure unique value for nonce\n");
		return -EAGAIN;
	}

	rc = sk_compute_dh_key(skc, &bufs[SK_RESP_PUB_KEY]);
	if (rc == GSS_S_BAD_QOP) {
		/* Defective token for short key means we need to retry
		 * because there is a chance that the parameters generated
		 * resulted in a key that is 1 byte short */
		printerr(0, "Short key computed, must retry\n");
		if (skc->sc_dh_shared_key.value) {
			/* erase secret key before freeing memory */
			memset(skc->sc_dh_shared_key.value, 0,
			       skc->sc_dh_shared_key.length);
			free(skc->sc_dh_shared_key.value);
			skc->sc_dh_shared_key.value = NULL;
		}
		skc->sc_dh_shared_key.length = 0;
		return -EAGAIN;
	} else if (rc != GSS_S_COMPLETE) {
		printerr(0, "Failed to compute session key: 0x%x\n", rc);
		return -EINVAL;
	}

	rc = sk_session_kdf(skc, cred->lc_self_nid, &cred->lc_mech_token,
			    token);
	if (rc) {
		printerr(0, "Failed to calculate derived key\n");
		return -EINVAL;
	}

	rc = sk_compute_keys(skc);
	if (rc) {
		printerr(0, "Failed to compute HMAC and session key\n");
		return -EINVAL;
	}

	if (sk_serialize_kctx(skc, ctx_token)) {
		printerr(0, "Failed to serialize context for kernel\n");
		return -EINVAL;
	}

	return 0;
}

struct lgss_mech_type lgss_mech_sk = {
	.lmt_name		= "sk",
	.lmt_mech_n		= LGSS_MECH_SK,
	.lmt_prepare_cred	= lgss_sk_prepare_cred,
	.lmt_release_cred	= lgss_sk_release_cred,
	.lmt_using_cred		= lgss_sk_using_cred,
	.lmt_validate_cred	= lgss_sk_validate_cred,
};
