/*
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright(c) 2026 Baptiste Daroussin <bapt@FreeBSD.org>
 */

#include <security/pam_appl.h>
#include <sys/param.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>
#include <syslog.h>
#include <errno.h>

struct conv_data {
	char *password; // owned pointer
};

static int
pam_conv_fn(int num_msg, const struct pam_message **msg,
    struct pam_response **resp, void *appdata_ptr)
{
	if (num_msg <= 0)
		return (PAM_CONV_ERR);
	struct pam_response *aresp = calloc(num_msg, sizeof(*aresp));
	if (!aresp)
		return(PAM_CONV_ERR);

	struct conv_data *cd = (struct conv_data *)appdata_ptr;

	for (int i = 0; i < num_msg; ++i) {
		const struct pam_message *m = msg[i];
		aresp[i].resp = NULL;
		aresp[i].resp_retcode = 0;
		if (m->msg_style == PAM_PROMPT_ECHO_OFF ||
		    m->msg_style == PAM_PROMPT_ECHO_ON) {
			if (!cd->password) {
				// read password once from stdin (no echo handling here)
				char buf[1024];
				if (!fgets(buf, sizeof(buf), stdin)) {
					// user didn't provide password
					free(aresp);
					return (PAM_CONV_ERR);
				}
				size_t ln = strlen(buf);
				if (ln == sizeof(buf)-1 && buf[ln-1] != '\n') {
					syslog(LOG_ERR, "Password too long, "
					   "truncated");
					free(aresp);
					return (PAM_CONV_ERR);
				}
				/* clear the newline character if present */
				if (ln && buf[ln-1] == '\n') { buf[ln-1] = '\0'; }
				cd->password = strdup(buf);
				if (!cd->password) {
					free(aresp);
					return (PAM_CONV_ERR);
				}
				explicit_bzero(buf, sizeof(buf));
			}
			aresp[i].resp = strdup(cd->password);
			if (!aresp[i].resp) {
				syslog(LOG_ERR, "memory error");
				for (int j = 0; j < i; j++)
					free(aresp[j].resp);
				free(aresp);
				return (PAM_CONV_ERR);
			}
		} else {
			// info/error messages: print to stderr
			if (m->msg) fputs(m->msg, stderr);
			aresp[i].resp = NULL;
		}
	}

	*resp = aresp;
	return PAM_SUCCESS;
}

int
main(int argc, char **argv)
{
	const char *profile;

	openlog("pam_worker", LOG_PID, LOG_AUTH);

	if (argc != 2) {
		syslog(LOG_ERR, "usage: pam_worker NAME");
		errx(EXIT_FAILURE, "usage pam_worker NAME");
	}

	profile = argv[1];
	if (strlen(profile) == 0) {
		syslog(LOG_ERR, "invalid empty profile");
		errx(EXIT_FAILURE, "invalid empty profile");
	}

	if (strpbrk(profile, "/.\0") != NULL) {
		syslog(LOG_ERR, "invalid characters in profile name: %s",
		    profile);
		errx(EXIT_FAILURE, "invalid characters in profile name: %s",
		    profile);
	}

	const char *username = getenv("PAM_USER");
	if (username == NULL) {
		syslog(LOG_ERR, "Impossible to retrieve username");
		errx(EXIT_FAILURE, "Impossible to retrieve username");
	}

	/* Validate username: prevent injections attacks */
	if (strpbrk(username, ";|`$\0") != NULL) {
		syslog(LOG_ERR, "invalid username contains shell "
		    "metacharacters: %s", username);
		errx(EXIT_FAILURE, "invalid username contains shell "
		    "metacharters: %s", username);
	}

	long sc = sysconf(_SC_LOGIN_NAME_MAX);
	if (sc != -1 && strlen(username) >= sc) {
		syslog(LOG_ERR, "invalid username '%s': too long, "
		    "maximum length is: %ld", username, sc);
		errx(EXIT_FAILURE, "invalid username '%s': too long, "
		    "maximum length is: %ld", username, sc);
	}

	struct conv_data cd = { .password = NULL };
	struct pam_conv conv = { pam_conv_fn, &cd };
	pam_handle_t *pamh = NULL;

	int pamr = pam_start(profile, username, &conv, &pamh);
	if (pamr != PAM_SUCCESS) {
		if (pamh)
			pam_end(pamh, pamr);
		if (cd.password) {
			explicit_bzero(cd.password, strlen(cd.password));
			free(cd.password);
		}
		syslog(LOG_ERR, "pam_start failed: %s", pam_strerror(pamh, pamr));
		errx(EXIT_FAILURE, "pam_start failed: %s", pam_strerror(pamh, pamr));
	}

	pamr = pam_authenticate(pamh, 0);
	if (pamr != PAM_SUCCESS) {
		// optional: try pam_acct_mgmt to check account
		syslog(LOG_INFO, "authentication failed: %s", pam_strerror(pamh, pamr));
		pam_end(pamh, pamr);
		if (cd.password) {
			explicit_bzero(cd.password, strlen(cd.password));
			free(cd.password);
		}
		return (EXIT_FAILURE);
	}

	pamr = pam_acct_mgmt(pamh, 0);
	if (pamr != PAM_SUCCESS) {
		syslog(LOG_INFO, "account management: %s", pam_strerror(pamh, pamr));
		pam_end(pamh, pamr);
		if (cd.password) {
			explicit_bzero(cd.password, strlen(cd.password));
			free(cd.password);
		}
		return (EXIT_FAILURE);
	}

	pam_end(pamh, PAM_SUCCESS);
	if (cd.password) {
		explicit_bzero(cd.password, strlen(cd.password));
		free(cd.password);
	}

	closelog();
	return (EXIT_SUCCESS);
}
