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