/*
 * pam_unix password management
 *
 * Main coding by Elliot Lee <sopwith@redhat.com>, Red Hat Software.
 * Copyright (C) 1996.
 * Copyright (c) Jan Rękorajski, 1999.
 * Copyright (c) Red Hat, Inc., 2007, 2008.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, and the entire permission notice in its entirety,
 *    including the disclaimer of warranties.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior
 *    written permission.
 *
 * ALTERNATIVELY, this product may be distributed under the terms of
 * the GNU Public License, in which case the provisions of the GPL are
 * required INSTEAD OF the above restrictions.  (This clause is
 * necessary due to a potential bad interaction between the GPL and
 * the restrictions contained in a BSD-style copyright.)
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <malloc.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <pwd.h>
#include <syslog.h>
#include <shadow.h>
#include <time.h>		/* for time() */
#include <fcntl.h>
#include <ctype.h>
#include <sys/time.h>
#include <sys/stat.h>

#include <signal.h>
#include <sys/wait.h>
#include <sys/resource.h>

#include <security/_pam_macros.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/pam_modutil.h>

#include "pam_inline.h"
#include "pam_cc_compat.h"
#include "md5.h"
#include "support.h"
#include "passverify.h"
#include "bigcrypt.h"

#ifdef HAVE_NIS
# include <rpc/rpc.h>
# include <rpcsvc/yp_prot.h>
# include <rpcsvc/ypclnt.h>

# include "yppasswd.h"

# if !defined(HAVE_DECL_GETRPCPORT) &&!defined(HAVE_RPCB_GETADDR)
extern int getrpcport(const char *host, unsigned long prognum,
		      unsigned long versnum, unsigned int proto);
# endif				/* GNU libc 2.1 */
#endif

/*
   How it works:
   Gets in username (has to be done) from the calling program
   Does authentication of user (only if we are not running as root)
   Gets new password/checks for sanity
   Sets it.
 */

#define MAX_PASSWD_TRIES	3

#ifdef HAVE_NIS
#ifdef HAVE_RPCB_GETADDR
static unsigned short
__taddr2port (const struct netconfig *nconf, const struct netbuf *nbuf)
{
  unsigned short port = 0;
  struct __rpc_sockinfo si;
  struct sockaddr_in *sin;
  struct sockaddr_in6 *sin6;
  if (!__rpc_nconf2sockinfo(nconf, &si))
    return 0;

  switch (si.si_af)
    {
    case AF_INET:
      sin = nbuf->buf;
      port = sin->sin_port;
      break;
    case AF_INET6:
      sin6 = nbuf->buf;
      port = sin6->sin6_port;
      break;
    default:
      break;
    }

  return htons (port);
}
#endif

static char *getNISserver(pam_handle_t *pamh, unsigned long long ctrl)
{
	char *master;
	char *domainname;
	int port, err;
#if defined(HAVE_RPCB_GETADDR)
	struct netconfig *nconf;
	struct netbuf svcaddr;
	char addrbuf[INET6_ADDRSTRLEN];
	void *handle;
	int found;
#endif


#ifdef HAVE_YP_GET_DEFAULT_DOMAIN
	if ((err = yp_get_default_domain(&domainname)) != 0) {
		pam_syslog(pamh, LOG_WARNING, "can't get local yp domain: %s",
			 yperr_string(err));
		return NULL;
	}
#elif defined(HAVE_GETDOMAINNAME)
	char domainname_res[256];

	if (getdomainname (domainname_res, sizeof (domainname_res)) == 0)
	  {
	    if (strcmp (domainname_res, "(none)") == 0)
	      {
		/* If domainname is not set, some systems will return "(none)" */
		domainname_res[0] = '\0';
	      }
	    domainname = domainname_res;
	  }
	else domainname = NULL;
#endif

	if ((err = yp_master(domainname, "passwd.byname", &master)) != 0) {
		pam_syslog(pamh, LOG_WARNING, "can't find the master ypserver: %s",
			 yperr_string(err));
		return NULL;
	}
#ifdef HAVE_RPCB_GETADDR
	svcaddr.len = 0;
	svcaddr.maxlen = sizeof (addrbuf);
	svcaddr.buf = addrbuf;
	port = 0;
	found = 0;

	handle = setnetconfig();
	while ((nconf = getnetconfig(handle)) != NULL) {
	  if (!strcmp(nconf->nc_proto, "udp")) {
	    if (rpcb_getaddr(YPPASSWDPROG, YPPASSWDPROC_UPDATE,
			     nconf, &svcaddr, master)) {
              port = __taddr2port (nconf, &svcaddr);
              endnetconfig (handle);
              found=1;
              break;
            }

	    if (rpc_createerr.cf_stat != RPC_UNKNOWNHOST) {
	      clnt_pcreateerror (master);
              pam_syslog (pamh, LOG_ERR,
			  "rpcb_getaddr (%s) failed!", master);
              return NULL;
            }
	  }
	}

	if (!found) {
	  pam_syslog (pamh, LOG_ERR,
		      "Cannot find suitable transport for protocol 'udp'");
	  return NULL;
	}
#else
	port = getrpcport(master, YPPASSWDPROG, YPPASSWDPROC_UPDATE, IPPROTO_UDP);
#endif
	if (port == 0) {
		pam_syslog(pamh, LOG_WARNING,
		         "yppasswdd not running on NIS master host");
		return NULL;
	}
	if (port >= IPPORT_RESERVED) {
		pam_syslog(pamh, LOG_WARNING,
		         "yppasswd daemon running on illegal port");
		return NULL;
	}
	if (on(UNIX_DEBUG, ctrl)) {
	  pam_syslog(pamh, LOG_DEBUG, "Use NIS server on %s with port %d",
		     master, port);
	}
	return master;
}
#endif

#ifdef WITH_SELINUX

static int _unix_run_update_binary(pam_handle_t *pamh, unsigned long long ctrl, const char *user,
    const char *fromwhat, const char *towhat, int remember)
{
    int retval, child, fds[2];
    struct sigaction newsa, oldsa;

    D(("called."));
    /* create a pipe for the password */
    if (pipe(fds) != 0) {
	D(("could not make pipe"));
	return PAM_AUTH_ERR;
    }

    if (off(UNIX_NOREAP, ctrl)) {
	/*
	 * This code arranges that the demise of the child does not cause
	 * the application to receive a signal it is not expecting - which
	 * may kill the application or worse.
	 *
	 * The "noreap" module argument is provided so that the admin can
	 * override this behavior.
	 */
        memset(&newsa, '\0', sizeof(newsa));
        newsa.sa_handler = SIG_DFL;
        sigaction(SIGCHLD, &newsa, &oldsa);
    }

    /* fork */
    child = fork();
    if (child == 0) {
	static char *envp[] = { NULL };
	const char *args[] = { NULL, NULL, NULL, NULL, NULL, NULL };
        char buffer[16];

	/* XXX - should really tidy up PAM here too */

	/* reopen stdin as pipe */
	if (dup2(fds[0], STDIN_FILENO) != STDIN_FILENO) {
		pam_syslog(pamh, LOG_ERR, "dup2 of %s failed: %m", "stdin");
		_exit(PAM_AUTHINFO_UNAVAIL);
	}

	if (pam_modutil_sanitize_helper_fds(pamh, PAM_MODUTIL_IGNORE_FD,
					    PAM_MODUTIL_PIPE_FD,
					    PAM_MODUTIL_PIPE_FD) < 0) {
		_exit(PAM_AUTHINFO_UNAVAIL);
	}

	/* exec binary helper */
	args[0] = UPDATE_HELPER;
	args[1] = user;
	args[2] = "update";
	if (on(UNIX_SHADOW, ctrl))
		args[3] = "1";
	else
		args[3] = "0";

        snprintf(buffer, sizeof(buffer), "%d", remember);
        args[4] = buffer;

	DIAG_PUSH_IGNORE_CAST_QUAL;
	execve(UPDATE_HELPER, (char *const *) args, envp);
	DIAG_POP_IGNORE_CAST_QUAL;

	/* should not get here: exit with error */
	D(("helper binary is not available"));
	_exit(PAM_AUTHINFO_UNAVAIL);
    } else if (child > 0) {
	/* wait for child */
	/* if the stored password is NULL */
        int rc=0;
	if (fromwhat) {
	    int len = strlen(fromwhat);

	    if (len > PAM_MAX_RESP_SIZE)
	      len = PAM_MAX_RESP_SIZE;
	    pam_modutil_write(fds[1], fromwhat, len);
	}
        pam_modutil_write(fds[1], "", 1);
	if (towhat) {
	    int len = strlen(towhat);

	    if (len > PAM_MAX_RESP_SIZE)
	      len = PAM_MAX_RESP_SIZE;
	    pam_modutil_write(fds[1], towhat, len);
        }
        pam_modutil_write(fds[1], "", 1);

	close(fds[0]);       /* close here to avoid possible SIGPIPE above */
	close(fds[1]);
	/* wait for helper to complete: */
	while ((rc=waitpid(child, &retval, 0)) < 0 && errno == EINTR);
	if (rc<0) {
	  pam_syslog(pamh, LOG_ERR, "unix_update waitpid failed: %m");
	  retval = PAM_AUTHTOK_ERR;
	} else if (!WIFEXITED(retval)) {
          pam_syslog(pamh, LOG_ERR, "unix_update abnormal exit: %d", retval);
          retval = PAM_AUTHTOK_ERR;
        } else {
	  retval = WEXITSTATUS(retval);
	}
    } else {
	D(("fork failed"));
	close(fds[0]);
	close(fds[1]);
	retval = PAM_AUTH_ERR;
    }

    if (off(UNIX_NOREAP, ctrl)) {
        sigaction(SIGCHLD, &oldsa, NULL);   /* restore old signal handler */
    }

    return retval;
}
#endif

static int check_old_password(const char *forwho, const char *newpass)
{
	static char buf[16384];
	char *s_pas;
	int retval = PAM_SUCCESS;
	FILE *opwfile;
	size_t len = strlen(forwho);

	opwfile = fopen(OLD_PASSWORDS_FILE, "r");
	if (opwfile == NULL)
		return PAM_ABORT;

	while (fgets(buf, 16380, opwfile)) {
		if (!strncmp(buf, forwho, len) && (buf[len] == ':' ||
			buf[len] == ',')) {
			char *sptr;
			buf[strlen(buf) - 1] = '\0';
			/* s_luser = */ strtok_r(buf, ":,", &sptr);
			/* s_uid = */ strtok_r(NULL, ":,", &sptr);
			/* s_npas = */ strtok_r(NULL, ":,", &sptr);
			s_pas = strtok_r(NULL, ":,", &sptr);
			while (s_pas != NULL) {
				char *md5pass = Goodcrypt_md5(newpass, s_pas);
				if (md5pass == NULL || !strcmp(md5pass, s_pas)) {
					_pam_delete(md5pass);
					retval = PAM_AUTHTOK_ERR;
					break;
				}
				s_pas = strtok_r(NULL, ":,", &sptr);
				_pam_delete(md5pass);
			}
			break;
		}
	}
	fclose(opwfile);

	return retval;
}

static int _do_setpass(pam_handle_t* pamh, const char *forwho,
		       const char *fromwhat,
		       char *towhat, unsigned long long ctrl, int remember)
{
	struct passwd *pwd = NULL;
	int retval = 0;
	int unlocked = 0;

	D(("called"));

	pwd = getpwnam(forwho);

	if (pwd == NULL) {
		retval = PAM_AUTHTOK_ERR;
		goto done;
	}

	if (on(UNIX_NIS, ctrl) && _unix_comesfromsource(pamh, forwho, 0, 1)) {
#ifdef HAVE_NIS
	  char *master;

	  if ((master=getNISserver(pamh, ctrl)) != NULL) {
		struct timeval timeout;
		struct yppasswd yppwd;
		CLIENT *clnt;
		int status;
		enum clnt_stat err;

		/* Unlock passwd file to avoid deadlock */
		unlock_pwdf();
		unlocked = 1;

		/* Initialize password information */
		yppwd.newpw.pw_passwd = pwd->pw_passwd;
		yppwd.newpw.pw_name = pwd->pw_name;
		yppwd.newpw.pw_uid = pwd->pw_uid;
		yppwd.newpw.pw_gid = pwd->pw_gid;
		yppwd.newpw.pw_gecos = pwd->pw_gecos;
		yppwd.newpw.pw_dir = pwd->pw_dir;
		yppwd.newpw.pw_shell = pwd->pw_shell;
		yppwd.oldpass = fromwhat ? strdup (fromwhat) : strdup ("");
		yppwd.newpw.pw_passwd = towhat;

		D(("Set password %s for %s", yppwd.newpw.pw_passwd, forwho));

		/* The yppasswd.x file said `unix authentication required',
		 * so I added it. This is the only reason it is in here.
		 * My yppasswdd doesn't use it, but maybe some others out there
		 * do.                                        --okir
		 */
		clnt = clnt_create(master, YPPASSWDPROG, YPPASSWDVERS, "udp");
		clnt->cl_auth = authunix_create_default();
		memset((char *) &status, '\0', sizeof(status));
		timeout.tv_sec = 25;
		timeout.tv_usec = 0;
		err = clnt_call(clnt, YPPASSWDPROC_UPDATE,
				(xdrproc_t) xdr_yppasswd, (char *) &yppwd,
				(xdrproc_t) xdr_int, (char *) &status,
				timeout);

		free (yppwd.oldpass);

		if (err) {
			_make_remark(pamh, ctrl, PAM_TEXT_INFO,
				clnt_sperrno(err));
		} else if (status) {
			D(("Error while changing NIS password.\n"));
		}
		D(("The password has%s been changed on %s.",
		   (err || status) ? " not" : "", master));
		pam_syslog(pamh, LOG_NOTICE, "password%s changed for %s on %s",
			 (err || status) ? " not" : "", pwd->pw_name, master);

		auth_destroy(clnt->cl_auth);
		clnt_destroy(clnt);
		if (err || status) {
			_make_remark(pamh, ctrl, PAM_TEXT_INFO,
				_("NIS password could not be changed."));
			retval = PAM_TRY_AGAIN;
		}
#ifdef PAM_DEBUG
		sleep(5);
#endif
	    } else {
		    retval = PAM_TRY_AGAIN;
	    }
#else
          if (on(UNIX_DEBUG, ctrl)) {
            pam_syslog(pamh, LOG_DEBUG, "No NIS support available");
          }

          retval = PAM_TRY_AGAIN;
#endif
	}

	if (_unix_comesfromsource(pamh, forwho, 1, 0)) {
		if(unlocked) {
			if (lock_pwdf() != PAM_SUCCESS) {
				return PAM_AUTHTOK_LOCK_BUSY;
			}
		}
#ifdef WITH_SELINUX
	        if (unix_selinux_confined())
			  return _unix_run_update_binary(pamh, ctrl, forwho, fromwhat, towhat, remember);
#endif
		/* first, save old password */
		if (save_old_password(pamh, forwho, fromwhat, remember)) {
			retval = PAM_AUTHTOK_ERR;
			goto done;
		}
		if (on(UNIX_SHADOW, ctrl) || is_pwd_shadowed(pwd)) {
			retval = unix_update_shadow(pamh, forwho, towhat);
			if (retval == PAM_SUCCESS)
				if (!is_pwd_shadowed(pwd))
					retval = unix_update_passwd(pamh, forwho, "x");
		} else {
			retval = unix_update_passwd(pamh, forwho, towhat);
		}
	}


done:
	unlock_pwdf();

	return retval;
}

static int _unix_verify_shadow(pam_handle_t *pamh, const char *user, unsigned long long ctrl)
{
	struct passwd *pwent = NULL;	/* Password and shadow password */
	struct spwd *spent = NULL;	/* file entries for the user */
	int daysleft;
	int retval;

	retval = get_account_info(pamh, user, &pwent, &spent);
	if (retval == PAM_USER_UNKNOWN) {
		return retval;
	}

	if (retval == PAM_SUCCESS && spent == NULL)
		return PAM_SUCCESS;

	if (retval == PAM_UNIX_RUN_HELPER) {
		retval = _unix_run_verify_binary(pamh, ctrl, user, &daysleft);
		if (retval == PAM_AUTH_ERR || retval == PAM_USER_UNKNOWN)
			return retval;
	}
	else if (retval == PAM_SUCCESS)
		retval = check_shadow_expiry(pamh, spent, &daysleft);

	if (on(UNIX__IAMROOT, ctrl) || retval == PAM_NEW_AUTHTOK_REQD)
		return PAM_SUCCESS;

	return retval;
}

static int _pam_unix_approve_pass(pam_handle_t * pamh
				  ,unsigned long long ctrl
				  ,const char *pass_old
				  ,const char *pass_new,
                                  int pass_min_len)
{
	const void *user;
	const char *remark = NULL;
	int retval = PAM_SUCCESS;

	D(("&new=%p, &old=%p", pass_old, pass_new));
	D(("new=[%s]", pass_new));
	D(("old=[%s]", pass_old));

	if (pass_new == NULL || (pass_old && !strcmp(pass_old, pass_new))) {
		if (on(UNIX_DEBUG, ctrl)) {
			pam_syslog(pamh, LOG_DEBUG, "bad authentication token");
		}
		_make_remark(pamh, ctrl, PAM_ERROR_MSG, pass_new == NULL ?
			_("No password has been supplied.") :
			_("The password has not been changed."));
		return PAM_AUTHTOK_ERR;
	}
	/*
	 * if one wanted to hardwire authentication token strength
	 * checking this would be the place - AGM
	 */

	retval = pam_get_item(pamh, PAM_USER, &user);
	if (retval != PAM_SUCCESS) {
		if (on(UNIX_DEBUG, ctrl)) {
			pam_syslog(pamh, LOG_ERR, "Can not get username");
			return PAM_AUTHTOK_ERR;
		}
	}

	if (strlen(pass_new) > PAM_MAX_RESP_SIZE) {
		remark = _("You must choose a shorter password.");
		D(("length exceeded [%s]", remark));
	} else if (off(UNIX__IAMROOT, ctrl)) {
		if ((int)strlen(pass_new) < pass_min_len)
		  remark = _("You must choose a longer password.");
		D(("length check [%s]", remark));
		if (on(UNIX_REMEMBER_PASSWD, ctrl)) {
			if ((retval = check_old_password(user, pass_new)) == PAM_AUTHTOK_ERR)
			  remark = _("Password has been already used. Choose another.");
			if (retval == PAM_ABORT) {
				pam_syslog(pamh, LOG_ERR, "can't open %s file to check old passwords",
					OLD_PASSWORDS_FILE);
				return retval;
			}
		}
	}
	if (remark) {
		_make_remark(pamh, ctrl, PAM_ERROR_MSG, remark);
		retval = PAM_AUTHTOK_ERR;
	}
	return retval;
}

int
pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
	unsigned long long ctrl, lctrl;
	int retval;
	int remember = -1;
	int rounds = 0;
	int pass_min_len = 0;

	/* <DO NOT free() THESE> */
	const char *user;
	const void *item;
	const char *pass_old, *pass_new;
	/* </DO NOT free() THESE> */

	D(("called."));

	ctrl = _set_ctrl(pamh, flags, &remember, &rounds, &pass_min_len,
	                 argc, argv);

	/*
	 * First get the name of a user
	 */
	retval = pam_get_user(pamh, &user, NULL);
	if (retval == PAM_SUCCESS) {
		/*
		 * Various libraries at various times have had bugs related to
		 * '+' or '-' as the first character of a user name. Don't
		 * allow them.
		 */
		if (user[0] == '-' || user[0] == '+') {
			pam_syslog(pamh, LOG_NOTICE, "bad username [%s]", user);
			return PAM_USER_UNKNOWN;
		}
		if (retval == PAM_SUCCESS && on(UNIX_DEBUG, ctrl))
			pam_syslog(pamh, LOG_DEBUG, "username [%s] obtained",
			         user);
	} else {
		if (on(UNIX_DEBUG, ctrl))
			pam_syslog(pamh, LOG_DEBUG,
			         "password - could not identify user");
		return retval;
	}

	D(("Got username of %s", user));

	/*
	 * Before we do anything else, check to make sure that the user's
	 * info is in one of the databases we can modify from this module,
	 * which currently is 'files' and 'nis'.  We have to do this because
	 * getpwnam() doesn't tell you *where* the information it gives you
	 * came from, nor should it.  That's our job.
	 */
	if (_unix_comesfromsource(pamh, user, 1, on(UNIX_NIS, ctrl)) == 0) {
		pam_syslog(pamh, LOG_DEBUG,
			 "user \"%s\" does not exist in /etc/passwd%s",
			 user, on(UNIX_NIS, ctrl) ? " or NIS" : "");
		return PAM_USER_UNKNOWN;
	} else {
		struct passwd *pwd;
		_unix_getpwnam(pamh, user, 1, 1, &pwd);
		if (pwd == NULL) {
			pam_syslog(pamh, LOG_DEBUG,
				"user \"%s\" has corrupted passwd entry",
				user);
			return PAM_USER_UNKNOWN;
		}
	}

	/*
	 * This is not an AUTH module!
	 */
	if (on(UNIX__NONULL, ctrl))
		set(UNIX__NULLOK, ctrl);

	if (on(UNIX__PRELIM, ctrl)) {
		/*
		 * obtain and verify the current password (OLDAUTHTOK) for
		 * the user.
		 */
		D(("prelim check"));

		if (_unix_blankpasswd(pamh, ctrl, user)) {
			return PAM_SUCCESS;
		} else if (off(UNIX__IAMROOT, ctrl) ||
			   (on(UNIX_NIS, ctrl) && _unix_comesfromsource(pamh, user, 0, 1))) {
			/* instruct user what is happening */
			if (off(UNIX__QUIET, ctrl)) {
				retval = pam_info(pamh, _("Changing password for %s."), user);
				if (retval != PAM_SUCCESS)
					return retval;
			}
			retval = pam_get_authtok(pamh, PAM_OLDAUTHTOK, &pass_old, NULL);

			if (retval != PAM_SUCCESS) {
				pam_syslog(pamh, LOG_NOTICE,
				    "password - (old) token not obtained");
				return retval;
			}
			/* verify that this is the password for this user */

			retval = _unix_verify_password(pamh, user, pass_old, ctrl);
		} else {
			D(("process run by root so do nothing this time around"));
			pass_old = NULL;
			retval = PAM_SUCCESS;	/* root doesn't have too */
		}

		if (retval != PAM_SUCCESS) {
			D(("Authentication failed"));
			pass_old = NULL;
			return retval;
		}
		pass_old = NULL;
		retval = _unix_verify_shadow(pamh,user, ctrl);
		if (retval == PAM_AUTHTOK_ERR) {
			if (off(UNIX__IAMROOT, ctrl))
				_make_remark(pamh, ctrl, PAM_ERROR_MSG,
					     _("You must wait longer to change your password."));
			else
				retval = PAM_SUCCESS;
		}
	} else if (on(UNIX__UPDATE, ctrl)) {
		/*
		 * tpass is used below to store the _pam_md() return; it
		 * should be _pam_delete()'d.
		 */

		char *tpass = NULL;
		int retry = 0;

		/*
		 * obtain the proposed password
		 */

		D(("do update"));

		/*
		 * get the old token back. NULL was ok only if root [at this
		 * point we assume that this has already been enforced on a
		 * previous call to this function].
		 */

		retval = pam_get_item(pamh, PAM_OLDAUTHTOK, &item);

		if (retval != PAM_SUCCESS) {
			pam_syslog(pamh, LOG_NOTICE, "user not authenticated");
			return retval;
		}
		pass_old = item;
		D(("pass_old [%s]", pass_old));

		D(("get new password now"));

		lctrl = ctrl;

		if (on(UNIX_USE_AUTHTOK, lctrl)) {
			set(UNIX_USE_FIRST_PASS, lctrl);
		}
		if (on(UNIX_USE_FIRST_PASS, lctrl)) {
			retry = MAX_PASSWD_TRIES-1;
		}
		retval = PAM_AUTHTOK_ERR;
		while ((retval != PAM_SUCCESS) && (retry++ < MAX_PASSWD_TRIES)) {
			/*
			 * use_authtok is to force the use of a previously entered
			 * password -- needed for pluggable password strength checking
			 */

			retval = pam_get_authtok(pamh, PAM_AUTHTOK, &pass_new, NULL);

			if (retval != PAM_SUCCESS) {
				if (on(UNIX_DEBUG, ctrl)) {
					pam_syslog(pamh, LOG_ERR,
						 "password - new password not obtained");
				}
				pass_old = NULL;	/* tidy up */
				return retval;
			}
			D(("returned to _unix_chauthtok"));

			/*
			 * At this point we know who the user is and what they
			 * propose as their new password. Verify that the new
			 * password is acceptable.
			 */

			if (*(const char *)pass_new == '\0') {	/* "\0" password = NULL */
				pass_new = NULL;
			}
			retval = _pam_unix_approve_pass(pamh, ctrl, pass_old,
			                                pass_new, pass_min_len);

			if (retval != PAM_SUCCESS) {
				pam_set_item(pamh, PAM_AUTHTOK, NULL);
			}
		}

		if (retval != PAM_SUCCESS) {
			pam_syslog(pamh, LOG_NOTICE,
			         "new password not acceptable");
			pass_new = pass_old = NULL;	/* tidy up */
			return retval;
		}
		if (lock_pwdf() != PAM_SUCCESS) {
			return PAM_AUTHTOK_LOCK_BUSY;
		}

		if (pass_old) {
			retval = _unix_verify_password(pamh, user, pass_old, ctrl);
			if (retval != PAM_SUCCESS) {
				pam_syslog(pamh, LOG_NOTICE, "user password changed by another process");
				unlock_pwdf();
				return retval;
			}
		}

		retval = _unix_verify_shadow(pamh, user, ctrl);
		if (retval != PAM_SUCCESS) {
			pam_syslog(pamh, LOG_NOTICE, "user shadow entry expired");
			unlock_pwdf();
			return retval;
		}

		retval = _pam_unix_approve_pass(pamh, ctrl, pass_old, pass_new,
		                                pass_min_len);
		if (retval != PAM_SUCCESS) {
			pam_syslog(pamh, LOG_NOTICE,
			         "new password not acceptable 2");
			pass_new = pass_old = NULL;	/* tidy up */
			unlock_pwdf();
			return retval;
		}

		/*
		 * By reaching here we have approved the passwords and must now
		 * rebuild the password database file.
		 */

		/*
		 * First we encrypt the new password.
		 */

		tpass = create_password_hash(pamh, pass_new, ctrl, rounds);
		if (tpass == NULL) {
			pam_syslog(pamh, LOG_CRIT,
				"crypt() failure or out of memory for password");
			pass_new = pass_old = NULL;	/* tidy up */
			unlock_pwdf();
			return PAM_BUF_ERR;
		}

		D(("password processed"));

		/* update the password database(s) -- race conditions..? */

		retval = _do_setpass(pamh, user, pass_old, tpass, ctrl,
		                     remember);
	        /* _do_setpass has called unlock_pwdf for us */

		_pam_delete(tpass);
		pass_old = pass_new = NULL;
	} else {		/* something has broken with the module */
		pam_syslog(pamh, LOG_CRIT,
		         "password received unknown request");
		retval = PAM_ABORT;
	}

	D(("retval was %d", retval));

	return retval;
}
