summaryrefslogtreecommitdiff
path: root/pam_bloom_check.c
diff options
context:
space:
mode:
authorDaniel Roberson <daniel@badoperation.net>2026-09-13 08:25:52 -0700
committerDaniel Roberson <daniel@badoperation.net>2026-09-13 08:25:52 -0700
commit5f28840a5c365099f0e0d855b19fa0c13c9020c0 (patch)
treeff51d7cea6ce33d6dd7ae073a6c7940983fefd08 /pam_bloom_check.c
parentd556c740a65f1468e3dadaeba25f67ea089ea25a (diff)
add pam_bloom_check
Diffstat (limited to 'pam_bloom_check.c')
-rw-r--r--pam_bloom_check.c226
1 files changed, 226 insertions, 0 deletions
diff --git a/pam_bloom_check.c b/pam_bloom_check.c
new file mode 100644
index 0000000..01df1a7
--- /dev/null
+++ b/pam_bloom_check.c
@@ -0,0 +1,226 @@
+#define _GNU_SOURCE
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "bloom.h"
+
+#define PAM_BLOOM_FILTER_DIR "/etc/security/pam_bloom/filters.d"
+#define MAX_PASSWORD_LENGTH 4096
+
+static bool
+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(int fd, const char *filename)
+{
+ struct stat st;
+
+ if (fstat(fd, &st) < 0) {
+ perror(filename);
+ return -1;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ fprintf(stderr, "%s: not a regular file\n", filename);
+ return -1;
+ }
+
+ if (st.st_uid != 0) {
+ fprintf(stderr, "%s: not owned by root\n", filename);
+ return -1;
+ }
+
+ if (st.st_mode & (S_IWGRP | S_IWOTH)) {
+ fprintf(stderr, "%s: writable by group or others\n", filename);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+validate_filter_directory(int dirfd, const char *directory)
+{
+ struct stat st;
+
+ if (fstat(dirfd, &st) < 0) {
+ perror(directory);
+ return -1;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "%s: not a directory\n", directory);
+ return -1;
+ }
+
+ if (st.st_uid != 0) {
+ fprintf(stderr, "%s: not owned by root\n", directory);
+ return -1;
+ }
+
+ if (st.st_mode & (S_IWGRP | S_IWOTH)) {
+ fprintf(stderr, "%s: writable by group or others\n", directory);
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * Returns:
+ * 1 password matched a filter
+ * 0 password did not match
+ * -1 error
+ */
+static int
+check_filters(const char *directory, const char *password, size_t password_len)
+{
+ int dirfd = open(directory, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+
+ if (dirfd < 0) {
+ perror(directory);
+ return -1;
+ }
+
+ if (validate_filter_directory(dirfd, directory) < 0) {
+ close(dirfd);
+ return -1;
+ }
+
+ DIR *dir = fdopendir(dirfd);
+
+ if (dir == NULL) {
+ perror(directory);
+ close(dirfd);
+ return -1;
+ }
+
+ bool saw_filter = false;
+ int result = 0;
+
+ for (;;) {
+ errno = 0;
+ struct dirent *entry = readdir(dir);
+
+ if (entry == NULL) {
+ if (errno != 0) {
+ perror("readdir");
+ 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) {
+ perror(entry->d_name);
+ result = -1;
+ break;
+ }
+
+ if (validate_filter_fd(fd, entry->d_name) < 0) {
+ close(fd);
+ result = -1;
+ break;
+ }
+
+ bloomfilter_mapped bf = {0};
+ bloom_error_t err = bloom_map_fd(&bf, fd);
+
+ if (err != BF_SUCCESS) {
+ fprintf(stderr, "%s: %s\n", entry->d_name, bloom_strerror(err));
+ close(fd);
+ result = -1;
+ break;
+ }
+
+ bool found = bloom_mapped_lookup(&bf, password, password_len);
+
+ if (found) {
+ fprintf(stderr, "password matches filter: %s\n",
+ (const char *)bf.header->name);
+ }
+
+ bloom_unmap(&bf);
+ close(fd);
+
+ if (found) {
+ result = 1;
+ break;
+ }
+ }
+
+ if (result == 0 && !saw_filter) {
+ fprintf(stderr, "warning: no Bloom filters found in %s\n", directory);
+ }
+
+ if (closedir(dir) != 0 && result == 0) {
+ perror("closedir");
+ result = -1;
+ }
+
+ return result;
+}
+
+int
+main(void)
+{
+ char password[MAX_PASSWORD_LENGTH + 2];
+
+ if (fgets(password, sizeof(password), stdin) == NULL) {
+ if (ferror(stdin)) {
+ perror("stdin");
+ } else {
+ fprintf(stderr, "no password provided\n");
+ }
+
+ return 2;
+ }
+
+ size_t len = strlen(password);
+
+ if (len > 0 && password[len - 1] == '\n') {
+ password[--len] = '\0';
+ }
+
+ if (len > 0 && password[len - 1] == '\r') {
+ password[--len] = '\0';
+ }
+
+ /*
+ * If fgets() filled the buffer without seeing a newline, reject the
+ * input rather than silently checking a truncated password.
+ */
+ if (len == MAX_PASSWORD_LENGTH + 1) {
+ fprintf(stderr, "password exceeds maximum length\n");
+ return 2;
+ }
+
+ int result = check_filters(PAM_BLOOM_FILTER_DIR, password, len);
+
+ /* zero the plaintext password in memory. */
+ explicit_bzero(password, sizeof(password));
+
+ if (result < 0) return 2;
+ if (result > 0) return 1;
+ return 0;
+}