// SPDX-License-Identifier: GPL-2.0-or-later
/* Application-specific bits for GSSAPI-based RxRPC security
 *
 * Copyright (C) 2025 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/net.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/key-type.h>
#include "ar-internal.h"
#include "rxgk_common.h"

/*
 * Decode a default-style YFS ticket in a response and turn it into an
 * rxrpc-type key.
 *
 * struct rxgk_key {
 *	afs_uint32	enctype;
 *	opaque		key<>;
 * };
 *
 * struct RXGK_AuthName {
 *	afs_int32	kind;
 *	opaque		data<AUTHDATAMAX>;
 *	opaque		display<AUTHPRINTABLEMAX>;
 * };
 *
 * struct RXGK_Token {
 *	rxgk_key		K0;
 *	RXGK_Level		level;
 *	rxgkTime		starttime;
 *	afs_int32		lifetime;
 *	afs_int32		bytelife;
 *	rxgkTime		expirationtime;
 *	struct RXGK_AuthName	identities<>;
 * };
 */
int rxgk_yfs_decode_ticket(struct rxrpc_connection *conn, struct sk_buff *skb,
			   void *buffer, unsigned int ticket_len,
			   struct key **_key)
{
	struct rxrpc_key_token *token;
	const struct cred *cred = current_cred(); // TODO - use socket creds
	struct key *key;
	size_t pre_ticket_len, payload_len;
	unsigned int klen, enctype;
	void *payload, *ticket;
	__be32 *t, *p, *q, *tmp;
	int ret;

	_enter("");

	if (ticket_len < 10 * sizeof(__be32))
		return rxrpc_abort_conn(conn, skb, RXGK_INCONSISTENCY, -EPROTO,
					rxgk_abort_resp_short_yfs_tkt);

	/* Get the session key length */
	tmp = buffer;
	enctype = ntohl(tmp[0]);
	klen = ntohl(tmp[1]);

	if (klen > ticket_len - 10 * sizeof(__be32))
		return rxrpc_abort_conn(conn, skb, RXGK_INCONSISTENCY, -EPROTO,
					rxgk_abort_resp_short_yfs_key);

	pre_ticket_len = ((5 + 14) * sizeof(__be32) +
			  xdr_round_up(klen) +
			  sizeof(__be32));
	payload_len = pre_ticket_len + xdr_round_up(ticket_len);

	payload = kzalloc(payload_len, GFP_NOFS);
	if (!payload)
		return -ENOMEM;

	/* We need to fill out the XDR form for a key payload that we can pass
	 * to add_key().  Start by copying in the ticket so that we can parse
	 * it.
	 */
	ticket = payload + pre_ticket_len;
	memcpy(ticket, buffer, ticket_len);

	/* Fill out the form header. */
	p = payload;
	p[0] = htonl(0); /* Flags */
	p[1] = htonl(1); /* len(cellname) */
	p[2] = htonl(0x20000000); /* Cellname " " */
	p[3] = htonl(1); /* #tokens */
	p[4] = htonl(15 * sizeof(__be32) + xdr_round_up(klen) +
		     xdr_round_up(ticket_len)); /* Token len */

	/* Now fill in the body.  Most of this we can just scrape directly from
	 * the ticket.
	 */
	t = ticket + sizeof(__be32) * 2 + xdr_round_up(klen);
	q = payload + 5 * sizeof(__be32);
	q[0]  = htonl(RXRPC_SECURITY_YFS_RXGK);
	q[1]  = t[1];		/* begintime - msw */
	q[2]  = t[2];		/* - lsw */
	q[3]  = t[5];		/* endtime - msw */
	q[4]  = t[6];		/* - lsw */
	q[5]  = 0;		/* level - msw */
	q[6]  = t[0];		/* - lsw */
	q[7]  = 0;		/* lifetime - msw */
	q[8]  = t[3];		/* - lsw */
	q[9]  = 0;		/* bytelife - msw */
	q[10] = t[4];		/* - lsw */
	q[11] = 0;		/* enctype - msw */
	q[12] = htonl(enctype);	/* - lsw */
	q[13] = htonl(klen);	/* Key length */

	q += 14;

	memcpy(q, ticket + sizeof(__be32) * 2, klen);
	q += xdr_round_up(klen) / 4;
	q[0] = htonl(ticket_len);
	q++;
	if (WARN_ON((unsigned long)q != (unsigned long)ticket)) {
		ret = -EIO;
		goto error;
	}

	/* Ticket appended above. */
	q += xdr_round_up(ticket_len) / 4;
	if (WARN_ON((unsigned long)q - (unsigned long)payload != payload_len)) {
		ret = -EIO;
		goto error;
	}

	/* Now turn that into a key. */
	key = key_alloc(&key_type_rxrpc, "x",
			GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, cred, // TODO: Use socket owner
			KEY_USR_VIEW,
			KEY_ALLOC_NOT_IN_QUOTA, NULL);
	if (IS_ERR(key)) {
		_leave(" = -ENOMEM [alloc %ld]", PTR_ERR(key));
		ret = PTR_ERR(key);
		goto error;
	}

	_debug("key %d", key_serial(key));

	ret = key_instantiate_and_link(key, payload, payload_len, NULL, NULL);
	if (ret < 0)
		goto error_key;

	token = key->payload.data[0];
	token->no_leak_key = true;
	*_key = key;
	key = NULL;
	ret = 0;
	goto error;

error_key:
	key_put(key);
error:
	kfree_sensitive(payload);
	_leave(" = %d", ret);
	return ret;
}

/*
 * Extract the token and set up a session key from the details.
 *
 * struct RXGK_TokenContainer {
 *	afs_int32	kvno;
 *	afs_int32	enctype;
 *	opaque		encrypted_token<>;
 * };
 *
 * [tools.ietf.org/html/draft-wilkinson-afs3-rxgk-afs-08 sec 6.1]
 */
int rxgk_extract_token(struct rxrpc_connection *conn, struct sk_buff *skb,
		       void *token, unsigned int token_len,
		       struct key **_key)
{
	const struct krb5_enctype *krb5;
	const struct krb5_buffer *server_secret;
	struct crypto_aead *token_enc = NULL;
	struct key *server_key;
	unsigned int ticket_len;
	void *ticket;
	u32 kvno, enctype;
	int ret, ec = 0;

	struct {
		__be32 kvno;
		__be32 enctype;
		__be32 token_len;
	} *container;

	if (token_len < sizeof(*container))
		goto short_packet;

	/* Decode the RXGK_TokenContainer object.  This tells us which server
	 * key we should be using.  We can then fetch the key, get the secret
	 * and set up the crypto to extract the token.
	 */
	container = token;
	token += sizeof(*container);

	kvno		= ntohl(container->kvno);
	enctype		= ntohl(container->enctype);
	ticket_len	= ntohl(container->token_len);

	if (ticket_len > xdr_round_down(token_len - sizeof(*container)))
		goto short_packet;

	_debug("KVNO %u", kvno);
	_debug("ENC  %u", enctype);
	_debug("TLEN %u", ticket_len);

	server_key = rxrpc_look_up_server_security(conn, skb, kvno, enctype);
	if (IS_ERR(server_key))
		goto cant_get_server_key;

	down_read(&server_key->sem);
	server_secret = (const void *)&server_key->payload.data[2];
	ret = rxgk_set_up_token_cipher(server_secret, &token_enc, enctype, &krb5, GFP_NOFS);
	up_read(&server_key->sem);
	key_put(server_key);
	if (ret < 0)
		goto cant_get_token;

	/* We can now decrypt and parse the token/ticket.  This allows us to
	 * gain access to K0, from which we can derive the transport key and
	 * thence decode the authenticator.
	 */
	ticket = token;
	ret = rxgk_decrypt(krb5, token_enc, &ticket, &ticket_len, &ec);
	crypto_free_aead(token_enc);
	token_enc = NULL;
	if (ret < 0) {
		if (ret != -ENOMEM)
			return rxrpc_abort_conn(conn, skb, ec, ret,
						rxgk_abort_resp_tok_dec);
		return ret;
	}

	ret = conn->security->default_decode_ticket(conn, skb, ticket,
						    ticket_len, _key);
	if (ret < 0)
		goto cant_get_token;

	_leave(" = 0");
	return ret;

cant_get_server_key:
	ret = PTR_ERR(server_key);
	switch (ret) {
	case -ENOMEM:
		goto temporary_error;
	case -ENOKEY:
	case -EKEYREJECTED:
	case -EKEYEXPIRED:
	case -EKEYREVOKED:
	case -EPERM:
		return rxrpc_abort_conn(conn, skb, RXGK_BADKEYNO, -EKEYREJECTED,
					rxgk_abort_resp_tok_nokey);
	default:
		return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EKEYREJECTED,
					rxgk_abort_resp_tok_keyerr);
	}

cant_get_token:
	switch (ret) {
	case -ENOMEM:
		goto temporary_error;
	case -EINVAL:
		return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EKEYREJECTED,
					rxgk_abort_resp_tok_internal_error);
	case -ENOPKG:
		return rxrpc_abort_conn(conn, skb, KRB5_PROG_KEYTYPE_NOSUPP,
					-EKEYREJECTED, rxgk_abort_resp_tok_nopkg);
	}

temporary_error:
	/* Ignore the response packet if we got a temporary error such as
	 * ENOMEM.  We just want to send the challenge again.  Note that we
	 * also come out this way if the ticket decryption fails.
	 */
	return ret;

short_packet:
	return rxrpc_abort_conn(conn, skb, RXGK_PACKETSHORT, -EPROTO,
				rxgk_abort_resp_tok_short);
}
