diff options
| author | Daniel Roberson <daniel@badoperation.net> | 2026-09-13 08:25:52 -0700 |
|---|---|---|
| committer | Daniel Roberson <daniel@badoperation.net> | 2026-09-13 08:25:52 -0700 |
| commit | 5f28840a5c365099f0e0d855b19fa0c13c9020c0 (patch) | |
| tree | ff51d7cea6ce33d6dd7ae073a6c7940983fefd08 /pam_bloom_check.c | |
| parent | d556c740a65f1468e3dadaeba25f67ea089ea25a (diff) | |
add pam_bloom_check
Diffstat (limited to 'pam_bloom_check.c')
| -rw-r--r-- | pam_bloom_check.c | 226 |
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; +} |
