#define PAM_SM_PASSWORD #include #include #include #include #include #include #include #include #include #include #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; }