/*-
 * Copyright (c) 2005 Robert N. M. Watson
 * All rights reserved.
 *
 * 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, this list of conditions and the following disclaimer.
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 *
 * $FreeBSD$
 */

/*
 * PAM module to log user login and password data for later processing.
 * Potential uses include password recovery in the event that a site is
 * migrating to Kerberos and requires plaintext passwords to build the new
 * password database, or to analyze attack patterns in brute force attempts.
 * Obviously, there is the potential for accidental (or otherwise) misuse, so
 * caution is advised.  Sites are advised to use the "excludeuser" feature in
 * order to avoid logging passwords for sensitive users, and to carefully
 * protect resulting log data.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <vis.h>

#define	PAM_SM_AUTH

#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_mod_misc.h>
#include <security/openpam.h>

#define USER_PROMPT		"Username: "
#define PASSWORD_PROMPT		"Password:"

#define	PAM_OPT_LOGFILE		"logfile"
#define	PAM_OPT_EXCLUDEUSER	"excludeusers"

#define	OPENSSH_BUG		"\b\n\r\177INCORRECT"

#define	FILE_HEADER		"time,entrystatus,service,rhost,user," \
				    "userstatus,password\n"

/*
 * Search a comma-delimited string for a particular username.  In the event
 * of memory exhaustion, will return a false positive match.
 */
static int
sniffpasswd_matchuser(const char *userlist, const char *user)
{
	char *dup_userlist, *free_userlist;
	char *try_user;

	/*
	 * Create writable copy of userlist.  If we run out of memory,
	 * exclude the user, in order to "fail closed" and not write
	 * sensitive password information to a log file.
	 */
	free_userlist = dup_userlist = strdup(userlist);
	if (dup_userlist == NULL)
		return (1);

	while ((try_user = strsep(&dup_userlist, ",")) != NULL) {
		if (strcmp(user, try_user) == 0) {
			free(free_userlist);
			return (1);
		}
	}

	free(free_userlist);
	return (0);
}

/*
 * Determine if a username is "valid".
 */
static int
sniffpasswd_validuser(const char *user)
{

	return (getpwnam(user) != NULL);
}

/*
 * Determine if the provided password is actually a symptom of an OpenSSH
 * bug, in which PAM is called using the error string returned to the remote
 * endpoint instead of with the attempted password.
 */
static int
sniffpasswd_opensshbug(const char *service, const char *passwd)
{

	if (strcmp(service, "sshd") != 0)
		return (0);
	if (strcmp(passwd, OPENSSH_BUG) != 0)
		return (0);
	return (1);
}

/*
 * Given a user-generated password string, sanitize it for inclusion in a
 * .csv file.  Currently this means passing the string through vis(3), then
 * performing additional escaping for handling of ',' and '"' characters.  In
 * the event of memory exhaustion, may return NULL.  Caller must free the
 * string if one is returned.
 *
 * There is no pretty way to implement this using C string routines, so this
 * is ugly.
 */
static char *
sniffpasswd_sanitize(const char *passwd)
{
	char *strvis_result, *quote_result, *source_cp, *dest_cp;
	char ch;

	/*
	 * strvis(3) requires 4*chars + space for nul terminator for result.
	 */
	strvis_result = malloc(strlen(passwd) * 4 + 1);
	if (strvis_result == NULL)
		return (NULL);
	strvis(strvis_result, passwd, VIS_NL | VIS_TAB | VIS_CSTYLE);

	/*
	 * Each '"' becomes a double quote, and we require two additional
	 * characters to begin and end each entry with quotes, requiring
	 * 2*chars + 2 + space for nul terminator.
	 */
	quote_result = malloc(strlen(strvis_result) * 2 + 2 + 1);
	if (quote_result == NULL) {
		free(strvis_result);
		return (NULL);
	}
	dest_cp = quote_result;

	*dest_cp = '"';
	dest_cp++;
	for (source_cp = strvis_result; *source_cp != '\0'; source_cp++) {
		ch = *source_cp;
		switch (ch) {
		case '"':
			*dest_cp = '"';
			dest_cp++;
			*dest_cp = '"';
			dest_cp++;
			break;
		default:
			*dest_cp = ch;
			dest_cp++;
		}
	}
	*dest_cp = '"';
	dest_cp++;
	*dest_cp = '\0';
	return (quote_result);
}

/*
 * authentication management
 */
PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags __unused,
    int argc __unused, const char *argv[] __unused)
{
	const char *user, *pass, *rhost, *excludeusers;
	const char *validuser, *logfile, *service;
	char *sanitized_user, *sanitized_pass;
	char *logstring, *timestring;
	const char *entrystatus;
	int fd, retval, write_header;
	struct stat sb;
	time_t thetime;

	logfile = openpam_get_option(pamh, PAM_OPT_LOGFILE);
	if (logfile == NULL)
		return (PAM_IGNORE);

	/*
	 * Attempt to use advisory locking to synchronize access to the file
	 * -- this is desirable so that we can atomically insert a header at
	 * the top of the file if it's 0-length.  If we fail to get a lock
	 * due to lack of file system support, go ahead anyway, since the
	 * race is pretty unlikely.
	 */
	fd = open(logfile, O_WRONLY | O_APPEND | O_CREAT | O_EXLOCK, 0600);
	if (fd < 0 && errno == EOPNOTSUPP)
		fd = open(logfile, O_WRONLY | O_APPEND | O_CREAT, 0600);
	if (fd < 0)
		return (PAM_IGNORE);

	if (fstat(fd, &sb) < 0) {
		close(fd);
		return (PAM_IGNORE);
	}
	write_header = (sb.st_size == 0);

	entrystatus = "ok";
	retval = pam_get_user(pamh, &user, USER_PROMPT);
	if (retval != PAM_SUCCESS) {
		user = "";
		entrystatus = "pam: no user";
	}

	retval = pam_get_authtok(pamh, PAM_AUTHTOK, &pass, PASSWORD_PROMPT);
	if (retval != PAM_SUCCESS) {
		pass = "";
		entrystatus = "pam: no password";
	}

	excludeusers = openpam_get_option(pamh, PAM_OPT_EXCLUDEUSER);
	if (excludeusers != NULL &&
	    sniffpasswd_matchuser(excludeusers, user)) {
		pass = "";
		entrystatus = "excluded";
	}

	if (sniffpasswd_validuser(user))
		validuser = "valid";
	else
		validuser = "invalid";

	retval = pam_get_item(pamh, PAM_RHOST, (void *)&rhost);
	if (retval != PAM_SUCCESS) {
		rhost = "";
		entrystatus = "pam: no host";
	}

	retval = pam_get_item(pamh, PAM_SERVICE, (void *)&service);
	if (retval != PAM_SUCCESS) {
		service = "unknown";
		entrystatus = "pam: no service";
	}

	/*
	 * Layout:
	 *
	 *   time,service,entrystatus,rhost,user,userstatus,password
	 *
	 * Remember to update FILE_HEADER when modifying this.
	 */
	thetime = time(NULL);
	timestring = ctime(&thetime);
	timestring[strlen(timestring) - 1] = '\0';

	sanitized_user = sniffpasswd_sanitize(user);
	if (sanitized_user == NULL)
		entrystatus = "sanitize user: out of memory";
	sanitized_pass = sniffpasswd_sanitize(pass);
	if (sanitized_pass == NULL)
		entrystatus = "sanitize password: out of memory";

	if (sniffpasswd_opensshbug(service, pass))
		entrystatus = "sshd invalid user bug";

	(void)asprintf(&logstring, "%s,%s,%s,%s,%s,%s,%s\n", timestring,
	    entrystatus, service, rhost, sanitized_user, validuser,
	    sanitized_pass);

	if (write_header)
		(void)write(fd, FILE_HEADER, strlen(FILE_HEADER));
	if (logstring != NULL)
		(void)write(fd, logstring, strlen(logstring));
	free(sanitized_user);
	free(sanitized_pass);
	free(logstring);

	close(fd);
	return (PAM_IGNORE);
}

PAM_EXTERN int
pam_sm_setcred(__unused pam_handle_t *_pamh,
        __unused int _flags,
        __unused int _argc,
        __unused const char **_argv)
{

	return (PAM_IGNORE);
}

PAM_MODULE_ENTRY("pam_sniffpasswd");
