/*
   Unix SMB/CIFS implementation.

   Samba kpasswd implementation

   Copyright (c) 2016      Andreas Schneider <asn@samba.org>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "includes.h"
#include "samba/service_task.h"
#include "param/param.h"
#include "auth/auth.h"
#include "auth/gensec/gensec.h"
#include "gensec_krb5_helpers.h"
#include "kdc/kdc-server.h"
#include "kdc/kpasswd_glue.h"
#include "kdc/kpasswd-service.h"
#include "kdc/kpasswd-helper.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_KERBEROS

static krb5_error_code kpasswd_change_password(struct kdc_server *kdc,
					       TALLOC_CTX *mem_ctx,
					       const struct gensec_security *gensec_security,
					       struct auth_session_info *session_info,
					       DATA_BLOB *password,
					       DATA_BLOB *kpasswd_reply,
					       const char **error_string)
{
	NTSTATUS status;
	NTSTATUS result = NT_STATUS_UNSUCCESSFUL;
	enum samPwdChangeReason reject_reason = SAM_PWD_CHANGE_NO_ERROR;
	const char *reject_string = NULL;
	struct samr_DomInfo1 *dominfo = NULL;
	bool ok;
	int ret;

	/*
	 * We're doing a password change (rather than a password set), so check
	 * that we were given an initial ticket.
	 */
	ret = gensec_krb5_initial_ticket(gensec_security);
	if (ret != 1) {
		*error_string = "Expected an initial ticket";
		return KRB5_KPASSWD_INITIAL_FLAG_NEEDED;
	}

	status = samdb_kpasswd_change_password(mem_ctx,
					       kdc->task->lp_ctx,
					       kdc->task->event_ctx,
					       session_info,
					       password,
					       &reject_reason,
					       &dominfo,
					       &reject_string,
					       &result);
	if (!NT_STATUS_IS_OK(status)) {
		ok = kpasswd_make_error_reply(mem_ctx,
					      KRB5_KPASSWD_ACCESSDENIED,
					      reject_string,
					      kpasswd_reply);
		if (!ok) {
			*error_string = "Failed to create reply";
			return KRB5_KPASSWD_HARDERROR;
		}
		/* We want to send an an authenticated packet. */
		return 0;
	}

	ok = kpasswd_make_pwchange_reply(mem_ctx,
					 result,
					 reject_reason,
					 dominfo,
					 kpasswd_reply);
	if (!ok) {
		*error_string = "Failed to create reply";
		return KRB5_KPASSWD_HARDERROR;
	}

	return 0;
}

static krb5_error_code kpasswd_set_password(struct kdc_server *kdc,
					    TALLOC_CTX *mem_ctx,
					    const struct gensec_security *gensec_security,
					    struct auth_session_info *session_info,
					    DATA_BLOB *decoded_data,
					    DATA_BLOB *kpasswd_reply,
					    const char **error_string)
{
	krb5_context context = kdc->smb_krb5_context->krb5_context;
	krb5_error_code code;
	krb5_principal target_principal;
	ChangePasswdDataMS chpw = {};
	size_t chpw_len = 0;
	DATA_BLOB password = data_blob_null;
	enum samPwdChangeReason reject_reason = SAM_PWD_CHANGE_NO_ERROR;
	struct samr_DomInfo1 *dominfo = NULL;
	char *target_principal_string = NULL;
	bool is_service_principal = false;
	NTSTATUS status;
	bool ok;

	code = decode_ChangePasswdDataMS(decoded_data->data,
					 decoded_data->length,
					 &chpw,
					 &chpw_len);
	if (code != 0) {
		DBG_WARNING("decode_ChangePasswdDataMS failed\n");
		ok = kpasswd_make_error_reply(mem_ctx,
					      KRB5_KPASSWD_MALFORMED,
					      "Failed to decode packet",
					      kpasswd_reply);
		if (!ok) {
			*error_string = "Failed to create reply";
			return KRB5_KPASSWD_HARDERROR;
		}
		return 0;
	}

	ok = convert_string_talloc_handle(mem_ctx,
					  lpcfg_iconv_handle(kdc->task->lp_ctx),
					  CH_UTF8,
					  CH_UTF16,
					  chpw.newpasswd.data,
					  chpw.newpasswd.length,
					  &password.data,
					  &password.length);
	if (!ok) {
		free_ChangePasswdDataMS(&chpw);
		DBG_WARNING("String conversion failed\n");
		*error_string = "String conversion failed";
		return KRB5_KPASSWD_HARDERROR;
	}

	if ((chpw.targname != NULL && chpw.targrealm == NULL) ||
	    (chpw.targname == NULL && chpw.targrealm != NULL)) {
		free_ChangePasswdDataMS(&chpw);
		ok = kpasswd_make_error_reply(mem_ctx,
					      KRB5_KPASSWD_MALFORMED,
					      "Realm and principal must be "
					      "both present, or neither present",
					      kpasswd_reply);
		if (!ok) {
			*error_string = "Failed to create reply";
			return KRB5_KPASSWD_HARDERROR;
		}
		return 0;
	}

	if (chpw.targname == NULL || chpw.targrealm == NULL) {
		free_ChangePasswdDataMS(&chpw);
		return kpasswd_change_password(kdc,
					       mem_ctx,
					       gensec_security,
					       session_info,
					       &password,
					       kpasswd_reply,
					       error_string);
	}
	code = krb5_build_principal_ext(context,
					&target_principal,
					strlen(*chpw.targrealm),
					*chpw.targrealm,
					0);
	if (code != 0) {
		free_ChangePasswdDataMS(&chpw);
		return kpasswd_make_error_reply(mem_ctx,
						KRB5_KPASSWD_MALFORMED,
						"Failed to parse principal",
						kpasswd_reply);
	}
	code = copy_PrincipalName(chpw.targname,
				  &target_principal->name);
	free_ChangePasswdDataMS(&chpw);
	if (code != 0) {
		krb5_free_principal(context, target_principal);
		return kpasswd_make_error_reply(mem_ctx,
						KRB5_KPASSWD_MALFORMED,
						"Failed to parse principal",
						kpasswd_reply);
	}

	if (target_principal->name.name_string.len >= 2) {
		is_service_principal = true;

		code = krb5_unparse_name_short(context,
					       target_principal,
					       &target_principal_string);
	} else {
		code = krb5_unparse_name(context,
					 target_principal,
					 &target_principal_string);
	}
	krb5_free_principal(context, target_principal);
	if (code != 0) {
		ok = kpasswd_make_error_reply(mem_ctx,
					      KRB5_KPASSWD_MALFORMED,
					      "Failed to parse principal",
					      kpasswd_reply);
		if (!ok) {
			krb5_xfree(target_principal_string);
			*error_string = "Failed to create reply";
			return KRB5_KPASSWD_HARDERROR;
		}
	}

	status = kpasswd_samdb_set_password(mem_ctx,
					    kdc->task->event_ctx,
					    kdc->task->lp_ctx,
					    session_info,
					    is_service_principal,
					    target_principal_string,
					    &password,
					    &reject_reason,
					    &dominfo);
	krb5_xfree(target_principal_string);
	if (!NT_STATUS_IS_OK(status)) {
		DBG_ERR("kpasswd_samdb_set_password failed - %s\n",
			nt_errstr(status));
	}

	ok = kpasswd_make_pwchange_reply(mem_ctx,
					 status,
					 reject_reason,
					 dominfo,
					 kpasswd_reply);
	if (!ok) {
		*error_string = "Failed to create reply";
		return KRB5_KPASSWD_HARDERROR;
	}

	return 0;
}

krb5_error_code kpasswd_handle_request(struct kdc_server *kdc,
				       TALLOC_CTX *mem_ctx,
				       struct gensec_security *gensec_security,
				       uint16_t verno,
				       DATA_BLOB *decoded_data,
				       DATA_BLOB *kpasswd_reply,
				       const char **error_string)
{
	struct auth_session_info *session_info;
	NTSTATUS status;
	krb5_error_code code;

	status = gensec_session_info(gensec_security,
				     mem_ctx,
				     &session_info);
	if (!NT_STATUS_IS_OK(status)) {
		*error_string = talloc_asprintf(mem_ctx,
						"gensec_session_info failed - %s",
						nt_errstr(status));
		return KRB5_KPASSWD_HARDERROR;
	}

	/*
	 * Since the kpasswd service shares its keys with the krbtgt, we might
	 * have received a TGT rather than a kpasswd ticket. We need to check
	 * the ticket type to ensure that TGTs cannot be misused in this manner.
	 */
	code = kpasswd_check_non_tgt(session_info,
				     error_string);
	if (code != 0) {
		DBG_WARNING("%s\n", *error_string);
		return code;
	}

	switch(verno) {
	case KRB5_KPASSWD_VERS_CHANGEPW: {
		DATA_BLOB password = data_blob_null;
		bool ok;

		ok = convert_string_talloc_handle(mem_ctx,
						  lpcfg_iconv_handle(kdc->task->lp_ctx),
						  CH_UTF8,
						  CH_UTF16,
						  decoded_data->data,
						  decoded_data->length,
						  &password.data,
						  &password.length);
		if (!ok) {
			*error_string = "String conversion failed!";
			DBG_WARNING("%s\n", *error_string);
			return KRB5_KPASSWD_HARDERROR;
		}

		return kpasswd_change_password(kdc,
					       mem_ctx,
					       gensec_security,
					       session_info,
					       &password,
					       kpasswd_reply,
					       error_string);
	}
	case KRB5_KPASSWD_VERS_SETPW: {
		return kpasswd_set_password(kdc,
					    mem_ctx,
					    gensec_security,
					    session_info,
					    decoded_data,
					    kpasswd_reply,
					    error_string);
	}
	default:
		*error_string = talloc_asprintf(mem_ctx,
						"Protocol version %u not supported",
						verno);
		return KRB5_KPASSWD_BAD_VERSION;
	}

	return 0;
}
