/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2014 Nexenta Systems, Inc.  All rights reserved.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <syslog.h>
#include <netdb.h>
#include <sys/param.h>
#include <kerberosv5/krb5.h>
#include <kerberosv5/com_err.h>

#include <smbsrv/libsmb.h>
#include <smbns_krb.h>

/*
 * Kerberized services available on the system.
 */
static smb_krb5_pn_t smb_krb5_pn_tab[] = {
	/*
	 * Service keys are salted with the SMB_KRB_PN_ID_ID_SALT prinipal
	 * name.
	 */
	{SMB_KRB5_PN_ID_SALT,		SMB_PN_SVC_HOST,	SMB_PN_SALT},

	/* CIFS SPNs. (HOST, CIFS, ...) */
	{SMB_KRB5_PN_ID_HOST_FQHN,	SMB_PN_SVC_HOST,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR | SMB_PN_UPN_ATTR},
	{SMB_KRB5_PN_ID_HOST_SHORT,	SMB_PN_SVC_HOST,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
	{SMB_KRB5_PN_ID_CIFS_FQHN,	SMB_PN_SVC_CIFS,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
	{SMB_KRB5_PN_ID_CIFS_SHORT,	SMB_PN_SVC_CIFS,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
	{SMB_KRB5_PN_ID_MACHINE,	NULL,
	    SMB_PN_KEYTAB_ENTRY},

	/* NFS */
	{SMB_KRB5_PN_ID_NFS_FQHN,	SMB_PN_SVC_NFS,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},

	/* HTTP */
	{SMB_KRB5_PN_ID_HTTP_FQHN,	SMB_PN_SVC_HTTP,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},

	/* ROOT */
	{SMB_KRB5_PN_ID_ROOT_FQHN,	SMB_PN_SVC_ROOT,
	    SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
};

#define	SMB_KRB5_SPN_TAB_SZ \
	(sizeof (smb_krb5_pn_tab) / sizeof (smb_krb5_pn_tab[0]))

#define	SMB_KRB5_MAX_BUFLEN	128

static int smb_krb5_kt_open(krb5_context, char *, krb5_keytab *);
static int smb_krb5_kt_addkey(krb5_context, krb5_keytab, const krb5_principal,
    krb5_enctype, krb5_kvno, const krb5_data *, const char *);
static int smb_krb5_spn_count(uint32_t);
static smb_krb5_pn_t *smb_krb5_lookup_pn(smb_krb5_pn_id_t);
static char *smb_krb5_get_pn_by_id(smb_krb5_pn_id_t, uint32_t,
    const char *);
static int smb_krb5_get_kprinc(krb5_context, smb_krb5_pn_id_t, uint32_t,
    const char *, krb5_principal *);


/*
 * Generates a null-terminated array of principal names that
 * represents the list of the available Kerberized services
 * of the specified type (SPN attribute, UPN attribute, or
 * keytab entry).
 *
 * Returns the number of principal names returned via the 1st
 * output parameter (i.e. vals).
 *
 * Caller must invoke smb_krb5_free_spns to free the allocated
 * memory when finished.
 */
uint32_t
smb_krb5_get_pn_set(smb_krb5_pn_set_t *set, uint32_t type, char *fqdn)
{
	int cnt, i;
	smb_krb5_pn_t *tabent;

	if (!set || !fqdn)
		return (0);

	bzero(set, sizeof (smb_krb5_pn_set_t));
	cnt = smb_krb5_spn_count(type);
	set->s_pns = (char **)calloc(cnt + 1, sizeof (char *));

	if (set->s_pns == NULL)
		return (0);

	for (i = 0, set->s_cnt = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
		tabent = &smb_krb5_pn_tab[i];

		if (set->s_cnt == cnt)
			break;

		if ((tabent->p_flags & type) != type)
			continue;

		set->s_pns[set->s_cnt] = smb_krb5_get_pn_by_id(tabent->p_id,
		    type, fqdn);
		if (set->s_pns[set->s_cnt] == NULL) {
			syslog(LOG_ERR, "smbns_ksetpwd: failed to obtain "
			    "principal names: possible transient memory "
			    "shortage");
			smb_krb5_free_pn_set(set);
			return (0);
		}

		set->s_cnt++;
	}

	if (set->s_cnt == 0)
		smb_krb5_free_pn_set(set);

	return (set->s_cnt);
}

void
smb_krb5_free_pn_set(smb_krb5_pn_set_t *set)
{
	int i;

	if (set == NULL || set->s_pns == NULL)
		return;

	for (i = 0; i < set->s_cnt; i++)
		free(set->s_pns[i]);

	free(set->s_pns);
	set->s_pns = NULL;
}

/*
 * Initialize the kerberos context.
 * Return 0 on success. Otherwise, return -1.
 */
int
smb_krb5_ctx_init(krb5_context *ctx)
{
	if (krb5_init_context(ctx) != 0)
		return (-1);

	return (0);
}

/*
 * Free the kerberos context.
 */
void
smb_krb5_ctx_fini(krb5_context ctx)
{
	krb5_free_context(ctx);
}

/*
 * Create an array of Kerberos Princiapls given an array of principal names.
 * Caller must free the allocated memory using smb_krb5_free_kprincs()
 * upon success.
 *
 * Returns 0 on success. Otherwise, returns -1.
 */
int
smb_krb5_get_kprincs(krb5_context ctx, char **names, size_t num,
    krb5_principal **krb5princs)
{
	int i;

	if ((*krb5princs = calloc(num, sizeof (krb5_principal *))) == NULL) {
		return (-1);
	}

	for (i = 0; i < num; i++) {
		if (krb5_parse_name(ctx, names[i], &(*krb5princs)[i]) != 0) {
			smb_krb5_free_kprincs(ctx, *krb5princs, i);
			return (-1);
		}
	}

	return (0);
}

void
smb_krb5_free_kprincs(krb5_context ctx, krb5_principal *krb5princs,
    size_t num)
{
	int i;

	for (i = 0; i < num; i++)
		krb5_free_principal(ctx, krb5princs[i]);

	free(krb5princs);
}

/*
 * Set the workstation trust account password.
 * Returns 0 on success.  Otherwise, returns non-zero value.
 */
int
smb_krb5_setpwd(krb5_context ctx, const char *fqdn, char *passwd)
{
	krb5_error_code code;
	krb5_ccache cc = NULL;
	int result_code = 0;
	krb5_data result_code_string, result_string;
	krb5_principal princ;
	char msg[SMB_KRB5_MAX_BUFLEN];

	if (smb_krb5_get_kprinc(ctx, SMB_KRB5_PN_ID_HOST_FQHN,
	    SMB_PN_UPN_ATTR, fqdn, &princ) != 0)
		return (-1);

	(void) memset(&result_code_string, 0, sizeof (result_code_string));
	(void) memset(&result_string, 0, sizeof (result_string));

	if ((code = krb5_cc_default(ctx, &cc)) != 0) {
		(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
		    "find %s", SMB_CCACHE_PATH);
		smb_krb5_log_errmsg(ctx, msg, code);
		krb5_free_principal(ctx, princ);
		return (-1);
	}

	code = krb5_set_password_using_ccache(ctx, cc, passwd, princ,
	    &result_code, &result_code_string, &result_string);

	(void) krb5_cc_close(ctx, cc);

	if (code != 0)
		smb_krb5_log_errmsg(ctx, "smbns_ksetpwd: KPASSWD protocol "
		    "exchange failed", code);

	if (result_code != 0) {
		syslog(LOG_ERR, "smbns_ksetpwd: KPASSWD failed: rc=%d %.*s",
		    result_code,
		    result_code_string.length,
		    result_code_string.data);
		if (code == 0)
			code = EACCES;
	}

	krb5_free_principal(ctx, princ);
	free(result_code_string.data);
	free(result_string.data);
	return (code);
}

/*
 * Open the keytab file for writing.
 * The keytab should be closed by calling krb5_kt_close().
 */
static int
smb_krb5_kt_open(krb5_context ctx, char *fname, krb5_keytab *kt)
{
	char *ktname;
	krb5_error_code code;
	int len;
	char msg[SMB_KRB5_MAX_BUFLEN];

	*kt = NULL;
	len = snprintf(NULL, 0, "WRFILE:%s", fname) + 1;
	if ((ktname = malloc(len)) == NULL) {
		syslog(LOG_ERR, "smbns_ksetpwd: unable to open keytab %s: "
		    "possible transient memory shortage", fname);
		return (-1);
	}

	(void) snprintf(ktname, len, "WRFILE:%s", fname);

	if ((code = krb5_kt_resolve(ctx, ktname, kt)) != 0) {
		(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: %s", fname);
		smb_krb5_log_errmsg(ctx, msg, code);
		free(ktname);
		return (-1);
	}

	free(ktname);
	return (0);
}

/*
 * Populate the keytab with keys of the specified key version for the
 * specified set of krb5 principals.  All service keys will be salted by:
 * host/<truncated@15_lower_case_hostname>.<fqdn>@<REALM>
 */
int
smb_krb5_kt_populate(krb5_context ctx, const char *fqdn,
    krb5_principal *princs, int count, char *fname, krb5_kvno kvno,
    char *passwd, krb5_enctype *enctypes, int enctype_count)
{
	krb5_keytab kt = NULL;
	krb5_data salt;
	krb5_error_code code;
	krb5_principal salt_princ;
	int i, j;

	if (smb_krb5_kt_open(ctx, fname, &kt) != 0)
		return (-1);

	if (smb_krb5_get_kprinc(ctx, SMB_KRB5_PN_ID_SALT, SMB_PN_SALT,
	    fqdn, &salt_princ) != 0) {
		(void) krb5_kt_close(ctx, kt);
		return (-1);
	}

	code = krb5_principal2salt(ctx, salt_princ, &salt);
	if (code != 0) {
		smb_krb5_log_errmsg(ctx, "smbns_ksetpwd: salt computation "
		    "failed", code);
		krb5_free_principal(ctx, salt_princ);
		(void) krb5_kt_close(ctx, kt);
		return (-1);
	}

	for (j = 0; j < count; j++) {
		for (i = 0; i < enctype_count; i++) {
			if (smb_krb5_kt_addkey(ctx, kt, princs[j], enctypes[i],
			    kvno, &salt, passwd) != 0) {
				krb5_free_principal(ctx, salt_princ);
				krb5_xfree(salt.data);
				(void) krb5_kt_close(ctx, kt);
				return (-1);
			}
		}

	}
	krb5_free_principal(ctx, salt_princ);
	krb5_xfree(salt.data);
	(void) krb5_kt_close(ctx, kt);
	return (0);
}

boolean_t
smb_krb5_kt_find(smb_krb5_pn_id_t id, const char *fqdn, char *fname)
{
	krb5_context ctx;
	krb5_keytab kt;
	krb5_keytab_entry entry;
	krb5_principal princ;
	char ktname[MAXPATHLEN];
	boolean_t found = B_FALSE;

	if (!fqdn || !fname)
		return (found);

	if (smb_krb5_ctx_init(&ctx) != 0)
		return (found);

	if (smb_krb5_get_kprinc(ctx, id, SMB_PN_KEYTAB_ENTRY, fqdn,
	    &princ) != 0) {
		smb_krb5_ctx_fini(ctx);
		return (found);
	}

	(void) snprintf(ktname, MAXPATHLEN, "FILE:%s", fname);
	if (krb5_kt_resolve(ctx, ktname, &kt) == 0) {
		if (krb5_kt_get_entry(ctx, kt, princ, 0, 0, &entry) == 0) {
			found = B_TRUE;
			(void) krb5_kt_free_entry(ctx, &entry);
		}

		(void) krb5_kt_close(ctx, kt);
	}

	krb5_free_principal(ctx, princ);
	smb_krb5_ctx_fini(ctx);
	return (found);
}

/*
 * Add a key of the specified encryption type for the specified principal
 * to the keytab file.
 * Returns 0 on success. Otherwise, returns -1.
 */
static int
smb_krb5_kt_addkey(krb5_context ctx, krb5_keytab kt, const krb5_principal princ,
    krb5_enctype enctype, krb5_kvno kvno, const krb5_data *salt,
    const char *pw)
{
	krb5_keytab_entry *entry;
	krb5_data password;
	krb5_keyblock key;
	krb5_error_code code;
	char buf[SMB_KRB5_MAX_BUFLEN], msg[SMB_KRB5_MAX_BUFLEN];
	int rc = 0;

	if ((code = krb5_enctype_to_string(enctype, buf, sizeof (buf)))) {
		(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: unknown "
		    "encryption type (%d)", enctype);
		smb_krb5_log_errmsg(ctx, msg, code);
		return (-1);
	}

	if ((entry = (krb5_keytab_entry *) malloc(sizeof (*entry))) == NULL) {
		syslog(LOG_ERR, "smbns_ksetpwd: possible transient "
		    "memory shortage");
		return (-1);
	}

	(void) memset((char *)entry, 0, sizeof (*entry));

	password.length = strlen(pw);
	password.data = (char *)pw;

	code = krb5_c_string_to_key(ctx, enctype, &password, salt, &key);
	if (code != 0) {
		(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
		    "generate key (%d)", enctype);
		smb_krb5_log_errmsg(ctx, msg, code);
		free(entry);
		return (-1);
	}

	(void) memcpy(&entry->key, &key, sizeof (krb5_keyblock));
	entry->vno = kvno;
	entry->principal = princ;

	if ((code = krb5_kt_add_entry(ctx, kt, entry)) != 0) {
		(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
		    "add key (%d)", enctype);
		smb_krb5_log_errmsg(ctx, msg, code);
		rc = -1;
	}

	free(entry);
	if (key.length)
		krb5_free_keyblock_contents(ctx, &key);
	return (rc);
}

static int
smb_krb5_spn_count(uint32_t type)
{
	int i, cnt;

	for (i = 0, cnt = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
		if (smb_krb5_pn_tab[i].p_flags & type)
			cnt++;
	}

	return (cnt);
}

/*
 * Generate the Kerberos Principal given a principal name format and the
 * fully qualified domain name. On success, caller must free the allocated
 * memory by calling krb5_free_principal().
 */
static int
smb_krb5_get_kprinc(krb5_context ctx, smb_krb5_pn_id_t id, uint32_t type,
    const char *fqdn, krb5_principal *princ)
{
	char *buf;

	if ((buf = smb_krb5_get_pn_by_id(id, type, fqdn)) == NULL)
		return (-1);

	if (krb5_parse_name(ctx, buf, princ) != 0) {
		free(buf);
		return (-1);
	}

	free(buf);
	return (0);
}

/*
 * Looks up an entry in the principal name table given the ID.
 */
static smb_krb5_pn_t *
smb_krb5_lookup_pn(smb_krb5_pn_id_t id)
{
	int i;
	smb_krb5_pn_t *tabent;

	for (i = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
		tabent = &smb_krb5_pn_tab[i];
		if (id == tabent->p_id)
			return (tabent);
	}

	return (NULL);
}

/*
 * Construct the principal name given an ID, the requested type, and the
 * fully-qualified name of the domain of which the principal is a member.
 */
static char *
smb_krb5_get_pn_by_id(smb_krb5_pn_id_t id, uint32_t type,
    const char *fqdn)
{
	char nbname[NETBIOS_NAME_SZ];
	char hostname[MAXHOSTNAMELEN];
	char *realm = NULL;
	smb_krb5_pn_t *pn;
	char *buf;

	(void) smb_getnetbiosname(nbname, NETBIOS_NAME_SZ);
	(void) smb_gethostname(hostname, MAXHOSTNAMELEN, SMB_CASE_LOWER);

	pn = smb_krb5_lookup_pn(id);

	/* detect inconsistent requested format and type */
	if ((type & pn->p_flags) != type)
		return (NULL);

	switch (id) {
	case SMB_KRB5_PN_ID_SALT:
		(void) asprintf(&buf, "%s/%s.%s",
		    pn->p_svc, smb_strlwr(nbname), fqdn);
		break;

	case SMB_KRB5_PN_ID_HOST_FQHN:
	case SMB_KRB5_PN_ID_CIFS_FQHN:
	case SMB_KRB5_PN_ID_NFS_FQHN:
	case SMB_KRB5_PN_ID_HTTP_FQHN:
	case SMB_KRB5_PN_ID_ROOT_FQHN:
		(void) asprintf(&buf, "%s/%s.%s",
		    pn->p_svc, hostname, fqdn);
		break;

	case SMB_KRB5_PN_ID_HOST_SHORT:
	case SMB_KRB5_PN_ID_CIFS_SHORT:
		(void) asprintf(&buf, "%s/%s",
		    pn->p_svc, nbname);
		break;

	/*
	 * SPN for the machine account, which is simply the
	 * (short) machine name with a dollar sign appended.
	 */
	case SMB_KRB5_PN_ID_MACHINE:
		(void) asprintf(&buf, "%s$", nbname);
		break;

	default:
		return (NULL);
	}

	/*
	 * If the requested principal is either added to keytab / the machine
	 * account as the UPN attribute or used for key salt generation,
	 * the principal name must have the @<REALM> portion.
	 */
	if (type & (SMB_PN_KEYTAB_ENTRY | SMB_PN_UPN_ATTR | SMB_PN_SALT)) {
		if ((realm = strdup(fqdn)) == NULL) {
			free(buf);
			return (NULL);
		}

		(void) smb_strupr(realm);
		if (buf != NULL) {
			char *tmp;

			(void) asprintf(&tmp, "%s@%s", buf,
			    realm);
			free(buf);
			buf = tmp;
		}

		free(realm);
	}

	return (buf);
}