/******************************************************************************
 * A module for Linux-PAM that will cache authentication results, inspired by
 * (and implemented with an eye toward being mixable with) sudo.
 *
 * Copyright (c) 2002 Red Hat, Inc.
 * Written by Nalin Dahyabhai <nalin@redhat.com>
 *
 * 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 <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <utmp.h>
#include <syslog.h>
#include <paths.h>
#ifdef WITH_OPENSSL
#include "hmac_openssl_wrapper.h"
#else
#include "hmacsha1.h"
#endif /* WITH_OPENSSL */

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

/* The default timeout we use is 5 minutes, which matches the sudo default
 * for the timestamp_timeout parameter. */
#define DEFAULT_TIMESTAMP_TIMEOUT (5 * 60)
#define MODULE "pam_timestamp"
#define TIMESTAMPDIR _PATH_VARRUN MODULE
#define TIMESTAMPKEY TIMESTAMPDIR "/_pam_timestamp_key"

/* Various buffers we use need to be at least as large as either PATH_MAX or
 * LINE_MAX, so choose the larger of the two. */
#if (LINE_MAX > PATH_MAX)
#define BUFLEN LINE_MAX
#else
#define BUFLEN PATH_MAX
#endif

#define ROOT_USER 	0
#define ROOT_GROUP 	0

/* Return PAM_SUCCESS if the given directory looks "safe". */
static int
check_dir_perms(pam_handle_t *pamh, const char *tdir)
{
	char scratch[BUFLEN];
	struct stat st;
	int i;
	/* Check that the directory is "safe". */
	if ((tdir == NULL) || (strlen(tdir) == 0)) {
		return PAM_AUTH_ERR;
	}
	/* Iterate over the path, checking intermediate directories. */
	memset(scratch, 0, sizeof(scratch));
	for (i = 0; (tdir[i] != '\0') && (i < (int)sizeof(scratch)); i++) {
		scratch[i] = tdir[i];
		if ((scratch[i] == '/') || (tdir[i + 1] == '\0')) {
			/* We now have the name of a directory in the path, so
			 * we need to check it. */
			if ((lstat(scratch, &st) == -1) && (errno != ENOENT)) {
				pam_syslog(pamh, LOG_ERR,
				       "unable to read `%s': %m",
				       scratch);
				return PAM_AUTH_ERR;
			}
			if (!S_ISDIR(st.st_mode)) {
				pam_syslog(pamh, LOG_ERR,
				       "`%s' is not a directory",
				       scratch);
				return PAM_AUTH_ERR;
			}
			if (S_ISLNK(st.st_mode)) {
				pam_syslog(pamh, LOG_ERR,
				       "`%s' is a symbolic link",
				       scratch);
				return PAM_AUTH_ERR;
			}
			if (st.st_uid != 0) {
				pam_syslog(pamh, LOG_ERR,
				       "`%s' owner UID != 0",
				       scratch);
				return PAM_AUTH_ERR;
			}
			if (st.st_gid != 0) {
				pam_syslog(pamh, LOG_ERR,
				       "`%s' owner GID != 0",
				       scratch);
				return PAM_AUTH_ERR;
			}
			if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
				pam_syslog(pamh, LOG_ERR,
				       "`%s' permissions are lax",
				       scratch);
				return PAM_AUTH_ERR;
			}
		}
	}
	return PAM_SUCCESS;
}

/* Validate a tty pathname as actually belonging to a tty, and return its base
 * name if it's valid. */
static const char *
check_tty(const char *tty)
{
	/* Check that we're not being set up to take a fall. */
	if ((tty == NULL) || (strlen(tty) == 0)) {
		return NULL;
	}
	/* Pull out the meaningful part of the tty's name. */
	if (strchr(tty, '/') != NULL) {
		if (pam_str_skip_prefix(tty, "/dev/") == NULL) {
			/* Make sure the device node is actually in /dev/,
			 * noted by Michal Zalewski. */
			return NULL;
		}
		tty = strrchr(tty, '/') + 1;
	}
	/* Make sure the tty wasn't actually a directory (no basename). */
	if (!strlen(tty) || !strcmp(tty, ".") || !strcmp(tty, "..")) {
		return NULL;
	}
	return tty;
}

/* Determine the right path name for a given user's timestamp. */
static int
format_timestamp_name(char *path, size_t len,
		      const char *timestamp_dir,
		      const char *tty,
		      const char *ruser,
		      const char *user)
{
	if (strcmp(ruser, user) == 0) {
		return snprintf(path, len, "%s/%s/%s", timestamp_dir,
				ruser, tty);
	} else {
		return snprintf(path, len, "%s/%s/%s:%s", timestamp_dir,
				ruser, tty, user);
	}
}

/* Check if a given timestamp date, when compared to a current time, fits
 * within the given interval. */
static int
timestamp_good(time_t then, time_t now, time_t interval)
{
	if (((now >= then) && ((now - then) < interval)) ||
	    ((now < then) && ((then - now) < (2 * interval)))) {
		return PAM_SUCCESS;
	}
	return PAM_AUTH_ERR;
}

static int
check_login_time(const char *ruser, time_t timestamp)
{
	struct utmp utbuf, *ut;
	time_t oldest_login = 0;

	setutent();
	while(
#ifdef HAVE_GETUTENT_R
	      !getutent_r(&utbuf, &ut)
#else
	      (ut = getutent()) != NULL
#endif
	      ) {
		if (ut->ut_type != USER_PROCESS) {
			continue;
		}
		if (strncmp(ruser, ut->ut_user, sizeof(ut->ut_user)) != 0) {
			continue;
		}
		if (oldest_login == 0 || oldest_login > ut->ut_tv.tv_sec) {
			oldest_login = ut->ut_tv.tv_sec;
		}
	}
	endutent();
	if(oldest_login == 0 || timestamp < oldest_login) {
		return PAM_AUTH_ERR;
	}
	return PAM_SUCCESS;
}

#ifndef PAM_TIMESTAMP_MAIN
static int
get_ruser(pam_handle_t *pamh, char *ruserbuf, size_t ruserbuflen)
{
	const void *ruser;
	struct passwd *pwd;

	if (ruserbuf == NULL || ruserbuflen < 1)
		return -2;
	/* Get the name of the source user. */
	if (pam_get_item(pamh, PAM_RUSER, &ruser) != PAM_SUCCESS) {
		ruser = NULL;
	}
	if ((ruser == NULL) || (strlen(ruser) == 0)) {
		/* Barring that, use the current RUID. */
		pwd = pam_modutil_getpwuid(pamh, getuid());
		if (pwd != NULL) {
			ruser = pwd->pw_name;
		}
	} else {
		/*
		 * This ruser is used by format_timestamp_name as a component
		 * of constructed timestamp pathname, so ".", "..", and '/'
		 * are disallowed to avoid potential path traversal issues.
		 */
		if (!strcmp(ruser, ".") ||
		    !strcmp(ruser, "..") ||
		    strchr(ruser, '/')) {
			ruser = NULL;
		}
	}
	if (ruser == NULL || strlen(ruser) >= ruserbuflen) {
		*ruserbuf = '\0';
		return -1;
	}
	strcpy(ruserbuf, ruser);
	return 0;
}

/* Get the path to the timestamp to use. */
static int
get_timestamp_name(pam_handle_t *pamh, int argc, const char **argv,
		   char *path, size_t len)
{
	const char *user, *tty;
	const void *void_tty;
	const char *tdir = TIMESTAMPDIR;
	char ruser[BUFLEN];
	int i, debug = 0;

	/* Parse arguments. */
	for (i = 0; i < argc; i++) {
		if (strcmp(argv[i], "debug") == 0) {
			debug = 1;
		}
	}
	for (i = 0; i < argc; i++) {
		const char *str;

		if ((str = pam_str_skip_prefix(argv[i], "timestampdir=")) != NULL) {
			tdir = str;
			if (debug) {
				pam_syslog(pamh, LOG_DEBUG,
				       "storing timestamps in `%s'",
				       tdir);
			}
		}
	}
	i = check_dir_perms(pamh, tdir);
	if (i != PAM_SUCCESS) {
		return i;
	}
	/* Get the name of the target user. */
	if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || user[0] == '\0') {
		return PAM_AUTH_ERR;
	}
	if (debug) {
		pam_syslog(pamh, LOG_DEBUG, "becoming user `%s'", user);
	}
	/* Get the name of the source user. */
	if (get_ruser(pamh, ruser, sizeof(ruser)) || strlen(ruser) == 0) {
		return PAM_AUTH_ERR;
	}
	if (debug) {
		pam_syslog(pamh, LOG_DEBUG, "currently user `%s'", ruser);
	}
	/* Get the name of the terminal. */
	if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS) {
		tty = NULL;
	} else {
		tty = void_tty;
	}
	if ((tty == NULL) || (strlen(tty) == 0)) {
		tty = ttyname(STDIN_FILENO);
		if ((tty == NULL) || (strlen(tty) == 0)) {
			tty = ttyname(STDOUT_FILENO);
		}
		if ((tty == NULL) || (strlen(tty) == 0)) {
			tty = ttyname(STDERR_FILENO);
		}
		if ((tty == NULL) || (strlen(tty) == 0)) {
			/* Match sudo's behavior for this case. */
			tty = "unknown";
		}
	}
	if (debug) {
		pam_syslog(pamh, LOG_DEBUG, "tty is `%s'", tty);
	}
	/* Snip off all but the last part of the tty name. */
	tty = check_tty(tty);
	if (tty == NULL) {
		return PAM_AUTH_ERR;
	}
	/* Generate the name of the file used to cache auth results.  These
	 * paths should jive with sudo's per-tty naming scheme. */
	if (format_timestamp_name(path, len, tdir, tty, ruser, user) >= (int)len) {
		return PAM_AUTH_ERR;
	}
	if (debug) {
		pam_syslog(pamh, LOG_DEBUG, "using timestamp file `%s'", path);
	}
	return PAM_SUCCESS;
}

/* Tell the user that access has been granted. */
static void
verbose_success(pam_handle_t *pamh, long diff)
{
	pam_info(pamh, _("Access has been granted"
			 " (last access was %ld seconds ago)."), diff);
}

int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
	struct stat st;
	time_t interval = DEFAULT_TIMESTAMP_TIMEOUT;
	int i, fd, debug = 0, verbose = 0;
	char path[BUFLEN], *p, *message, *message_end;
	long tmp;
	const void *void_service;
	const char *service;
	time_t now, then;

	/* Parse arguments. */
	for (i = 0; i < argc; i++) {
		if (strcmp(argv[i], "debug") == 0) {
			debug = 1;
		}
	}
	for (i = 0; i < argc; i++) {
		const char *str;

		if ((str = pam_str_skip_prefix(argv[i], "timestamp_timeout=")) != NULL) {
			tmp = strtol(str, &p, 0);
			if ((p != NULL) && (*p == '\0')) {
				interval = tmp;
				if (debug) {
					pam_syslog(pamh, LOG_DEBUG,
					       "setting timeout to %ld"
					       " seconds", (long)interval);
				}
			}
		} else
		if (strcmp(argv[i], "verbose") == 0) {
			verbose = 1;
			if (debug) {
				pam_syslog(pamh, LOG_DEBUG,
				       "becoming more verbose");
			}
		}
	}

	if (flags & PAM_SILENT) {
		verbose = 0;
	}

	/* Get the name of the timestamp file. */
	if (get_timestamp_name(pamh, argc, argv,
			       path, sizeof(path)) != PAM_SUCCESS) {
		return PAM_AUTH_ERR;
	}

	/* Get the name of the service. */
	if (pam_get_item(pamh, PAM_SERVICE, &void_service) != PAM_SUCCESS) {
		service = NULL;
	} else {
		service = void_service;
	}
	if ((service == NULL) || (strlen(service) == 0)) {
		service = "(unknown)";
	}

	/* Open the timestamp file. */
	fd = open(path, O_RDONLY | O_NOFOLLOW);
	if (fd == -1) {
		if (debug) {
			pam_syslog(pamh, LOG_DEBUG,
			       "cannot open timestamp `%s': %m",
			       path);
		}
		return PAM_AUTH_ERR;
	}

	if (fstat(fd, &st) == 0) {
		int count;
		void *mac;
		size_t maclen;
		char ruser[BUFLEN];

		/* Check that the file is owned by the superuser. */
		if ((st.st_uid != 0) || (st.st_gid != 0)) {
			pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is "
			       "not owned by root", path);
			close(fd);
			return PAM_AUTH_ERR;
		}

		/* Check that the file is a normal file. */
		if (!(S_ISREG(st.st_mode))) {
			pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is "
			       "not a regular file", path);
			close(fd);
			return PAM_AUTH_ERR;
		}

#ifdef WITH_OPENSSL
		if (hmac_size(pamh, debug, &maclen)) {
			return PAM_AUTH_ERR;
		}
#else
		maclen = hmac_sha1_size();
#endif /* WITH_OPENSSL */
		/* Check that the file is the expected size. */
		if (st.st_size == 0) {
			/* Invalid, but may have been created by sudo. */
			close(fd);
			return PAM_AUTH_ERR;
		}
		if (st.st_size !=
		    (off_t)(strlen(path) + 1 + sizeof(then) + maclen)) {
			pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' "
			       "appears to be corrupted", path);
			close(fd);
			return PAM_AUTH_ERR;
		}

		/* Read the file contents. */
		message = malloc(st.st_size);
		count = 0;
                if (!message) {
			close(fd);
			return PAM_BUF_ERR;
		}
		while (count < st.st_size) {
			i = read(fd, message + count, st.st_size - count);
			if ((i == 0) || (i == -1)) {
				break;
			}
			count += i;
		}
		if (count < st.st_size) {
			pam_syslog(pamh, LOG_NOTICE, "error reading timestamp "
				"file `%s': %m", path);
			close(fd);
			free(message);
			return PAM_AUTH_ERR;
		}
		message_end = message + strlen(path) + 1 + sizeof(then);

		/* Regenerate the MAC. */
#ifdef WITH_OPENSSL
		if (hmac_generate(pamh, debug, &mac, &maclen, TIMESTAMPKEY,
				  ROOT_USER, ROOT_GROUP, message, message_end - message)) {
			close(fd);
			free(message);
			return PAM_AUTH_ERR;
		}
#else
		hmac_sha1_generate_file(pamh, &mac, &maclen, TIMESTAMPKEY,
					ROOT_USER, ROOT_GROUP, message, message_end - message);
#endif /* WITH_OPENSSL */
		if ((mac == NULL) ||
		    (memcmp(path, message, strlen(path)) != 0) ||
		    (memcmp(mac, message_end, maclen) != 0)) {
			pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
				"corrupted", path);
			close(fd);
			free(mac);
			free(message);
			return PAM_AUTH_ERR;
		}
		free(mac);
		memmove(&then, message + strlen(path) + 1, sizeof(then));
		free(message);

		/* Check oldest login against timestamp */
		if (get_ruser(pamh, ruser, sizeof(ruser)))
		{
			close(fd);
			return PAM_AUTH_ERR;
		}
		if (check_login_time(ruser, then) != PAM_SUCCESS)
		{
			pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
			       "older than oldest login, disallowing "
			       "access to %s for user %s",
			       path, service, ruser);
			close(fd);
			return PAM_AUTH_ERR;
		}

		/* Compare the dates. */
		now = time(NULL);
		if (timestamp_good(then, now, interval) == PAM_SUCCESS) {
			close(fd);
			pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
			       "only %ld seconds old, allowing access to %s "
			       "for user %s", path, (long) (now - st.st_mtime),
			       service, ruser);
			if (verbose) {
				verbose_success(pamh, now - st.st_mtime);
			}
			return PAM_SUCCESS;
		} else {
			close(fd);
			pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' has "
			       "unacceptable age (%ld seconds), disallowing "
			       "access to %s for user %s",
			       path, (long) (now - st.st_mtime),
			       service, ruser);
			return PAM_AUTH_ERR;
		}
	}
	close(fd);

	/* Fail by default. */
	return PAM_AUTH_ERR;
}

int
pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED)
{
	return PAM_SUCCESS;
}

int
pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv)
{
	char path[BUFLEN], subdir[BUFLEN], *text, *p;
	void *mac;
	size_t maclen;
	time_t now;
	int fd, i, debug = 0;

	/* Parse arguments. */
	for (i = 0; i < argc; i++) {
		if (strcmp(argv[i], "debug") == 0) {
			debug = 1;
		}
	}

	/* Get the name of the timestamp file. */
	if (get_timestamp_name(pamh, argc, argv,
			       path, sizeof(path)) != PAM_SUCCESS) {
		return PAM_SESSION_ERR;
	}

	/* Create the directory for the timestamp file if it doesn't already
	 * exist. */
	for (i = 1; i < (int) sizeof(path) && path[i] != '\0'; i++) {
		if (path[i] == '/') {
			/* Attempt to create the directory. */
			memcpy(subdir, path, i);
			subdir[i] = '\0';
			if (mkdir(subdir, 0700) == 0) {
				/* Attempt to set the owner to the superuser. */
			        if (lchown(subdir, 0, 0) != 0) {
					if (debug) {
						pam_syslog(pamh, LOG_DEBUG,
						    "error setting permissions on `%s': %m",
						    subdir);
					}
					return PAM_SESSION_ERR;
				}
			} else {
				if (errno != EEXIST) {
					if (debug) {
						pam_syslog(pamh, LOG_DEBUG,
						    "error creating directory `%s': %m",
						    subdir);
					}
					return PAM_SESSION_ERR;
				}
			}
		}
	}

#ifdef WITH_OPENSSL
	if (hmac_size(pamh, debug, &maclen)) {
		return PAM_SESSION_ERR;
	}
#else
	maclen = hmac_sha1_size();
#endif /* WITH_OPENSSL */

	/* Generate the message. */
	text = malloc(strlen(path) + 1 + sizeof(now) + maclen);
	if (text == NULL) {
		pam_syslog(pamh, LOG_CRIT, "unable to allocate memory: %m");
		return PAM_SESSION_ERR;
	}
	p = text;

	strcpy(text, path);
	p += strlen(path) + 1;

	now = time(NULL);
	memmove(p, &now, sizeof(now));
	p += sizeof(now);

	/* Generate the MAC and append it to the plaintext. */
#ifdef WITH_OPENSSL
	if (hmac_generate(pamh, debug, &mac, &maclen, TIMESTAMPKEY,
			  ROOT_USER, ROOT_GROUP, text, p - text)) {
		free(text);
		return PAM_SESSION_ERR;
	}
#else
	hmac_sha1_generate_file(pamh, &mac, &maclen, TIMESTAMPKEY,
				ROOT_USER, ROOT_GROUP, text, p - text);
	if (mac == NULL) {
		pam_syslog(pamh, LOG_ERR, "failure generating MAC: %m");
		free(text);
		return PAM_SESSION_ERR;
	}
#endif /* WITH_OPENSSL */
	memmove(p, mac, maclen);
	p += maclen;
	free(mac);

	/* Open the file. */
	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
	if (fd == -1) {
		pam_syslog(pamh, LOG_ERR, "unable to open `%s': %m", path);
		free(text);
		return PAM_SESSION_ERR;
	}

	/* Attempt to set the owner to the superuser. */
	if (fchown(fd, 0, 0) != 0) {
	  if (debug) {
	    pam_syslog(pamh, LOG_DEBUG,
		       "error setting ownership of `%s': %m",
		       path);
	  }
	  close(fd);
	  free(text);
	  return PAM_SESSION_ERR;
	}


	/* Write the timestamp to the file. */
	if (write(fd, text, p - text) != p - text) {
		pam_syslog(pamh, LOG_ERR, "unable to write to `%s': %m", path);
		close(fd);
		free(text);
		return PAM_SESSION_ERR;
	}

	/* Close the file and return successfully. */
	close(fd);
	free(text);
	pam_syslog(pamh, LOG_DEBUG, "updated timestamp file `%s'", path);
	return PAM_SUCCESS;
}

int
pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED)
{
	return PAM_SUCCESS;
}

#else /* PAM_TIMESTAMP_MAIN */

#define USAGE "Usage: %s [[-k] | [-d]] [target user]\n"
#define CHECK_INTERVAL 7

int
main(int argc, char **argv)
{
	int i, retval = 0, dflag = 0, kflag = 0;
	const char *target_user = NULL, *user = NULL, *tty = NULL;
	struct passwd *pwd;
	struct timeval tv;
	fd_set write_fds;
	char path[BUFLEN];
	struct stat st;

	/* Check that there's nothing funny going on with stdio. */
	if ((fstat(STDIN_FILENO, &st) == -1) ||
	    (fstat(STDOUT_FILENO, &st) == -1) ||
	    (fstat(STDERR_FILENO, &st) == -1)) {
		/* Appropriate the "no controlling tty" error code. */
		return 3;
	}

	/* Parse arguments. */
	while ((i = getopt(argc, argv, "dk")) != -1) {
		switch (i) {
			case 'd':
				dflag++;
				break;
			case 'k':
				kflag++;
				break;
			default:
				fprintf(stderr, USAGE, argv[0]);
				return 1;
				break;
		}
	}

	/* Bail if both -k and -d are given together. */
	if ((kflag + dflag) > 1) {
		fprintf(stderr, USAGE, argv[0]);
		return 1;
	}

	/* Check that we're setuid. */
	if (geteuid() != 0) {
		fprintf(stderr, "%s must be setuid root\n",
			argv[0]);
		retval = 2;
	}

	/* Check that we have a controlling tty. */
	tty = ttyname(STDIN_FILENO);
	if ((tty == NULL) || (strlen(tty) == 0)) {
		tty = ttyname(STDOUT_FILENO);
	}
	if ((tty == NULL) || (strlen(tty) == 0)) {
		tty = ttyname(STDERR_FILENO);
	}
	if ((tty == NULL) || (strlen(tty) == 0)) {
		tty = "unknown";
	}

	/* Get the name of the invoking (requesting) user. */
	pwd = getpwuid(getuid());
	if (pwd == NULL) {
		retval = 4;
	}

	/* Get the name of the target user. */
	user = strdup(pwd->pw_name);
	if (user == NULL) {
		retval = 4;
	} else {
		target_user = (optind < argc) ? argv[optind] : user;
		if ((strchr(target_user, '.') != NULL) ||
		    (strchr(target_user, '/') != NULL) ||
		    (strchr(target_user, '%') != NULL)) {
			fprintf(stderr, "unknown user: %s\n",
				target_user);
			retval = 4;
		}
	}

	/* Sanity check the tty to make sure we should be checking
	 * for timestamps which pertain to it. */
	if (retval == 0) {
		tty = check_tty(tty);
		if (tty == NULL) {
			fprintf(stderr, "invalid tty\n");
			retval = 6;
		}
	}

	do {
		/* Sanity check the timestamp directory itself. */
		if (retval == 0) {
			if (check_dir_perms(NULL, TIMESTAMPDIR) != PAM_SUCCESS) {
				retval = 5;
			}
		}

		if (retval == 0) {
			/* Generate the name of the timestamp file. */
			format_timestamp_name(path, sizeof(path), TIMESTAMPDIR,
					      tty, user, target_user);
		}

		if (retval == 0) {
			if (kflag) {
				/* Remove the timestamp. */
				if (lstat(path, &st) != -1) {
					retval = unlink(path);
				}
			} else {
				/* Check the timestamp. */
				if (lstat(path, &st) != -1) {
					/* Check oldest login against timestamp */
					if (check_login_time(user, st.st_mtime) != PAM_SUCCESS) {
						retval = 7;
					} else if (timestamp_good(st.st_mtime, time(NULL),
							DEFAULT_TIMESTAMP_TIMEOUT) != PAM_SUCCESS) {
						retval = 7;
					}
				} else {
					retval = 7;
				}
			}
		}

		if (dflag > 0) {
			struct timeval now;
			/* Send the would-be-returned value to our parent. */
			signal(SIGPIPE, SIG_DFL);
			fprintf(stdout, "%d\n", retval);
			fflush(stdout);
			/* Wait. */
			gettimeofday(&now, NULL);
			tv.tv_sec = CHECK_INTERVAL;
			/* round the sleep time to get woken up on a whole second */
			tv.tv_usec = 1000000 - now.tv_usec;
			if (now.tv_usec < 500000)
				tv.tv_sec--;
			FD_ZERO(&write_fds);
			FD_SET(STDOUT_FILENO, &write_fds);
			select(STDOUT_FILENO + 1,
			       NULL, NULL, &write_fds,
			       &tv);
			retval = 0;
		}
	} while (dflag > 0);

	return retval;
}

#endif
