diff options
| -rw-r--r-- | Makefile | 38 | ||||
| -rw-r--r-- | pam_bloom_check.c | 226 |
2 files changed, 251 insertions, 13 deletions
@@ -14,19 +14,23 @@ CREATE_LDLIBS = -lm PAM_TARGET = pam_bloom.so CREATE_TARGET = pam_bloom_create -PREFIX ?= /usr -BINDIR ?= $(PREFIX)/bin +CHECK_TARGET = pam_bloom_check +CHECK_OBJS = pam_bloom_check.o $(COMMON_OBJS) +CHECK_LDLIBS = -lm -MULTIARCH := $(shell dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null) +PREFIX ?= /usr +BINDIR ?= $(PREFIX)/bin + +MULTIARCH := $(shell dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null) ifeq ($(MULTIARCH),) -PAMDIR ?= $(PREFIX)/lib/security +PAMDIR ?= $(PREFIX)/lib/security else -PAMDIR ?= $(PREFIX)/lib/$(MULTIARCH)/security +PAMDIR ?= $(PREFIX)/lib/$(MULTIARCH)/security endif -FILTERDIR ?= /etc/security/pam_bloom/filters.d -DESTDIR ?= +FILTERDIR ?= /etc/security/pam_bloom/filters.d +DESTDIR ?= COMMON_OBJS = bloom.o mmh3.o PAM_OBJS = pam_bloom.o $(COMMON_OBJS) @@ -35,11 +39,11 @@ CREATE_OBJS = pam_bloom_create.o $(COMMON_OBJS) FILTERS = rockyou.txt.bloom PAM_CONFIG_DIR ?= /usr/share/pam-configs -PAM_CONFIG = pam-configs/pam_bloom +PAM_CONFIG = pam-configs/pam_bloom .PHONY: all clean install uninstall -all: $(PAM_TARGET) $(CREATE_TARGET) +all: $(PAM_TARGET) $(CREATE_TARGET) $(CHECK_TARGET) $(PAM_TARGET): $(PAM_OBJS) $(CC) $(LDFLAGS) -shared -o $@ $(PAM_OBJS) $(PAM_LDLIBS) @@ -47,6 +51,12 @@ $(PAM_TARGET): $(PAM_OBJS) $(CREATE_TARGET): $(CREATE_OBJS) $(CC) $(LDFLAGS) -o $@ $(CREATE_OBJS) $(CREATE_LDLIBS) +$(CHECK_TARGET): $(CHECK_OBJS) + $(CC) $(LDFLAGS) -o $@ $(CHECK_OBJS) $(CHECK_LDLIBS) + +pam_bloom_check.o: pam_bloom_check.c bloom.h + $(CC) $(CFLAGS) -c -o $@ $< + pam_bloom.o: pam_bloom.c bloom.h $(CC) $(CFLAGS) -c -o $@ $< @@ -70,14 +80,16 @@ install: all install -m 0644 $(FILTERS) "$(DESTDIR)$(FILTERDIR)/" install -d "$(DESTDIR)$(PAM_CONFIG_DIR)" - install -m 0644 $(PAM_CONFIG) \ - "$(DESTDIR)$(PAM_CONFIG_DIR)/pam_bloom" + install -m 0644 $(PAM_CONFIG) "$(DESTDIR)$(PAM_CONFIG_DIR)/pam_bloom" + + install -m 0755 $(CHECK_TARGET) "$(DESTDIR)$(BINDIR)/$(CHECK_TARGET)" uninstall: rm -f "$(DESTDIR)$(BINDIR)/$(CREATE_TARGET)" + rm -f "$(DESTDIR)$(BINDIR)/$(CHECK_TARGET)" rm -f "$(DESTDIR)$(PAMDIR)/$(PAM_TARGET)" rm -f "$(DESTDIR)$(PAM_CONFIG_DIR)/pam_bloom" clean: - rm -f $(PAM_TARGET) $(CREATE_TARGET) \ - $(PAM_OBJS) pam_bloom_create.o + rm -f $(PAM_TARGET) $(CREATE_TARGET) $(CHECK_TARGET) \ + $(PAM_OBJS) $(CREATE_OBJS) $(CHECK_OBJS) 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; +} |
