diff options
Diffstat (limited to 'pam_bloom.c')
| -rw-r--r-- | pam_bloom.c | 246 |
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; +} |
