/*
 * pam_ism_wordcount - enforce the ISM ism-1558 minimum word count.
 *
 * ism-1558 constrains passwords "using a sequence of words". A single-token
 * password is not one, so the rule does not apply to it -- which is what keeps
 * ism-2080 (no complexity requirements imposed) intact. A character-class module
 * such as pam_passwdqc cannot express this distinction.
 *
 * The entire check is a single pass over the candidate counting whitespace
 * transitions. There is no allocation, no copying, no buffer arithmetic and no
 * parsing, so the usual memory-safety hazards of C are simply not present here.
 * The candidate is never copied, logged, or passed to another process.
 *
 * Stack usage:
 *   password requisite pam_ism_wordcount.so minwords=4
 *
 * Build:
 *   cc -O2 -Wall -Wextra -fPIC -shared -o pam_ism_wordcount.so \
 *      pam_ism_wordcount.c -lpam
 */

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include <security/pam_modules.h>
#include <security/pam_ext.h>

#define DEFAULT_MIN_WORDS 4

/* Count whitespace-delimited words. Read-only, single pass, no allocation. */
static int count_words(const char *s)
{
	int words = 0;
	int in_word = 0;

	for (; *s != '\0'; s++) {
		if (isspace((unsigned char)*s)) {
			in_word = 0;
		} else if (!in_word) {
			in_word = 1;
			words++;
		}
	}
	return words;
}

static int min_words_from_args(int argc, const char **argv)
{
	static const char key[] = "minwords=";
	int i;

	for (i = 0; i < argc; i++) {
		if (strncmp(argv[i], key, sizeof(key) - 1) == 0) {
			int v = atoi(argv[i] + sizeof(key) - 1);
			if (v > 0 && v <= 64)
				return v;
		}
	}
	return DEFAULT_MIN_WORDS;
}

PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags,
				int argc, const char **argv)
{
	const void *item = NULL;
	const char *candidate;
	int min_words, words;

	/* The stack runs twice. The new token only exists on the update pass. */
	if (!(flags & PAM_UPDATE_AUTHTOK))
		return PAM_SUCCESS;

	if (pam_get_item(pamh, PAM_AUTHTOK, &item) != PAM_SUCCESS || item == NULL)
		return PAM_SUCCESS;	/* another module will reject an absent token */

	candidate = (const char *)item;
	min_words = min_words_from_args(argc, argv);
	words = count_words(candidate);

	/* Not a sequence of words: ism-1558 does not govern it. Imposing any
	 * further requirement here would breach ism-2080. */
	if (words <= 1)
		return PAM_SUCCESS;

	if (words >= min_words)
		return PAM_SUCCESS;

	pam_error(pamh, "A passphrase must use at least %d words.", min_words);
	return PAM_AUTHTOK_ERR;
}
