Blob Blame History Raw
/*
 * Copyright Red Hat, Inc., 1998, 1999, 2001, 2002, 2015.
 *
 * 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.
 */

/* Written by Cristian Gafton <gafton@redhat.com> */

#include "config.h"

#include <sys/types.h>

#include <ctype.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <popt.h>
#include <errno.h>
#include <limits.h>
#include <syslog.h>
#include <locale.h>
#include <libintl.h>
#include "pwdb.h"

#ifdef WITH_SELINUX
#include "selinux_utils.h"
#else
#define selinux_init(x) 0
#define selinux_check_root() 0
#endif

#ifdef WITH_AUDIT
#include <libaudit.h>
#else
#define audit_log_acct_message(d,ty,p,o,n,i,h,a,t,r) do { ; } while(0) 
static int audit_open(void) { errno = EPROTONOSUPPORT; return -1; } 
#endif

#include <security/pam_appl.h>
#include <security/pam_misc.h>

static int audit_fd = -1;

/* conversation function & corresponding structure */
static struct pam_conv conv = {
	misc_conv,
	NULL
};

const char *username = NULL;	/* username specified on the command line */
const char *progname = NULL;	/* the name of the program */
int passwd_flags = 0;		/* flags specified by root */

#define PASSWD_KEEP	0x0001	/* keep un-expired tokens */

#define PASSWD_LOCK	0x0002	/* lock the password */
#define PASSWD_UNLOCK	0x0004	/* unlock the password, if locked */
#define PASSWD_DELETE	0x0008	/* delete the user's password */
#define PASSWD_STATUS	0x0010	/* report the password status */
#define PASSWD_FORCE	0x0020	/* force change of expired token */
#define PASSWD_STDIN	0x0040	/* read the password from stdin (root only) */
#define PASSWD_EXPIRE	0x0080	/* expire the password */
#define PASSWD_ROOT	0x009E	/* options which are mutually exclusive */

#define PASSWD_MIN	0x0100	/* set the minimum password lifetime */
#define PASSWD_MAX	0x0200	/* set the maximum password lifetime */
#define PASSWD_WARN	0x0400	/* set the password warning */
#define PASSWD_INACT	0x0800	/* set the inactive time */
#define PASSWD_AGING	0x0F00	/* aging options */

#ifdef HAVE_PAM_FAIL_DELAY
#define PASSWD_FAIL_DELAY	2000000	/* usec delay on failure */
#endif


/* A conversation function which uses an internally-stored value for
 * the responses. */
static int
stdin_conv(int num_msg, const struct pam_message **msgm,
	   struct pam_response **response, void *appdata_ptr)
{
	struct pam_response *reply;
	int count;

	/* Sanity test. */
	if (num_msg <= 0) {
		return PAM_CONV_ERR;
	}

	/* Allocate memory for the responses. */
	reply = calloc(num_msg, sizeof(struct pam_response));
	if (reply == NULL) {
		return PAM_CONV_ERR;
	}

	/* Each prompt elicits the same response. */
	for (count = 0; count < num_msg; ++count) {
		if (msgm[count]->msg_style == PAM_PROMPT_ECHO_OFF) {
			reply[count].resp_retcode = 0;
			reply[count].resp = strdup(appdata_ptr);
		} else {
			reply[count].resp_retcode = 0;
			reply[count].resp = strdup("");
		}
	}

	/* Set the pointers in the response structure and return. */
	*response = reply;
	return PAM_SUCCESS;
}

/* Parse command-line arguments, rejecting conflicting flags and performing
 * various other initialization tasks. */
static void
parse_args(int argc, const char **argv,
	   long *min, long *max, long *warn, long *inact)
{
	poptContext optCon;
	int delete = 0, force = 0, keep = 0, lock = 0, status = 0, unlock = 0;
	int expire = 0;
	int use_stdin = 0;
	int rc;
	const char **extraArgs;
	struct poptOption options[] = {
		{"keep-tokens", 'k', POPT_ARG_NONE, &keep, 0,
		 _("keep non-expired authentication tokens"), NULL},
		{"delete", 'd', POPT_ARG_NONE, &delete, 0,
		 _("delete the password for the named account (root only); "
		 "also removes password lock if any"), NULL},
		{"lock", 'l', POPT_ARG_NONE, &lock, 0,
		 _("lock the password for the named account (root only)"),
		 NULL},
		{"unlock", 'u', POPT_ARG_NONE, &unlock, 0,
		 _("unlock the password for the named account (root only)"),
		 NULL},
		{"expire", 'e', POPT_ARG_NONE, &expire, 0,
		 _("expire the password for the named account (root only)"),
		 NULL},
		{"force", 'f', POPT_ARG_NONE, &force, 0,
		 _("force operation"), NULL},
		{"maximum", 'x', POPT_ARG_LONG, max, 0,
		 _("maximum password lifetime (root only)"), "DAYS"},
		{"minimum", 'n', POPT_ARG_LONG, min, 0,
		 _("minimum password lifetime (root only)"), "DAYS"},
		{"warning", 'w', POPT_ARG_LONG, warn, 0,
		 _("number of days warning users receives before password "
		 "expiration (root only)"), "DAYS"},
		{"inactive", 'i', POPT_ARG_LONG, inact, 0,
		 _("number of days after password expiration when an account "
		 "becomes disabled (root only)"), "DAYS"},
		{"status", 'S', POPT_ARG_NONE, &status, 0,
		 _("report password status on the named account (root only)"),
		 NULL},
		{"stdin", '\0', POPT_ARG_NONE, &use_stdin, 0,
		 _("read new tokens from stdin (root only)"), NULL},
		POPT_AUTOHELP POPT_TABLEEND
	};
	struct passwd *pw;

	*min = *max = *warn = *inact = -2;
	optCon = poptGetContext("passwd", argc, argv, options, 0);
	poptSetOtherOptionHelp(optCon, _("[OPTION...] <accountName>"));

	if ((rc = poptGetNextOpt(optCon)) < -1) {
		fprintf(stderr, _("%s: bad argument %s: %s\n"), progname,
			poptBadOption(optCon, POPT_BADOPTION_NOALIAS),
			poptStrerror(rc));
		exit(-3);
	}

	extraArgs = poptGetArgs(optCon);

	if (keep) {
		passwd_flags |= PASSWD_KEEP;
	}
	if (lock) {
		passwd_flags |= PASSWD_LOCK;
	}
	if (unlock) {
		passwd_flags |= PASSWD_UNLOCK;
	}
	if (expire) {
		passwd_flags |= PASSWD_EXPIRE;
	}
	if (status) {
		passwd_flags |= PASSWD_STATUS;
	}
	if (delete) {
		passwd_flags |= PASSWD_DELETE;
	}
	if (force) {
		passwd_flags |= PASSWD_FORCE;
	}
	if (use_stdin) {
		passwd_flags |= PASSWD_STDIN;
	}
	if (*min != -2) {
		passwd_flags |= PASSWD_MIN;
	}
	if (*max != -2) {
		passwd_flags |= PASSWD_MAX;
	}
	if (*warn != -2) {
		passwd_flags |= PASSWD_WARN;
	}
	if (*inact != -2) {
		passwd_flags |= PASSWD_INACT;
	}

	/* The rest of the flags are mutually-exclusive, except for --force. */
	if (passwd_flags) {
		int tmp;
		int count;
		tmp = passwd_flags & PASSWD_ROOT;
		count = 0;
		/* Check the rightmost bit and shift right. */
		while (tmp != 0) {
			if (tmp & 0x01) {
				count++;
			}
			tmp = tmp >> 1;
		}
		/* Error if other bits are set. */
		if (count > 1) {
			fprintf(stderr,
				_("%s: Only one of -l, -u, -d, -S may be specified.\n"),
				progname);
			exit(-2);
		}
		/* Error out if we had -l/-u/-d/-S and an aging option. */
		if (count > 0) {
			tmp = passwd_flags & PASSWD_AGING;
			if (tmp != 0) {
				fprintf(stderr,
					_("%s: Cannot mix one of -l, -u, -d, -S and one of -i, -n, -w, -x.\n"),
					progname);
				exit(-2);
			}
		}
	}

	/* The only flag which unprivileged users get to use is -k. */
	if ((passwd_flags & ~PASSWD_KEEP) && 
	    (getuid() != 0)) {
		/* Auditing is not needed for displaying status */
		if (passwd_flags != PASSWD_STATUS) {
			audit_log_acct_message(audit_fd,  AUDIT_USER_CHAUTHTOK,
				NULL, "attempted-to-change-password-attribute",
				NULL, getuid(), NULL, NULL, NULL, 0);
		}
		fprintf(stderr, _("Only root can do that.\n"));
		exit(-2);
	}

	/* Only root gets to specify a user name. */
	username = NULL;
	if ((extraArgs != NULL) && (extraArgs[0] != NULL)) {
		if (getuid() != 0) {
			/* The invoking user was not root. */
			audit_log_acct_message(audit_fd,  AUDIT_USER_CHAUTHTOK,
				NULL, "attempted-to-change-password",
				extraArgs[0], getuid(), NULL, NULL, NULL, 0);
			fprintf(stderr,
				_("%s: Only root can specify a user name.\n"),
				progname);
			exit(-3);
		} else {
			/* The invoking user was root. */
			username = extraArgs[0];
			/* Sanity-check the user name */
			if (strlen(username) > MAX_USERNAMESIZE) {
				fprintf(stderr,
					_("%s: The user name supplied is too long.\n"),
					progname);
				exit(-3);
			}
		}

		/* If there is more than one unrecognized argument, we suddenly
		 * get confused. */
		if (extraArgs[1] != NULL) {
			fprintf(stderr,
				_("%s: Only one user name may be specified.\n"),
				progname);
			exit(-3);
		}
	}

	/* Now if any of the -l, -u, -d, -S, -i, -n, -w, or -x options were
	 * given, and a username was not specified, bail out. */
	if ((passwd_flags & ~PASSWD_KEEP) && (username == NULL)) {
		fprintf(stderr,
			_("%s: This option requires a user name.\n"),
			progname);
		exit(-2);
	}

	/* Determine the name of the user whose account we're operating on,
	 * and make sure the account exists. */
	if (username == NULL) {
		/* The invoking user. */
		pw = getpwuid(getuid());
		if (pw == NULL) {
			fprintf(stderr, _("%s: Can not identify you!\n"),
				progname);
			exit(-3);
		}
		username = strdup(pw->pw_name);
	} else {
		/* The name specified on the command-line. */
		pw = getpwnam(username);
		if (pw == NULL) {
			fprintf(stderr, _("%s: Unknown user name '%s'.\n"),
				progname, username);
			exit(-4);
		}
	}
}

int
main(int argc, const char **argv)
{
	int retval;
	long min, max, warn, inact;
	pam_handle_t *pamh = NULL;
	struct passwd *pwd;
	char *tty_name, *ttyn;

	setlocale(LC_ALL, "");
	bindtextdomain("passwd", "/usr/share/locale");
	textdomain("passwd");
	
	audit_fd = audit_open();
	if (audit_fd < 0 && !(errno == EINVAL || errno == EPROTONOSUPPORT ||
				errno == EAFNOSUPPORT)) {
		/* The above error codes are only given when the kernel doesn't
		 * have audit compiled in. */
		fprintf(stderr, "Error - unable to connect to audit system\n");
		exit(1);
	}

	/* Parse command-line arguments. */
	progname = basename(argv[0]);
	parse_args(argc, argv, &min, &max, &warn, &inact);

	pwd = getpwnam(username);
	if (pwd == NULL) {
		fprintf(stderr, _("%s: Unknown user name '%s'.\n"),
			progname, username);
		exit(-4);
	}

	if (audit_fd >= 0)
		selinux_init(audit_fd);

	if (selinux_check_root() != 0) {
		fprintf(stderr, _("%s: SELinux denying access due to security policy.\n"), progname);
		
		audit_log_acct_message(audit_fd,  AUDIT_USER_CHAUTHTOK,
			NULL, "attempted-to-change-password", NULL, pwd->pw_uid,
			NULL, NULL, NULL, 0);
		exit(1);
	}

	/* Handle account locking request. */
	if (passwd_flags & PASSWD_LOCK) {
		printf(_("Locking password for user %s.\n"), username);
		retval = pwdb_lock_password(username);
		printf("%s: %s\n", progname,
		       retval ==
		       0 ? _("Success") : _("Error (password not set?)"));
		audit_log_acct_message(audit_fd, AUDIT_ACCT_LOCK,
			NULL, "locked-password", NULL, pwd->pw_uid,
			NULL, NULL, NULL, retval == 0);
		return retval;
	}
	/* Handle account unlocking request. */
	if (passwd_flags & PASSWD_UNLOCK) {
		printf(_("Unlocking password for user %s.\n"), username);
		retval = pwdb_unlock_password(username,
					      passwd_flags & PASSWD_FORCE);
		printf("%s: %s\n", progname,
		       retval == 0 ? _("Success") :
		       retval ==
		       -2 ? _("Unsafe operation (use -f to force)") :
		       _("Error (password not set?)"));
		audit_log_acct_message(audit_fd, AUDIT_ACCT_UNLOCK,
			NULL, "unlocked-password", NULL, pwd->pw_uid,
			NULL, NULL, NULL, retval == 0);
		return retval;
	}
	/* Handle password expiration request. */
	if (passwd_flags & PASSWD_EXPIRE) {
		printf(_("Expiring password for user %s.\n"), username);
		retval = pwdb_update_aging(username, -2, -2, -2, -2, 0);
		printf("%s: %s\n", progname,
		       retval ==
		       0 ? _("Success") : _("Error"));
		audit_log_acct_message(audit_fd, AUDIT_USER_MGMT,
			NULL, "expired-password", NULL, pwd->pw_uid,
			NULL, NULL, NULL, retval == 0);
		return retval;
	}
	/* Handle password clearing request. */
	if (passwd_flags & PASSWD_DELETE) {
		printf(_("Removing password for user %s.\n"), username);
		retval = pwdb_clear_password(username);
		printf("%s: %s\n", progname,
		       (retval == 0) ? _("Success") : _("Error"));
		audit_log_acct_message(audit_fd,  AUDIT_USER_CHAUTHTOK,
			NULL, "deleted-password", NULL, pwd->pw_uid,
			NULL, NULL, NULL, retval == 0);
		return retval;
	}
	/* Display account status. */
	if (passwd_flags & PASSWD_STATUS) {
		/* Auditing is not needed for displaying status */
		retval = pwdb_display_status(username);
		return retval;
	}
	/* Adjust aging parameters. */
	if (passwd_flags & PASSWD_AGING) {
		char aubuf[PATH_MAX];
		printf(_("Adjusting aging data for user %s.\n"), username);
		retval = pwdb_update_aging(username, min, max, warn, inact, -2);
		printf("%s: %s\n", progname,
		       (retval == 0) ? _("Success") : _("Error"));
		snprintf(aubuf, sizeof(aubuf), "changed-password-aging"
				" min=%li max=%li warn=%li inact=%li",
				min, max, warn, inact);
		audit_log_acct_message(audit_fd,  AUDIT_USER_MGMT,
			NULL, aubuf, NULL, pwd->pw_uid,
			NULL, NULL, NULL, retval == 0);
		return retval;
	}

	/* The standard behavior follows.  At this point we know for whom
	 * we are going to change a password, so let the invoking user
	 * know what's going on. */
	printf(_("Changing password for user %s.\n"), username);

	/* If we need to read the new password from stdin, read it and switch
	 * to the really-quiet stdin conversation function. */
	if (passwd_flags & PASSWD_STDIN) {
		/* PAM's documentation says that PAM_MAX_RESP_SIZE is the
		 * maximum supported length of the password, but in practice
		 * the code (including examples in the OSF RFC) often truncates
		 * data at PAM_MAX_RESP_SIZE - 1. So, refuse to use anything
		 * longer than PAM_MAX_RESP_SIZE - 1, to prevent users from
		 * setting a password they won't be able to use to log in. */
		char *ptr, newPassword[PAM_MAX_RESP_SIZE];
		int i;

		i = read(STDIN_FILENO, newPassword,
			 sizeof(newPassword));
		if (i < 0) {
			fprintf(stderr,
				_("%s: error reading from stdin: %s\n"), progname,
				strerror(errno));
			exit(1);
		}
		if (i == sizeof(newPassword)) {
			if (newPassword[i - 1] != '\n') {
				fprintf(stderr,
					_("%s: password too long, maximum is %zu"),
					progname, sizeof(newPassword) - 1);
				exit(1);
			}
			i--;
		}

		newPassword[i] = '\0';
		ptr = strchr(newPassword, '\n');
		if (ptr)
			*ptr = 0;
		conv.conv = stdin_conv;
		conv.appdata_ptr = strdup(newPassword);
	}

	/* Start up PAM. */
	retval = pam_start("passwd", username, &conv, &pamh);
	if (retval != PAM_SUCCESS) {
		fprintf(stderr,
			_("%s: unable to start pam: %s\n"), progname,
			pam_strerror(pamh, retval));
		exit(1);
	}

	if ((ttyn = ttyname(0)) != NULL) {	
		if (strncmp(ttyn, "/dev/", 5) == 0)
			tty_name = ttyn+5;
		else
			tty_name = ttyn;
		retval = pam_set_item(pamh, PAM_TTY, tty_name);
		if (retval != PAM_SUCCESS) {
			fprintf(stderr,
				_("%s: unable to set tty for pam: %s\n"), progname,
				pam_strerror(pamh, retval));
			pam_end(pamh, retval);
			exit(1);
		}
	}
#ifdef HAVE_PAM_FAIL_DELAY
	/* We have to pause on failure, so tell libpam the minimum amount
	 * of time it should wait after a failure. */
	retval = pam_fail_delay(pamh, PASSWD_FAIL_DELAY);
	if (retval != PAM_SUCCESS) {
		fprintf(stderr, _("%s: unable to set failure delay: %s\n"),
			progname, pam_strerror(pamh, retval));
		exit(1);
	}
#endif

	/* Go for it. Note: pam will send audit event. */
	retval = pam_chauthtok(pamh,
			       (passwd_flags & PASSWD_KEEP) ?
			       PAM_CHANGE_EXPIRED_AUTHTOK : 0);
	if (retval == PAM_SUCCESS) {
		/* We're done.  Tell the invoking user that it worked. */
		retval = pam_end(pamh, PAM_SUCCESS);
		if (passwd_flags & PASSWD_KEEP)
			printf(_("%s: expired authentication tokens updated successfully.\n"),
				progname);
		else
			printf(_("%s: all authentication tokens updated successfully.\n"),
				progname);
		retval = 0;
	} else {
		/* Horrors!  It failed.  Relay the bad news. */
		fprintf(stderr, "%s: %s\n", progname,
			pam_strerror(pamh, retval));
		pam_end(pamh, retval);
		retval = 1;
	}
	return retval;
}