/*
 * Copyright (c) 2003-2015
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*****************************************************************************
 * COPYRIGHT AND PERMISSION NOTICE
 * 
 * Copyright (c) 2001-2003 The Queen in Right of Canada
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, and/or sell
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, provided that the above copyright notice(s) and this
 * permission notice appear in all copies of the Software and that both the
 * above copyright notice(s) and this permission notice appear in supporting
 * documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE 
 * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
 * SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder shall not
 * be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization of the
 * copyright holder.
 ***************************************************************************/

/*
 * Unix password based authentication.
 * Installations that use shadow passwords and access them via getspnam()
 * must have HAVE_GETSPNAM defined.
 * On some Unix platforms, getpwnam(3) can be configured to use YP (NIS)
 * maps and so this program may inherit that capability.
 * On some Unix platforms, getpwnam(3) etc. do not return encrypted passwords;
 * the PAM module for validating Unix platforms must be used.
 *
 * This program will likely have to run as root to retrieve an encrypted
 * password or ask for password validation.
 *
 * Recognized OPTIONS:
 * USE_PAM: if "yes" or "on", only use the PAM-based method
 *          if "no" or "off", do not use the PAM-based method
 *          if not specified, use the PAM-based method only if the
 *          encrypted password is unavailable
 * PAM_SERVICE: the name of the PAM service policy to use; if unspecified,
 *          DEFAULT_PAM_SERVICE is used.
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2015\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_unix_auth.c 2770 2015-01-06 19:25:15Z brachman $";
#endif

#include "dacs.h"

static const char *log_module_name = "local_unix_auth";

static char *get_encrypted_passwd_by_name(char *username);

#ifdef HAVE_GETSPNAM
#include <shadow.h>

static char *
get_encrypted_passwd_by_name(char *username)
{
  struct spwd *spwd;

  if ((spwd = getspnam(username)) == NULL) {
	log_err((LOG_ERROR_LEVEL, "getspnam() failed for '%s'", username));
	return(NULL);
  }

  return(strdup(spwd->sp_pwdp));
}
#else
#include <pwd.h>

static char *
get_encrypted_passwd_by_name(char *username)
{
  struct passwd *pwd;

  if ((pwd = getpwnam(username)) == NULL) {
	log_err((LOG_ERROR_LEVEL, "getpwnam() failed for '%s'", username));
	return(NULL);
  }

  return(strdup(pwd->pw_passwd));
}
#endif

/*
 * Simple Unix password authentication
 * This must have root privileges to retrieve the encrypted password,
 * otherwise the encrypted password won't be returned (although getpwnam()
 * will appear to be successful).
 * We assume the caller destroys the plaintext password as soon as we
 * return.
 * Return 0 if authentication succeeds, -1 if an error occurs (including if
 * USERNAME or PASSWORD is invalid), or -2 if it appears that this method did
 * not work because the system did not return an encrypted password.
 */
int
local_unix_auth(char *username, char *password, char *aux)
{
  char *encrypted_pass, *epwd, *salt;
  size_t off;

  if (username == NULL || password == NULL || password[0] == '\0') {
	log_msg((LOG_DEBUG_LEVEL, "Missing username or password"));
	return(-1);
  }

  if ((epwd = get_encrypted_passwd_by_name(username)) == NULL) {
	log_msg((LOG_DEBUG_LEVEL, "Password lookup for \"%s\" failed", username));
	return(-1);
  }

  if (epwd[0] == '\0') {
	/* The encrypted password was not returned. */
	log_msg((LOG_DEBUG_LEVEL, "Invalid encrypted password for \"%s\"",
			 username));
	return(-2);
  }

  off = strspn(epwd, "*");
  if (epwd[off] == '\0') {
	/*
	 * If the returned string consists solely of '*', we assume that the
	 * system does not return an encrypted password (e.g., Mac OS X).
	 */
	log_msg((LOG_DEBUG_LEVEL, "Invalid encrypted password for \"%s\"",
			 username));
	return(-2);
  }

  salt = epwd;

  if ((encrypted_pass = crypt(password, salt)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Can't compute encrypted password for \"%s\"",
			 username));
	return(-1);
  }

  if (streq(encrypted_pass, epwd)) {
	log_msg((LOG_INFO_LEVEL,
			 "Auth succeeded using encrypted Unix password for username=\"%s\"",
			 username));
	return(0);
  }

  /* We failed, probably because we couldn't get the encrypted password. */
  if (geteuid() != 0)
	log_msg((LOG_ERROR_LEVEL, "Check that this program is running as root."));

  log_msg((LOG_TRACE_LEVEL, "Auth failed for username=\"%s\"", username));
  log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
		   "Using password=\"%s\"", password));

  return(-1);
}

#ifdef HAVE_LIBPAM
#include <security/pam_appl.h>

typedef struct Pam_data {
  pam_handle_t *pamh;
  char *passwd;
  int num_msg;
  struct pam_response *resp_data;
} Pam_data;

/*
 * Return 1 if PROMPT is an acceptable prompt by a PAM module for a password,
 * 0 otherwise.
 * Case is ignored, as are any trailing spaces on PROMPT.
 * This might need to be generalized.
 */
static int
pam_passwd_prompt_ok(const char *prompt)
{
  size_t prefix_len, suffix_len;

  /* If the empty string has been configured, everything is acceptable. */
  if ((prefix_len = strlen(PAM_PASSWD_CONV_MSG)) == 0)
	return(1);

  if (!strncaseeq(prompt, PAM_PASSWD_CONV_MSG, prefix_len))
	return(0);

  /* Look for a case-insensitive match, followed by zero or more spaces. */
  suffix_len = strspn(prompt + prefix_len, " ");
  if (prompt[prefix_len + suffix_len] == '\0')
	return(1);

  return(0);
}

static int
pam_passwd_conv(int num_msg, const struct pam_message **msg,
				struct pam_response **resp, void *appdata_ptr)
{
  struct pam_response *r;
  Pam_data *pam_data;

  log_msg((LOG_DEBUG_LEVEL, "Received %d messages", num_msg));
  if (num_msg != 1) {
	log_msg((LOG_ERROR_LEVEL, "Expected exactly one message"));
	return(PAM_CONV_ERR);
  }

  if (appdata_ptr == NULL) {
	log_msg((LOG_ERROR_LEVEL, "NULL pam_data"));
	return(PAM_CONV_ERR);
  }
  pam_data = (Pam_data *) appdata_ptr;

  r = (struct pam_response *) calloc(num_msg, sizeof(struct pam_response));
  pam_data->num_msg = num_msg;
  pam_data->resp_data = r;

  r[0].resp = NULL;
  r[0].resp_retcode = 0;

  switch (msg[0]->msg_style) {
  case PAM_PROMPT_ECHO_OFF:
	log_msg((LOG_DEBUG_LEVEL, "PAM_PROMPT_ECHO_OFF"));
	if (msg[0]->msg != NULL)
	  log_msg((LOG_DEBUG_LEVEL, "\"%s\"", msg[0]->msg));
	if (!pam_passwd_prompt_ok(msg[0]->msg)) {
	  log_msg((LOG_ERROR_LEVEL, "Unexpected message: \"%s\"", msg[0]->msg));
	  return(PAM_CONV_ERR);
	}

	r[0].resp = pam_data->passwd;
	r[0].resp_retcode = 0;
	break;

  case PAM_PROMPT_ECHO_ON:
	log_msg((LOG_ERROR_LEVEL, "Unexpected message type: PAM_PROMPT_ECHO_ON"));
	if (msg[0]->msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Message: \"%s\"", msg[0]->msg));
	break;
  
  case PAM_ERROR_MSG:
	log_msg((LOG_ERROR_LEVEL, "Unexpected message type: PAM_ERROR_MSG"));
	if (msg[0]->msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Message: \"%s\"", msg[0]->msg));
	break;

  case PAM_TEXT_INFO:
	log_msg((LOG_ERROR_LEVEL, "Unexpected message type: PAM_TEXT_INFO"));
	if (msg[0]->msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Message: \"%s\"", msg[0]->msg));
	break;

  default:
	log_msg((LOG_ERROR_LEVEL, "Unknown message type"));
	if (msg[0]->msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Message: \"%s\"", msg[0]->msg));
	break;
  }

  *resp = r;

  return((r[0].resp != NULL) ? PAM_SUCCESS : PAM_CONV_ERR);
}

/*
 * Like local_unix_auth(), except use PAM to validate the system password
 * PASSWORD for user USERNAME via policy PAM_SERVICE.
 * Return 0 if authentication succeeds, -1 otherwise.
 */
int
local_unix_pam_auth(char *pam_service, char *username, char *password,
					char *aux)
{
  int retval;
  char *service;
  Pam_data *pam_data;
  pam_handle_t *pamh;
  static struct pam_conv pam_conv = { pam_passwd_conv, NULL };

  if (username == NULL || password == NULL || password[0] == '\0') {
	log_msg((LOG_DEBUG_LEVEL, "Missing username or password"));
	return(-1);
  }

  pam_data = ALLOC(Pam_data);
  pam_data->pamh  = pamh = NULL;
  pam_data->passwd = NULL;
  pam_data->num_msg = 0;
  pam_data->resp_data = NULL;

  pam_conv.appdata_ptr = pam_data;

  if (pam_service != NULL)
	service = pam_service;
  else
	service = DEFAULT_PAM_SERVICE;

  retval = pam_start(service, username, &pam_conv, &pamh);
  if (retval != PAM_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "pam_start() failed for service \"%s\": %s",
			 service, PAM_STRERROR(pamh, retval)));
	log_msg((LOG_TRACE_LEVEL, "Auth failed for username=\"%s\"", username));
	return(-1);
  }

  pam_data->pamh = pamh;
  /* Do not make a copy. */
  pam_data->passwd = password;

  retval = pam_authenticate(pamh, PAM_SILENT);

  if (pam_end(pamh, retval) != PAM_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "pam_end() failed"));
	retval = PAM_SYSTEM_ERR;
  }

  /* It appears that PAM frees pam_data->resp_data... */
  free(pam_data);

  if (retval == PAM_SUCCESS) {
	log_msg((LOG_INFO_LEVEL, "Auth succeeded using PAM for username=\"%s\"",
			 username));
	return(0);
  }

  if (geteuid() != 0)
	log_msg((LOG_ERROR_LEVEL, "Check that this program is running as root."));

  log_msg((LOG_TRACE_LEVEL, "Auth failed for username=\"%s\" (%s)",
		   username, PAM_STRERROR(NULL, retval)));
  log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
		   "Using password=\"%s\"", password));

  return(-1);
}
#else
int
local_unix_pam_auth(char *pam_service, char *username, char *password,
					char *aux)
{

  log_msg((LOG_ERROR_LEVEL, "PAM support is unavailable."));
  return(-1);
}
#endif

#ifdef PROG
int
main(int argc, char **argv)
{
  int emitted_dtd, i, st, use_pam;
  char *errmsg, *jurisdiction, *username, *pam_service, *password, *aux;
  Auth_reply_ok ok;
  Kwv *kwv;

  use_pam = AUTH_UNIX_DEFAULT_PAM;
  pam_service = DEFAULT_PAM_SERVICE;
  errmsg = "internal";
  emitted_dtd = 0;
  username = password = aux = jurisdiction = NULL;

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);
	if (emitted_dtd) {
	  printf("%s\n", make_xml_auth_reply_failed(NULL, NULL));
	  emit_xml_trailer(stdout);
	}
	if (errmsg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Failed: reason=%s", errmsg));

	exit(1);
  }

  /* This must go after initialization. */
  emitted_dtd = emit_xml_header(stdout, "auth_reply");

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else if (streq(kwv->pairs[i]->name, "USE_PAM")) {
	  if (strcaseeq(kwv->pairs[i]->val, "yes")
		  || strcaseeq(kwv->pairs[i]->val, "on"))
		use_pam = AUTH_UNIX_USE_PAM;
	  else if (strcaseeq(kwv->pairs[i]->val, "no")
			   || strcaseeq(kwv->pairs[i]->val, "off"))
		use_pam = AUTH_UNIX_WITHOUT_PAM;
	  else if (strcaseeq(kwv->pairs[i]->val, "both")
			   || strcaseeq(kwv->pairs[i]->val, "try"))
		use_pam = AUTH_UNIX_TRY_PAM;
	  else {
		errmsg = "Unrecognized USE_PAM value";
		goto fail;
	  }
	}
	else if (streq(kwv->pairs[i]->name, "PAM_SERVICE"))
	  pam_service = kwv->pairs[i]->val;
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* If possible, verify that we're truly responsible for DACS_JURISDICTION */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  if (use_pam == AUTH_UNIX_USE_PAM)
	st = local_unix_pam_auth(pam_service, username, password, aux);
  else {
	st = local_unix_auth(username, password, aux);
	if (st == -2 && use_pam == AUTH_UNIX_TRY_PAM)
	  st = local_unix_pam_auth(pam_service, username, password, aux);
  }

  if (st != 0) {
	errmsg = "Username/Password/Aux incorrect";
	goto fail;
  }

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  ok.username = username;
  /* If this wasn't specified, dacs_authenticate will use the default. */
  ok.lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");
  ok.roles_reply = NULL;
  printf("%s\n", make_xml_auth_reply_ok(&ok));

  emit_xml_trailer(stdout);
  exit(0);
}
#endif
