summaryrefslogtreecommitdiff
path: root/pam_bloom.c
diff options
context:
space:
mode:
authorDaniel Roberson <daniel@badoperation.net>2026-09-13 07:44:32 -0700
committerDaniel Roberson <daniel@badoperation.net>2026-09-13 07:44:32 -0700
commitf70ce379de6261733dc71279c76fc76a210285e1 (patch)
tree07704c8156c03df696460b6c0edae6673174ecdf /pam_bloom.c
initial commit
Diffstat (limited to 'pam_bloom.c')
-rw-r--r--pam_bloom.c246
1 files changed, 246 insertions, 0 deletions
diff --git a/pam_bloom.c b/pam_bloom.c
new file mode 100644
index 0000000..7233ed8
--- /dev/null
+++ b/pam_bloom.c
@@ -0,0 +1,246 @@
+
+#define PAM_SM_PASSWORD
+
+#include <security/pam_modules.h>
+#include <security/pam_ext.h>
+
+#include <dirent.h>
+#include <stdbool.h>
+#include <string.h>
+#include <syslog.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#include "bloom.h"
+
+#define PAM_BLOOM_FILTER_DIR "/etc/security/pam_bloom/filters.d"
+
+static int
+has_bloom_extension(const char *name)
+{
+ size_t len = strlen(name);
+
+ return len > 6 &&
+ strcmp(name + len - 6, ".bloom") == 0;
+}
+
+static int
+validate_filter_fd(pam_handle_t *pamh, int fd, const char *filename)
+{
+ struct stat st;
+
+ if (fstat(fd, &st) < 0) {
+ pam_syslog(pamh, LOG_ERR, "unable to stat Bloom filter %s", filename);
+ return -1;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ pam_syslog(pamh, LOG_ERR, "Bloom filter %s is not a regular file", filename);
+ return -1;
+ }
+
+ if (st.st_uid != 0) {
+ pam_syslog(pamh, LOG_ERR, "Bloom filter %s is not owned by root", filename);
+ return -1;
+ }
+
+ if (st.st_mode & (S_IWGRP | S_IWOTH)) {
+ pam_syslog(pamh, LOG_ERR, "Bloom filter %s is writable by group or others", filename);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+check_filter_fd(pam_handle_t *pamh, int fd, const char *filename, const char *password)
+{
+ bloomfilter_mapped bf = {0};
+
+ bloom_error_t err = bloom_map_fd(&bf, fd);
+
+ if (err != BF_SUCCESS) {
+ pam_syslog(pamh, LOG_ERR,
+ "unable to map Bloom filter %s: %s", filename, bloom_strerror(err));
+ return -1;
+ }
+
+ bool found = bloom_mapped_lookup(&bf, password, strlen(password));
+
+ if (found) {
+ pam_error(pamh,
+ "Password rejected: it matches the %s filter.",
+ (const char *)bf.header->name);
+ }
+
+ bloom_unmap(&bf);
+
+ return found ? 1 : 0;
+}
+
+static int
+validate_filter_directory(pam_handle_t *pamh, int dirfd, const char *directory)
+{
+ struct stat st;
+
+ if (fstat(dirfd, &st) < 0) {
+ pam_syslog(pamh, LOG_ERR, "unable to stat Bloom filter directory %s", directory);
+ return -1;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ pam_syslog(pamh, LOG_ERR, "Bloom filter path %s is not a directory", directory);
+ return -1;
+ }
+
+ if (st.st_uid != 0) {
+ pam_syslog(pamh, LOG_ERR, "Bloom filter directory %s is not owned by root", directory);
+ return -1;
+ }
+
+ if (st.st_mode & (S_IWGRP | S_IWOTH)) {
+ pam_syslog(pamh, LOG_ERR,
+ "Bloom filter directory %s is writable by group or others", directory);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+check_filter_directory(pam_handle_t *pamh, const char *directory, const char *password)
+{
+ int dirfd = open(directory, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+
+ if (dirfd < 0) {
+ pam_syslog(pamh, LOG_ERR, "unable to open Bloom filter directory %s", directory);
+ return -1;
+ }
+
+ if (validate_filter_directory(pamh, dirfd, directory) < 0) {
+ close(dirfd);
+ return -1;
+ }
+
+ DIR *dir = fdopendir(dirfd);
+
+ if (dir == NULL) {
+ close(dirfd);
+ pam_syslog(pamh, LOG_ERR, "unable to read Bloom filter directory %s", directory);
+ return -1;
+ }
+
+ bool saw_filter = false;
+ int result = 0;
+ struct dirent *entry;
+
+ for (;;) {
+ errno = 0;
+ entry = readdir(dir);
+
+ if (entry == NULL) {
+ if (errno != 0) {
+ pam_syslog(pamh, LOG_ERR, "error reading Bloom filter directory %s", directory);
+ result = -1;
+ }
+
+ break;
+ }
+
+ if (!has_bloom_extension(entry->d_name)) {
+ continue;
+ }
+
+ saw_filter = true;
+
+ int fd = openat(dirfd, entry->d_name, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+
+ if (fd < 0) {
+ pam_syslog(pamh, LOG_ERR, "unable to open Bloom filter %s", entry->d_name);
+ result = -1;
+ break;
+ }
+
+ if (validate_filter_fd(pamh, fd, entry->d_name) < 0) {
+ close(fd);
+ result = -1;
+ break;
+ }
+
+ result = check_filter_fd(pamh, fd, entry->d_name, password);
+
+ close(fd);
+
+ if (result != 0) {
+ break;
+ }
+ }
+
+ if (result == 0 && !saw_filter) {
+ pam_syslog(pamh, LOG_WARNING, "no Bloom filters found in directory %s", directory);
+ }
+
+
+ if (closedir(dir) != 0) {
+ pam_syslog(pamh, LOG_ERR, "error closing Bloom filter directory %s", directory);
+
+ if (result == 0) {
+ result = -1;
+ }
+ }
+
+ return result;
+}
+
+PAM_EXTERN int
+pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ const char *filter_dir = PAM_BLOOM_FILTER_DIR;
+
+ bool dir_seen = false;
+
+ for (int i = 0; i < argc; i++) {
+ static const char prefix[] = "dir=";
+
+ if (strncmp(argv[i], prefix, sizeof(prefix) - 1) != 0) {
+ pam_syslog(pamh, LOG_ERR, "pam_bloom: unknown option: %s", argv[i]);
+ return PAM_SERVICE_ERR;
+ }
+
+ if (dir_seen) {
+ pam_syslog(pamh, LOG_ERR, "pam_bloom: duplicate dir= option");
+ return PAM_SERVICE_ERR;
+ }
+
+ dir_seen = true;
+
+ const char *value = argv[i] + sizeof(prefix) - 1;
+
+ if (*value == '\0') {
+ pam_syslog(pamh, LOG_ERR, "pam_bloom: empty dir= option");
+ return PAM_SERVICE_ERR;
+ }
+
+ filter_dir = value;
+ }
+
+ if (!(flags & PAM_UPDATE_AUTHTOK)) {
+ return PAM_SUCCESS;
+ }
+
+ const char *password = NULL;
+
+ int rc = pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL);
+
+ if (rc != PAM_SUCCESS || password == NULL) {
+ return PAM_AUTHTOK_ERR;
+ }
+
+ int result = check_filter_directory(pamh, filter_dir, password);
+ if (result < 0) return PAM_SERVICE_ERR;
+ if (result > 0) return PAM_AUTHTOK_ERR;
+
+ return PAM_SUCCESS;
+}