/*-
 * Copyright (c) 2001-2003 Networks Associates Technology, Inc.
 * Copyright (c) 2004-2025 Dag-Erling Smørgrav
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by ThinkSec AS and
 * Network Associates Laboratories, the Security Research Division of
 * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
 * ("CBOSS"), as part of the DARPA CHATS research program.
 *
 * 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.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior written
 *    permission.
 *
 * 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.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <sys/param.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <security/pam_appl.h>

#include "openpam_impl.h"
#include "openpam_ctype.h"
#include "openpam_strlcat.h"
#include "openpam_strlcpy.h"

static int openpam_load_chain(pam_handle_t *, const char *, pam_facility_t);

/*
 * Validate a service name.
 *
 * Returns a non-zero value if the argument points to a NUL-terminated
 * string consisting entirely of characters in the POSIX portable filename
 * character set, excluding the path separator character.
 */
static int
valid_service_name(const char *name)
{
	const char *p;

	if (OPENPAM_FEATURE(RESTRICT_SERVICE_NAME)) {
		/* path separator not allowed */
		for (p = name; *p != '\0'; ++p)
			if (!is_pfcs(*p))
				return (0);
	} else {
		/* path separator allowed */
		for (p = name; *p != '\0'; ++p)
			if (!is_pfcs(*p) && *p != '/')
				return (0);
	}
	return (1);
}

/*
 * Parse the facility name.
 *
 * Returns the corresponding pam_facility_t value, or -1 if the argument
 * is not a valid facility name.
 */
static pam_facility_t
parse_facility_name(const char *name)
{
	int i;

	for (i = 0; i < PAM_NUM_FACILITIES; ++i)
		if (strcmp(pam_facility_name[i], name) == 0)
			return (i);
	return ((pam_facility_t)-1);
}

/*
 * Parse the control flag.
 *
 * Returns the corresponding pam_control_t value, or -1 if the argument is
 * not a valid control flag name.
 */
static pam_control_t
parse_control_flag(const char *name)
{
	int i;

	for (i = 0; i < PAM_NUM_CONTROL_FLAGS; ++i)
		if (strcmp(pam_control_flag_name[i], name) == 0)
			return (i);
	return ((pam_control_t)-1);
}

/*
 * Validate a file name.
 *
 * Returns a non-zero value if the argument points to a NUL-terminated
 * string consisting entirely of characters in the POSIX portable filename
 * character set, including the path separator character.
 */
static int
valid_module_name(const char *name)
{
	const char *p;

	if (OPENPAM_FEATURE(RESTRICT_MODULE_NAME)) {
		/* path separator not allowed */
		for (p = name; *p != '\0'; ++p)
			if (!is_pfcs(*p))
				return (0);
	} else {
		/* path separator allowed */
		for (p = name; *p != '\0'; ++p)
			if (!is_pfcs(*p) && *p != '/')
				return (0);
	}
	return (1);
}

typedef enum { pam_conf_style, pam_d_style } openpam_style_t;

/*
 * Extracts given chains from a policy file.
 *
 * Returns the number of policy entries which were found for the specified
 * service and facility, or -1 if a system error occurred or a syntax
 * error was encountered.
 */
static int
openpam_parse_chain(pam_handle_t *pamh,
	const char *service,
	pam_facility_t facility,
	FILE *f,
	const char *filename,
	openpam_style_t style)
{
	pam_chain_t *this, **next;
	pam_module_t *module;
	pam_facility_t fclt;
	pam_control_t ctlf;
	char *name, *servicename, *modulename;
	int count, lineno, nonfatal, ret, serrno;
	char **wordv, *word;
	int i, wordc;

	count = 0;
	this = NULL;
	name = NULL;
	lineno = 0;
	wordc = 0;
	wordv = NULL;
	while ((wordv = openpam_readlinev(f, &lineno, &wordc)) != NULL) {
		/* blank line? */
		if (wordc == 0) {
			FREEV(wordc, wordv);
			continue;
		}
		i = 0;

		/* check service name if necessary */
		if (style == pam_conf_style &&
		    strcmp(wordv[i++], service) != 0) {
			FREEV(wordc, wordv);
			continue;
		}

		/* check facility name */
		if ((word = wordv[i++]) == NULL) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): missing facility",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}
		if (*word == '-') {
			nonfatal = 1;
			word++;
		} else {
			nonfatal = 0;
		}
		if ((fclt = parse_facility_name(word)) == (pam_facility_t)-1) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): invalid facility",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}
		if (facility != fclt && facility != PAM_FACILITY_ANY) {
			FREEV(wordc, wordv);
			continue;
		}

		/* control flag or "include" */
		if ((word = wordv[i++]) == NULL) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): missing control flag",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}
		if (strcmp(word, "include") == 0) {
			if ((servicename = wordv[i++]) == NULL) {
				openpam_log(PAM_LOG_ERROR,
				    "%s(%d): missing service name",
				    filename, lineno);
				errno = EINVAL;
				goto fail;
			}
			if (!valid_service_name(servicename)) {
				openpam_log(PAM_LOG_ERROR,
				    "%s(%d): invalid service name",
				    filename, lineno);
				errno = EINVAL;
				goto fail;
			}
			if (wordv[i] != NULL) {
				openpam_log(PAM_LOG_ERROR,
				    "%s(%d): garbage at end of line",
				    filename, lineno);
				errno = EINVAL;
				goto fail;
			}
			ret = openpam_load_chain(pamh, servicename, fclt);
			FREEV(wordc, wordv);
			if (ret < 0) {
				/*
				 * Bogus errno, but this ensures that the
				 * outer loop does not just ignore the
				 * error and keep searching.
				 */
				if (errno == ENOENT) {
					if (nonfatal)
						continue;
					errno = EINVAL;
				}
				goto fail;
			}
			continue;
		}
		if ((ctlf = parse_control_flag(word)) == (pam_control_t)-1) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): invalid control flag",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}

		/* get module name */
		if ((modulename = wordv[i++]) == NULL) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): missing module name",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}
		if (!valid_module_name(modulename)) {
			openpam_log(PAM_LOG_ERROR,
			    "%s(%d): invalid module name",
			    filename, lineno);
			errno = EINVAL;
			goto fail;
		}

		/* load module */
		if ((module = openpam_load_module(modulename)) == NULL) {
			if (errno == ENOENT) {
				if (nonfatal) {
					FREEV(wordc, wordv);
					continue;
				}
				errno = ENOEXEC;
			}
			goto fail;
		}

		/* allocate new entry */
		if ((this = calloc(1, sizeof *this)) == NULL)
			goto syserr;
		this->flag = ctlf;
		this->module = module;

		/*
		 * The remaining items in wordv are the module's
		 * arguments.  We could set this->optv = wordv + i, but
		 * then free(this->optv) wouldn't work.  Instead, we free
		 * the words we've already consumed, shift the rest up,
		 * and clear the tail end of the array.
		 */
		this->optc = wordc - i;
		for (i = 0; i < wordc - this->optc; ++i) {
			FREE(wordv[i]);
		}
		for (i = 0; i < this->optc; ++i) {
			wordv[i] = wordv[wordc - this->optc + i];
			wordv[wordc - this->optc + i] = NULL;
		}
		this->optv = wordv;
		wordv = NULL;
		wordc = 0;

		/* hook it up */
		for (next = &pamh->chains[fclt]; *next != NULL;
		     next = &(*next)->next)
			/* nothing */ ;
		*next = this;
		this = NULL;
		++count;
	}
	/*
	 * The loop ended because openpam_readword() returned NULL, which
	 * can happen for four different reasons: an I/O error (ferror(f)
	 * is true), a memory allocation failure (ferror(f) is false,
	 * feof(f) is false, errno is non-zero), the file ended with an
	 * unterminated quote or backslash escape (ferror(f) is false,
	 * feof(f) is true, errno is non-zero), or the end of the file was
	 * reached without error (ferror(f) is false, feof(f) is true,
	 * errno is zero).
	 */
	if (ferror(f) || errno != 0)
		goto syserr;
	if (!feof(f))
		goto fail;
	fclose(f);
	return (count);
syserr:
	serrno = errno;
	openpam_log(PAM_LOG_ERROR, "%s: %m", filename);
	errno = serrno;
	/* fall through */
fail:
	serrno = errno;
	if (this && this->optc && this->optv)
		FREEV(this->optc, this->optv);
	FREE(this);
	FREEV(wordc, wordv);
	FREE(wordv);
	FREE(name);
	fclose(f);
	errno = serrno;
	return (-1);
}

/*
 * Read the specified chains from the specified file.
 *
 * Returns 0 if the file exists but does not contain any matching lines.
 *
 * Returns -1 and sets errno to ENOENT if the file does not exist.
 *
 * Returns -1 and sets errno to some other non-zero value if the file
 * exists but is unsafe or unreadable, or an I/O error occurs.
 */
static int
openpam_load_file(pam_handle_t *pamh,
	const char *service,
	pam_facility_t facility,
	const char *filename,
	openpam_style_t style)
{
	FILE *f;
	int ret, serrno;

	/* attempt to open the file */
	if ((f = fopen(filename, "r")) == NULL) {
		serrno = errno;
		openpam_log(errno == ENOENT ? PAM_LOG_DEBUG : PAM_LOG_ERROR,
		    "%s: %m", filename);
		errno = serrno;
		RETURNN(-1);
	} else {
		openpam_log(PAM_LOG_DEBUG, "found %s", filename);
	}

	/* verify type, ownership and permissions */
	if (OPENPAM_FEATURE(VERIFY_POLICY_FILE) &&
	    openpam_check_desc_owner_perms(filename, fileno(f)) != 0) {
		/* already logged the cause */
		serrno = errno;
		fclose(f);
		errno = serrno;
		RETURNN(-1);
	}

	/* parse the file */
	ret = openpam_parse_chain(pamh, service, facility,
	    f, filename, style);
	RETURNN(ret);
}

/*
 * Locates the policy file for a given service and reads the given chains
 * from it.
 *
 * Returns the number of policy entries which were found for the specified
 * service and facility, or -1 if a system error occurred or a syntax
 * error was encountered.
 */
static int
openpam_load_chain(pam_handle_t *pamh,
	const char *service,
	pam_facility_t facility)
{
	const char *p, **path;
	char filename[PATH_MAX];
	size_t len;
	openpam_style_t style;
	int ret;

	ENTERS(facility < 0 ? "any" : pam_facility_name[facility]);

	/* either absolute or relative to cwd */
	if (strchr(service, '/') != NULL) {
		if ((p = strrchr(service, '.')) != NULL && strcmp(p, ".conf") == 0)
			style = pam_conf_style;
		else
			style = pam_d_style;
		ret = openpam_load_file(pamh, service, facility,
		    service, style);
		RETURNN(ret);
	}

	/* search standard locations */
	for (path = openpam_policy_path; *path != NULL; ++path) {
		/* construct filename */
		len = strlcpy(filename, *path, sizeof filename);
		if (len >= sizeof filename) {
			errno = ENAMETOOLONG;
			RETURNN(-1);
		}
		if (filename[len - 1] == '/') {
			len = strlcat(filename, service, sizeof filename);
			if (len >= sizeof filename) {
				errno = ENAMETOOLONG;
				RETURNN(-1);
			}
			style = pam_d_style;
		} else {
			style = pam_conf_style;
		}
		ret = openpam_load_file(pamh, service, facility,
		    filename, style);
		/* success */
		if (ret > 0)
			RETURNN(ret);
		/* the file exists, but an error occurred */
		if (ret == -1 && errno != ENOENT)
			RETURNN(ret);
		/* in pam.d style, an empty file counts as a hit */
		if (ret == 0 && style == pam_d_style)
			RETURNN(ret);
	}

	/* no hit */
	errno = ENOENT;
	RETURNN(-1);
}

/*
 * OpenPAM internal
 *
 * Configure a service
 */

int
openpam_configure(pam_handle_t *pamh,
	const char *service)
{
	pam_facility_t fclt;
	int serrno;

	ENTERS(service);
	if (!valid_service_name(service)) {
		openpam_log(PAM_LOG_ERROR, "invalid service name");
		RETURNC(PAM_SYSTEM_ERR);
	}
	if (openpam_load_chain(pamh, service, PAM_FACILITY_ANY) < 0) {
		if (errno != ENOENT)
			goto load_err;
	}
	for (fclt = 0; fclt < PAM_NUM_FACILITIES; ++fclt) {
		if (pamh->chains[fclt] != NULL)
			continue;
		if (OPENPAM_FEATURE(FALLBACK_TO_OTHER)) {
			if (openpam_load_chain(pamh, PAM_OTHER, fclt) < 0)
				goto load_err;
		}
	}
	RETURNC(PAM_SUCCESS);
load_err:
	serrno = errno;
	openpam_clear_chains(pamh->chains);
	errno = serrno;
	RETURNC(PAM_SYSTEM_ERR);
}

/*
 * NODOC
 *
 * Error codes:
 *	PAM_SYSTEM_ERR
 */
