summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile38
-rw-r--r--pam_bloom_check.c226
2 files changed, 251 insertions, 13 deletions
diff --git a/Makefile b/Makefile
index 35fe1e1..84f895d 100644
--- a/Makefile
+++ b/Makefile
@@ -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;
+}