/*
 * COPYRIGHT (c) 2011
 * The Regents of the University of Michigan
 * ALL RIGHTS RESERVED
 *
 * Permission is granted to use, copy, create derivative works
 * and redistribute this software and such derivative works
 * for any purpose, so long as the name of The University of
 * Michigan is not used in any advertising or publicity
 * pertaining to the use of distribution of this software
 * without specific, written prior authorization.  If the
 * above copyright notice or any other identification of the
 * University of Michigan is included in any copy of any
 * portion of this software, then the disclaimer below must
 * also be included.
 *
 * THIS SOFTWARE IS PROVIDED AS IS, WITHOUT REPRESENTATION
 * FROM THE UNIVERSITY OF MICHIGAN AS TO ITS FITNESS FOR ANY
 * PURPOSE, AND WITHOUT WARRANTY BY THE UNIVERSITY OF
 * MICHIGAN OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
 * WITHOUT LIMITATION THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
 * REGENTS OF THE UNIVERSITY OF MICHIGAN SHALL NOT BE LIABLE
 * FOR ANY DAMAGES, INCLUDING SPECIAL, INDIRECT, INCIDENTAL, OR
 * CONSEQUENTIAL DAMAGES, WITH RESPECT TO ANY CLAIM ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OF THE SOFTWARE, EVEN
 * IF IT HAS BEEN OR IS HEREAFTER ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGES.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif	/* HAVE_CONFIG_H */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <gssapi/gssapi.h>
#include <krb5.h>

#include "gss_util.h"
#include "gss_oids.h"
#include "err_util.h"
#include "svcgssd_krb5.h"
#include "version.h"

#define MYBUFLEN 1024

char *supported_enctypes_filename = "/proc/fs/nfsd/supported_krb5_enctypes";
int parsed_num_enctypes = 0;
krb5_enctype *parsed_enctypes = NULL;
char *cached_enctypes = NULL;

/*==========================*/
/*===  Internal routines ===*/
/*==========================*/

/*
 * Parse the supported encryption type information
 */
static int
parse_enctypes(char *enctypes)
{
	int n = 0;
	char *curr, *comma;
	int i;

	/* Don't parse the same string over and over... */
	if (cached_enctypes && strcmp(cached_enctypes, enctypes) == 0)
		return 0;

	/* Free any existing cached_enctypes */
	svcgssd_free_enctypes();

	/* count the number of commas */
	for (curr = enctypes; curr && *curr != '\0'; curr = ++comma) {
		comma = strchr(curr, ',');
		if (comma != NULL)
			n++;
		else
			break;
	}

	/* If no more commas and we're not at the end, there's one more value */
	if (*curr != '\0')
		n++;

	/* Empty string, return an error */
	if (n == 0)
		return ENOENT;

	/* Skip pass any non digits */
	while (*enctypes && isdigit(*enctypes) == 0)
		enctypes++;
	if (*enctypes == '\0')
		return EINVAL;

	/* Allocate space for enctypes array */
	if ((parsed_enctypes = (int *) calloc(n, sizeof(int))) == NULL) {
		return ENOMEM;
	}

	/* Now parse each value into the array */
	for (curr = enctypes, i = 0; curr && *curr != '\0'; curr = ++comma) {
		parsed_enctypes[i++] = atoi(curr);
		comma = strchr(curr, ',');
		if (comma == NULL)
			break;
	}

	parsed_num_enctypes = n;
	if ((cached_enctypes = malloc(strlen(enctypes)+1)))
		strcpy(cached_enctypes, enctypes);

	return 0;
}

static void
get_kernel_supported_enctypes(void)
{
	FILE *s_e;
	int ret;
	char buffer[MYBUFLEN + 1];

	memset(buffer, '\0', sizeof(buffer));

	s_e = fopen(supported_enctypes_filename, "r");
	if (s_e == NULL)
		goto out_clean_parsed;

	ret = fread(buffer, 1, MYBUFLEN, s_e);
	if (ret < 0) {
		fclose(s_e);
		goto out_clean_parsed;
	}
	fclose(s_e);
	if (parse_enctypes(buffer)) {
		goto out_clean_parsed;
	}
out:
	return;

out_clean_parsed:
	if (parsed_enctypes != NULL) {
		free(parsed_enctypes);
		parsed_num_enctypes = 0;
	}
	goto out;
}

/*==========================*/
/*===  External routines ===*/
/*==========================*/

void
svcgssd_free_enctypes(void)
{
	free(cached_enctypes);
	cached_enctypes = NULL;

	if (parsed_enctypes != NULL) {
		free(parsed_enctypes);
		parsed_enctypes = NULL;
		parsed_num_enctypes = 0;
	}
}

/*
 * Get encryption types supported by the kernel, and then
 * call gss_krb5_set_allowable_enctypes() to limit the
 * encryption types negotiated.
 *
 * Returns:
 *	0 => all went well
 *     -1 => there was an error
 */

int
svcgssd_limit_krb5_enctypes(void)
{
#ifdef HAVE_SET_ALLOWABLE_ENCTYPES
	u_int maj_stat, min_stat;
	krb5_enctype old_kernel_enctypes[] = {
		ENCTYPE_DES_CBC_CRC,
		ENCTYPE_DES_CBC_MD5,
		ENCTYPE_DES_CBC_MD4 };
	krb5_enctype new_kernel_enctypes[] = {
		ENCTYPE_AES256_CTS_HMAC_SHA1_96,
		ENCTYPE_AES128_CTS_HMAC_SHA1_96,
		ENCTYPE_DES3_CBC_SHA1,
		ENCTYPE_ARCFOUR_HMAC,
		ENCTYPE_DES_CBC_CRC,
		ENCTYPE_DES_CBC_MD5,
		ENCTYPE_DES_CBC_MD4 };
	krb5_enctype *default_enctypes, *enctypes;
	int default_num_enctypes, num_enctypes;


	if (linux_version_code() < MAKE_VERSION(2, 6, 35)) {
		default_enctypes = old_kernel_enctypes;
		default_num_enctypes =
			sizeof(old_kernel_enctypes) / sizeof(old_kernel_enctypes[0]);
	} else {
		default_enctypes = new_kernel_enctypes;
		default_num_enctypes =
			sizeof(new_kernel_enctypes) / sizeof(new_kernel_enctypes[0]);
	}

	get_kernel_supported_enctypes();

	if (parsed_enctypes != NULL) {
		enctypes = parsed_enctypes;
		num_enctypes = parsed_num_enctypes;
		printerr(2, "%s: Calling gss_set_allowable_enctypes with %d "
			"enctypes from the kernel\n", __func__, num_enctypes);
	} else {
		enctypes = default_enctypes;
		num_enctypes = default_num_enctypes;
		printerr(2, "%s: Calling gss_set_allowable_enctypes with %d "
			"enctypes from defaults\n", __func__, num_enctypes);
	}

	maj_stat = gss_set_allowable_enctypes(&min_stat, gssd_creds,
			&krb5oid, num_enctypes, enctypes);
	if (maj_stat != GSS_S_COMPLETE) {
		printerr(1, "WARNING: gss_set_allowable_enctypes failed\n");
		pgsserr("svcgssd_limit_krb5_enctypes: gss_set_allowable_enctypes",
			maj_stat, min_stat, &krb5oid);
		return -1;
	}
#endif
	return 0;
}
