summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Roberson <daniel@badoperation.net>2026-09-13 07:44:32 -0700
committerDaniel Roberson <daniel@badoperation.net>2026-09-13 07:44:32 -0700
commitf70ce379de6261733dc71279c76fc76a210285e1 (patch)
tree07704c8156c03df696460b6c0edae6673174ecdf
initial commit
-rw-r--r--.clang-format12
-rw-r--r--LICENSE21
-rw-r--r--Makefile83
-rw-r--r--README.md290
-rw-r--r--bloom.c439
-rw-r--r--bloom.h76
-rw-r--r--mmh3.c105
-rw-r--r--mmh3.h11
-rw-r--r--pam-configs/pam_bloom7
-rw-r--r--pam_bloom.c246
-rw-r--r--pam_bloom_create.c236
-rw-r--r--rockyou.txt.bloombin0 -> 25780030 bytes
-rw-r--r--test_mmh3.c173
13 files changed, 1699 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..99ce97f
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,12 @@
+BasedOnStyle: LLVM
+
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+
+ColumnLimit: 100
+
+BreakBeforeBraces: Attach
+
+BinPackArguments: true
+BinPackParameters: true
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8d71747
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Daniel Roberson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..35fe1e1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,83 @@
+CC ?= cc
+
+CFLAGS ?= -O2
+CFLAGS += -Wall -Wextra -Wpedantic
+CFLAGS += -fPIC
+CFLAGS += -fstack-protector-strong
+CFLAGS += -D_FORTIFY_SOURCE=2
+
+LDFLAGS += -Wl,-z,relro,-z,now
+
+PAM_LDLIBS = -lpam -lm
+CREATE_LDLIBS = -lm
+
+PAM_TARGET = pam_bloom.so
+CREATE_TARGET = pam_bloom_create
+
+PREFIX ?= /usr
+BINDIR ?= $(PREFIX)/bin
+
+MULTIARCH := $(shell dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)
+
+ifeq ($(MULTIARCH),)
+PAMDIR ?= $(PREFIX)/lib/security
+else
+PAMDIR ?= $(PREFIX)/lib/$(MULTIARCH)/security
+endif
+
+FILTERDIR ?= /etc/security/pam_bloom/filters.d
+DESTDIR ?=
+
+COMMON_OBJS = bloom.o mmh3.o
+PAM_OBJS = pam_bloom.o $(COMMON_OBJS)
+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
+
+.PHONY: all clean install uninstall
+
+all: $(PAM_TARGET) $(CREATE_TARGET)
+
+$(PAM_TARGET): $(PAM_OBJS)
+ $(CC) $(LDFLAGS) -shared -o $@ $(PAM_OBJS) $(PAM_LDLIBS)
+
+$(CREATE_TARGET): $(CREATE_OBJS)
+ $(CC) $(LDFLAGS) -o $@ $(CREATE_OBJS) $(CREATE_LDLIBS)
+
+pam_bloom.o: pam_bloom.c bloom.h
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+pam_bloom_create.o: pam_bloom_create.c bloom.h
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+bloom.o: bloom.c bloom.h mmh3.h
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+mmh3.o: mmh3.c mmh3.h
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+install: all
+ install -d "$(DESTDIR)$(BINDIR)"
+ install -m 0755 $(CREATE_TARGET) "$(DESTDIR)$(BINDIR)/$(CREATE_TARGET)"
+
+ install -d "$(DESTDIR)$(PAMDIR)"
+ install -m 0755 $(PAM_TARGET) "$(DESTDIR)$(PAMDIR)/$(PAM_TARGET)"
+
+ install -d -m 0755 "$(DESTDIR)$(FILTERDIR)"
+ install -m 0644 $(FILTERS) "$(DESTDIR)$(FILTERDIR)/"
+
+ install -d "$(DESTDIR)$(PAM_CONFIG_DIR)"
+ install -m 0644 $(PAM_CONFIG) \
+ "$(DESTDIR)$(PAM_CONFIG_DIR)/pam_bloom"
+
+uninstall:
+ rm -f "$(DESTDIR)$(BINDIR)/$(CREATE_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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..898b5fa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,290 @@
+# pam_bloom
+
+`pam_bloom` is a Linux PAM password module that rejects passwords
+found in one or more Bloom filters.
+
+It can be used to prevent users from choosing passwords found in known
+password lists such as RockYou, organization-specific password lists,
+or other administrator-provided dictionaries.
+
+The package includes:
+
+* `pam_bloom.so` — PAM module
+* `pam_bloom_create` — utility for creating Bloom filters from wordlists
+
+
+WARNING: This software modifies PAM configuration. There is always
+risk involved with re-configuring the system's authentication system
+such as locking legitimate users out. Administrators are responsible
+for testing their PAM configuration before deployment.
+
+## Requirements
+
+On Debian or Ubuntu, install the compiler, PAM development files, and
+other build tools:
+
+```sh
+sudo apt update
+sudo apt install build-essential libpam0g-dev
+```
+
+The Makefile also uses `dpkg-architecture` to determine Debian's
+multiarch PAM module directory. This is normally already available on
+Debian systems.
+
+## Build
+
+Clone or unpack the source and run:
+
+```sh
+make
+```
+
+This builds:
+
+```text
+pam_bloom.so
+pam_bloom_create
+```
+
+Install them with:
+
+```sh
+sudo make install
+```
+
+On a typical x86-64 Debian system this installs:
+
+```text
+/usr/bin/pam_bloom_create
+/usr/lib/x86_64-linux-gnu/security/pam_bloom.so
+/etc/security/pam_bloom/filters.d/
+```
+
+Finally, run `pam-auth-update` to enable `pam_bloom`:
+
+```sh
+sudo pam-auth-update
+```
+
+## Create a Bloom filter
+
+`pam_bloom_create` creates a Bloom filter from a wordlist:
+
+```sh
+pam_bloom_create WORDLIST OUTPUT NAME
+```
+
+For example:
+
+```sh
+pam_bloom_create rockyou.txt rockyou.bloom rockyou
+```
+
+The default false-positive rate is `0.001`.
+
+A different false-positive rate can be selected with `-p`:
+
+```sh
+pam_bloom_create -p 0.000001 rockyou.txt rockyou.bloom rockyou
+```
+
+Install the resulting filter:
+
+```sh
+sudo cp rockyou.bloom /etc/security/pam_bloom/filters.d/
+```
+
+The PAM module checks every `.bloom` file in this directory. A
+password matching any installed filter is rejected.
+
+## Configure PAM
+
+**Be careful when modifying PAM configuration. An incorrect PAM
+configuration can prevent authentication or password changes. Keep a
+root session open while testing changes.**
+
+On Debian and Ubuntu, edit:
+
+```text
+/etc/pam.d/common-password
+```
+
+Add `pam_bloom.so` before the `pam_unix.so` password entry.
+
+For example:
+
+```text
+password requisite pam_bloom.so
+password [success=1 default=ignore] pam_unix.so obscure yescrypt use_authtok
+password requisite pam_deny.so
+password required pam_permit.so
+```
+
+The important points are that `pam_bloom.so` runs before `pam_unix.so`
+and that `pam_unix.so` uses `use_authtok`.
+
+`pam_bloom` obtains the new password through PAM, checks it against
+the installed Bloom filters, and rejects it if a match is
+found. `pam_unix` then uses the same password token.
+
+## Test
+
+Change a test user's password normally:
+
+```sh
+passwd
+```
+
+Try selecting a password contained in one of the installed wordlists.
+
+`pam_bloom` should reject the password and identify the Bloom filter
+that matched it.
+
+A password that does not match any filter is passed on to the rest of
+the PAM password stack.
+
+## Custom filter directory
+
+The default filter directory is:
+
+```text
+/etc/security/pam_bloom/filters.d
+```
+
+A different directory can be configured with the PAM `dir=` option:
+
+```text
+password requisite pam_bloom.so dir=/srv/security/bloom
+```
+
+The directory and filter files must satisfy `pam_bloom`'s ownership
+and permission checks.
+
+## Included RockYou filter
+
+`pam_bloom` includes a pre-built Bloom filter generated from the
+RockYou password list:
+
+```text
+rockyou.txt.bloom
+```
+
+The supplied filter was created with a false-positive rate of:
+
+```text
+0.001
+```
+
+This filter is installed to
+`/etc/security/pam_bloom/filters.d/rockyou.txt.bloom` by default.
+
+Because this is a Bloom filter, there is a small probability of false
+positives. With the supplied filter, the configured false-positive
+probability is 0.001 (approximately 0.1%). In the 1 in 1000 chance
+that there is a false-positive, the user will simply have to choose
+another password.
+
+The original RockYou plaintext wordlist is not required at runtime.
+
+## Creating your own filters
+
+Additional Bloom filters can be created with:
+
+```sh
+pam_bloom_create WORDLIST OUTPUT NAME
+```
+
+For example:
+
+```sh
+pam_bloom_create passwords.txt passwords.bloom passwords
+```
+
+The default false-positive rate is `0.001`, the same value used for
+the supplied RockYou filter.
+
+A different false-positive rate can be selected with `-p`:
+
+```sh
+pam_bloom_create -p 0.000001 passwords.txt passwords.bloom passwords
+```
+
+Install the resulting filter by copying it into the filter directory:
+
+```sh
+sudo cp passwords.bloom /etc/security/pam_bloom/filters.d/
+```
+
+Multiple `.bloom` files may be installed at the same time. A password
+matching any installed filter is rejected.
+
+
+
+## Adding multiple filters
+
+Multiple filters can be installed simply by copying additional
+`.bloom` files into the filter directory:
+
+```text
+/etc/security/pam_bloom/filters.d/rockyou.bloom
+/etc/security/pam_bloom/filters.d/company.bloom
+/etc/security/pam_bloom/filters.d/banned-passwords.bloom
+```
+
+If the candidate password matches **any** filter, it is rejected.
+
+## Removing a filter
+
+Remove the corresponding `.bloom` file:
+
+```sh
+sudo rm /etc/security/pam_bloom/filters.d/rockyou.bloom
+```
+
+No rebuild of the PAM module is necessary.
+
+## Uninstall
+
+uninstall the binaries:
+
+```sh
+sudo make uninstall
+```
+
+`make uninstall` deliberately does not remove:
+
+```text
+/etc/security/pam_bloom/filters.d/
+```
+
+or any Bloom filters stored there. Administrator-created filters are
+preserved.
+
+Finally, update PAM configuration to remove `pam_bloom`:
+
+```sh
+sudo pam-auth-update
+```
+
+Alternatively, edit `/etc/pam.d/common-password` to remove `pam_bloom`
+
+## Security
+
+Bloom filters can produce false positives. The false-positive
+probability can be controlled when creating a filter with
+`pam_bloom_create -p`.
+
+The PAM module treats installed Bloom filters as trusted
+administrative data. Filter directories and files are checked for
+appropriate ownership and permissions before they are used.
+
+Malformed, inaccessible, or otherwise invalid filters cause the
+password check to fail closed rather than silently bypassing the
+password policy.
+
+## Murmur3
+
+The mmh3 implementation by Austin Appleby was referenced when
+implementing this. Appleby's implementation was released under the
+public domain.
diff --git a/bloom.c b/bloom.c
new file mode 100644
index 0000000..bf57819
--- /dev/null
+++ b/bloom.c
@@ -0,0 +1,439 @@
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+
+#include "mmh3.h"
+#include "bloom.h"
+
+static const char *const bloom_errors[] = {
+ "Success",
+ "Out of memory",
+ "Unable to open file",
+ "Unable to read file",
+ "Unable to write to file",
+ "fstat() failure",
+ "Invalid file format"
+};
+
+static const uint8_t bloom_magic[8] = { '!', 'b', 'l', 'o', 'o', 'm', 'f', '!' };
+
+static bool
+ideal_size(size_t expected, double accuracy, size_t *result)
+{
+ double bits = -((double)expected * log(accuracy)) / (log(2.0) * log(2.0));
+
+ bits = ceil(bits);
+
+ if (!isfinite(bits) || bits < 1.0 || bits > (double)SIZE_MAX) {
+ return false;
+ }
+
+ *result = (size_t)bits;
+ return true;
+}
+
+
+bloom_error_t bloom_init(bloomfilter *bf, const size_t expected, const double accuracy) {
+ if (!bf || expected == 0 || !isfinite(accuracy) || accuracy <= 0.0 || accuracy >= 1.0) {
+ return BF_INVALIDFILE;
+ }
+
+ float stored_accuracy = (float)accuracy;
+
+ if (!isfinite(stored_accuracy) || stored_accuracy <= 0.0f || stored_accuracy >= 1.0f) {
+ return BF_INVALIDFILE;
+ }
+
+ if (!ideal_size(expected, accuracy, &bf->size)) {
+ return BF_INVALIDFILE;
+ }
+
+ bf->bitmap_size = bf->size / 8 + ((bf->size % 8) != 0);
+
+ double hashes = ((double)bf->size / (double)expected) * log(2.0);
+
+ if (!isfinite(hashes) || hashes < 1.0 || hashes > (double)BLOOM_MAX_HASHCOUNT) {
+ return BF_INVALIDFILE;
+ }
+
+ bf->hashcount = (size_t)llround(hashes);
+
+ bf->expected = expected;
+ bf->accuracy = stored_accuracy;
+
+ snprintf(bf->name, sizeof(bf->name), "DEFAULT");
+
+ bf->bitmap = calloc(bf->bitmap_size, 1);
+
+ if (!bf->bitmap) {
+ return BF_OUTOFMEMORY;
+ }
+
+ return BF_SUCCESS;
+}
+
+void
+bloom_destroy(bloomfilter *bf) {
+ if (bf->bitmap) {
+ free(bf->bitmap);
+ bf->bitmap = NULL;
+ }
+}
+
+
+// lookup table for bloom_saturation() and bloom_saturation_count()
+static const uint8_t
+bit_count_table[256] = {
+ 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
+ 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+ 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+ 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+ 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+ 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+ 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+ 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
+};
+
+
+size_t
+bloom_saturation_count(const bloomfilter *bf) {
+ size_t count = 0;
+
+ for (size_t i = 0; i < bf->bitmap_size; i++) {
+ count += bit_count_table[bf->bitmap[i]];
+ }
+
+ return count;
+}
+
+float
+bloom_saturation(const bloomfilter *bf) {
+ size_t set_bits = bloom_saturation_count(bf);
+
+ return (float)set_bits / (float)bf->size * 100.0f;
+}
+
+
+void
+bloom_add(bloomfilter *bf, const void *element, const size_t len)
+{
+ uint64_t hash[2];
+
+ mmh3_128(element, len, 0, hash);
+
+ for (size_t i = 0; i < bf->hashcount; i++) {
+ uint64_t result = (hash[0] + i * hash[1]) % bf->size;
+
+ uint64_t byte_position = result / 8;
+ uint8_t bit_position = result % 8;
+
+ bf->bitmap[byte_position] |= (0x01 << bit_position);
+ }
+}
+
+bool
+bloom_set_name(bloomfilter *bf, const char *name)
+{
+ size_t len = strlen(name);
+
+ if (!bloom_valid_filter_name((const uint8_t *)name, len + 1))
+ return false;
+
+ memcpy(bf->name, name, len + 1);
+ return true;
+}
+
+static bloom_error_t
+bloom_write_file(const bloomfilter *bf, FILE *fp)
+{
+ bloomfilter_file bff = {0};
+
+ memcpy(bff.magic, bloom_magic, sizeof(bff.magic));
+
+ bff.version = BLOOM_FILE_VERSION;
+ bff.size = bf->size;
+ bff.hashcount = bf->hashcount;
+ bff.bitmap_size = bf->bitmap_size;
+ bff.expected = bf->expected;
+ bff.accuracy = bf->accuracy;
+
+ memcpy(bff.name, bf->name, sizeof(bff.name));
+
+ if (fwrite(&bff, sizeof(bff), 1, fp) != 1) {
+ return BF_FWRITE;
+ }
+
+ if (fwrite(bf->bitmap, 1, bf->bitmap_size, fp) != bf->bitmap_size) {
+ return BF_FWRITE;
+ }
+
+ return BF_SUCCESS;
+}
+
+bloom_error_t
+bloom_save(const bloomfilter *bf, const char *path)
+{
+ bloom_error_t result;
+ FILE *fp = NULL;
+ int fd = -1;
+
+ size_t path_len = strlen(path);
+
+ if (path_len > SIZE_MAX - sizeof(".tmp.XXXXXX")) {
+ return BF_FOPEN;
+ }
+
+ char *tmp_path = malloc(path_len + sizeof(".tmp.XXXXXX"));
+ if (tmp_path == NULL) {
+ return BF_OUTOFMEMORY;
+ }
+
+ snprintf(tmp_path, path_len + sizeof(".tmp.XXXXXX"), "%s.tmp.XXXXXX", path);
+
+ fd = mkstemp(tmp_path);
+ if (fd < 0) {
+ free(tmp_path);
+ return BF_FOPEN;
+ }
+
+ fp = fdopen(fd, "wb");
+ if (fp == NULL) {
+ close(fd);
+ unlink(tmp_path);
+ free(tmp_path);
+ return BF_FOPEN;
+ }
+
+ result = bloom_write_file(bf, fp);
+
+ if (result != BF_SUCCESS)
+ goto fail;
+
+ if (fflush(fp) != 0) {
+ result = BF_FWRITE;
+ goto fail;
+ }
+
+ if (fsync(fd) != 0) {
+ result = BF_FWRITE;
+ goto fail;
+ }
+
+ if (fclose(fp) != 0) {
+ fp = NULL;
+ result = BF_FWRITE;
+ goto fail_closed;
+ }
+
+ fp = NULL;
+
+ if (rename(tmp_path, path) != 0) {
+ result = BF_FWRITE;
+ goto fail_closed;
+ }
+
+ free(tmp_path);
+ return BF_SUCCESS;
+
+fail:
+ fclose(fp);
+ fp = NULL;
+
+fail_closed:
+ unlink(tmp_path);
+ free(tmp_path);
+
+ return result;
+}
+
+const char *
+bloom_strerror(bloom_error_t error) {
+ if (error < 0 || error >= BF_ERRORCOUNT) {
+ return "Unknown error";
+ }
+
+ return bloom_errors[error];
+}
+
+bool
+bloom_valid_filter_name(const uint8_t *name, size_t size)
+{
+ size_t len = 0;
+
+ while (len < size && name[len] != '\0') {
+ len++;
+ }
+
+ if (len == 0 || len == size) {
+ return false;
+ }
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = name[i];
+
+ if (!((c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ c == ' ' || c == '-' || c == '_' || c == '.')) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bloom_error_t
+validate_mapped_header(const bloomfilter_file *header, size_t file_size)
+{
+ if (memcmp(header->magic, bloom_magic, sizeof(header->magic)) != 0) {
+ return BF_INVALIDFILE;
+ }
+
+ if (header->version != BLOOM_FILE_VERSION) return BF_INVALIDFILE;
+ if (header->size == 0) return BF_INVALIDFILE;
+ if (header->hashcount == 0) return BF_INVALIDFILE;
+
+ /*
+ * Bound the amount of work for each lookup. Legitimate Bloom
+ * filters should require nowhere near this many hashes.
+ */
+ if (header->hashcount > BLOOM_MAX_HASHCOUNT) {
+ return BF_INVALIDFILE;
+ }
+
+ /*
+ * Compute ceil(size / 8) without doing size + 7,
+ * which could overflow.
+ */
+ uint64_t expected_bitmap_size = header->size / 8 + ((header->size % 8) != 0);
+ if (header->bitmap_size != expected_bitmap_size) {
+ return BF_INVALIDFILE;
+ }
+
+ /*
+ * Make sure converting bitmap_size to size_t is safe.
+ */
+ if (header->bitmap_size > SIZE_MAX) {
+ return BF_INVALIDFILE;
+ }
+
+ size_t bitmap_size = (size_t)header->bitmap_size;
+
+ /*
+ * Avoid overflow in sizeof(header) + bitmap_size
+ */
+ if (bitmap_size > SIZE_MAX - sizeof(bloomfilter_file)) {
+ return BF_INVALIDFILE;
+ }
+
+ if (sizeof(bloomfilter_file) + bitmap_size != file_size) {
+ return BF_INVALIDFILE;
+ }
+
+ /*
+ * Validate filter name. Shouldn't contain many special
+ * characters.
+ */
+ if (!bloom_valid_filter_name(header->name, sizeof(header->name))) {
+ return BF_INVALIDFILE;
+ }
+
+ if (!isfinite(header->accuracy)) {
+ return BF_INVALIDFILE;
+ }
+
+ if (header->accuracy <= 0.0f || header->accuracy >= 1.0f) {
+ return BF_INVALIDFILE;
+ }
+
+ if (header->expected == 0) {
+ return BF_INVALIDFILE;
+ }
+
+ return BF_SUCCESS;
+}
+
+bloom_error_t
+bloom_map_fd(bloomfilter_mapped *bf, int fd)
+{
+ struct stat st;
+
+ if (bf == NULL) return BF_INVALIDFILE;
+ if (fstat(fd, &st) < 0) return BF_FSTAT;
+ if (st.st_size < 0) return BF_INVALIDFILE;
+ if ((uintmax_t)st.st_size > SIZE_MAX) return BF_INVALIDFILE;
+
+ size_t file_size = (size_t)st.st_size;
+ if (file_size < sizeof(bloomfilter_file)) return BF_INVALIDFILE;
+
+ void *mapping = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (mapping == MAP_FAILED) {
+ return BF_FREAD;
+ }
+
+ const bloomfilter_file *header = (const bloomfilter_file *)mapping;
+
+ bloom_error_t err = validate_mapped_header(header, file_size);
+ if (err != BF_SUCCESS) {
+ munmap(mapping, file_size);
+ return err;
+ }
+
+ const uint8_t *bitmap = (const uint8_t *)mapping + sizeof(bloomfilter_file);
+
+ bf->header = header;
+ bf->bitmap = bitmap;
+ bf->mapping = mapping;
+ bf->mapping_size = file_size;
+
+ return BF_SUCCESS;
+}
+
+void
+bloom_unmap(bloomfilter_mapped *bf)
+{
+ if (bf->mapping) {
+ munmap(bf->mapping, bf->mapping_size);
+ }
+
+ bf->header = NULL;
+ bf->bitmap = NULL;
+ bf->mapping = NULL;
+ bf->mapping_size = 0;
+}
+
+bool
+bloom_mapped_lookup(const bloomfilter_mapped *bf, const void *element, const size_t len)
+{
+ uint64_t hash[2];
+
+ mmh3_128(element, len, 0, hash);
+
+ for (size_t i = 0; i < bf->header->hashcount; i++) {
+ uint64_t result = (hash[0] + i * hash[1]) % bf->header->size;
+
+ uint64_t byte_position = result / 8;
+ uint8_t bit_position = result % 8;
+
+ if ((bf->bitmap[byte_position] & (0x01 << bit_position)) == 0) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/bloom.h b/bloom.h
new file mode 100644
index 0000000..3b95f9b
--- /dev/null
+++ b/bloom.h
@@ -0,0 +1,76 @@
+
+#ifndef BLOOM_H
+#define BLOOM_H
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#define BLOOM_MAX_NAME_LENGTH 255
+#define BLOOM_MAX_HASHCOUNT 1024
+
+typedef enum {
+ BF_SUCCESS = 0,
+ BF_OUTOFMEMORY,
+ BF_FOPEN,
+ BF_FREAD,
+ BF_FWRITE,
+ BF_FSTAT,
+ BF_INVALIDFILE,
+ // ERRORCOUNT is used as a counter. do not add anything below this line.
+ BF_ERRORCOUNT
+} bloom_error_t;
+
+
+typedef struct {
+ size_t size; /**< Size of the Bloom filter in bits */
+ size_t hashcount; /**< Number of hashes performed per element */
+ size_t bitmap_size; /**< Size of the bitmap in bytes */
+ size_t expected; /**< Expected capacity of the filter */
+ float accuracy; /**< Desired margin of error */
+ char name[BLOOM_MAX_NAME_LENGTH + 1];
+ uint8_t *bitmap; /**< Pointer to the bitmap of the filter */
+} bloomfilter;
+
+#define BLOOM_FILE_VERSION 1
+
+typedef struct {
+ uint8_t magic[8];
+ uint32_t version;
+
+ uint8_t name[BLOOM_MAX_NAME_LENGTH + 1];
+
+ uint64_t size;
+ uint64_t hashcount;
+ uint64_t bitmap_size;
+ uint64_t expected;
+
+ float accuracy;
+} bloomfilter_file;
+
+
+typedef struct {
+ const bloomfilter_file *header;
+ const uint8_t *bitmap;
+
+ void *mapping;
+ size_t mapping_size;
+} bloomfilter_mapped;
+
+/* function declarations
+ */
+bloom_error_t bloom_init(bloomfilter *, const size_t, const double);
+void bloom_destroy(bloomfilter *);
+bool bloom_set_name(bloomfilter *, const char *);
+const char *bloom_strerror(const bloom_error_t);
+bloom_error_t bloom_save(const bloomfilter *, const char *);
+size_t bloom_saturation_count(const bloomfilter *);
+float bloom_saturation(const bloomfilter *);
+
+void bloom_add(bloomfilter *, const void *, const size_t);
+bloom_error_t bloom_map_fd(bloomfilter_mapped *, int);
+void bloom_unmap(bloomfilter_mapped *);
+bool bloom_valid_name(const char *name);
+
+bool bloom_mapped_lookup(const bloomfilter_mapped *, const void *, const size_t);
+#endif /* BLOOM_H */
diff --git a/mmh3.c b/mmh3.c
new file mode 100644
index 0000000..dbbb09a
--- /dev/null
+++ b/mmh3.c
@@ -0,0 +1,105 @@
+/* mmh3.c
+ */
+#include <string.h>
+#include <stdint.h>
+
+#include "mmh3.h"
+
+
+static uint64_t
+load64_le(const uint8_t *p)
+{
+ return ((uint64_t)p[0]) |
+ ((uint64_t)p[1] << 8) |
+ ((uint64_t)p[2] << 16) |
+ ((uint64_t)p[3] << 24) |
+ ((uint64_t)p[4] << 32) |
+ ((uint64_t)p[5] << 40) |
+ ((uint64_t)p[6] << 48) |
+ ((uint64_t)p[7] << 56);
+}
+
+/* mmh3_128() -- calculate 128 bit mmh3 hash.
+ *
+ * Args:
+ * key
+ * len
+ * seed
+ * out
+ *
+ * Returns:
+ * Nothing. Output is written to 'out'.
+ */
+void mmh3_128(const void *key, const size_t len, const uint64_t seed, uint64_t *out) {
+ const uint64_t c1 = 0x87c37b91114253d5;
+ const uint64_t c2 = 0x4cf5ad432745937f;
+ const uint8_t *data = (const uint8_t *)key;
+ const size_t nblocks = len / 16;
+ uint64_t h1 = seed;
+ uint64_t h2 = seed;
+
+ // Process the data in 128-bit chunks
+ for (size_t i = 0; i < nblocks; i++) {
+ const uint8_t *block = data + i * 16;
+
+ uint64_t k1 = load64_le(block);
+ uint64_t k2 = load64_le(block + 8);
+
+ k1 *= c1; k1 = (k1 << 31) | (k1 >> 33); k1 *= c2; h1 ^= k1;
+ h1 = (h1 << 27) | (h1 >> 37); h1 += h2; h1 = h1 * 5 + 0x52dce729;
+
+ k2 *= c2; k2 = (k2 << 33) | (k2 >> 31); k2 *= c1; h2 ^= k2;
+ h2 = (h2 << 31) | (h2 >> 33); h2 += h1; h2 = h2 * 5 + 0x38495ab5;
+ }
+
+ // Process the remaining bytes
+ const uint8_t *tail = (const uint8_t *)(data + nblocks * 16);
+ uint64_t k1 = 0;
+ uint64_t k2 = 0;
+
+ switch (len & 15) {
+ case 15: k2 ^= ((uint64_t)tail[14]) << 48; /* fall through */
+ case 14: k2 ^= ((uint64_t)tail[13]) << 40; /* fall through */
+ case 13: k2 ^= ((uint64_t)tail[12]) << 32; /* fall through */
+ case 12: k2 ^= ((uint64_t)tail[11]) << 24; /* fall through */
+ case 11: k2 ^= ((uint64_t)tail[10]) << 16; /* fall through */
+ case 10: k2 ^= ((uint64_t)tail[9]) << 8; /* fall through */
+ case 9: k2 ^= ((uint64_t)tail[8]) << 0; /* fall through */
+ k2 *= c2; k2 = (k2 << 33) | (k2 >> 31); k2 *= c1; h2 ^= k2;
+ /* fall through */
+ case 8: k1 ^= ((uint64_t)tail[7]) << 56; /* fall through */
+ case 7: k1 ^= ((uint64_t)tail[6]) << 48; /* fall through */
+ case 6: k1 ^= ((uint64_t)tail[5]) << 40; /* fall through */
+ case 5: k1 ^= ((uint64_t)tail[4]) << 32; /* fall through */
+ case 4: k1 ^= ((uint64_t)tail[3]) << 24; /* fall through */
+ case 3: k1 ^= ((uint64_t)tail[2]) << 16; /* fall through */
+ case 2: k1 ^= ((uint64_t)tail[1]) << 8; /* fall through */
+ case 1: k1 ^= ((uint64_t)tail[0]) << 0; /* fall through */
+ k1 *= c1; k1 = (k1 << 31) | (k1 >> 33); k1 *= c2; h1 ^= k1;
+ }
+
+ // Finalization
+ h1 ^= len;
+ h2 ^= len;
+
+ h1 += h2;
+ h2 += h1;
+
+ h1 ^= h1 >> 33;
+ h1 *= 0xff51afd7ed558ccd;
+ h1 ^= h1 >> 33;
+ h1 *= 0xc4ceb9fe1a85ec53;
+ h1 ^= h1 >> 33;
+
+ h2 ^= h2 >> 33;
+ h2 *= 0xff51afd7ed558ccd;
+ h2 ^= h2 >> 33;
+ h2 *= 0xc4ceb9fe1a85ec53;
+ h2 ^= h2 >> 33;
+
+ h1 += h2;
+ h2 += h1;
+
+ out[0] = h1;
+ out[1] = h2;
+}
diff --git a/mmh3.h b/mmh3.h
new file mode 100644
index 0000000..9ec6326
--- /dev/null
+++ b/mmh3.h
@@ -0,0 +1,11 @@
+/* mmh3.h
+ */
+#ifndef MMH3_H
+#define MMH3_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+void mmh3_128(const void *, const size_t, const uint64_t, uint64_t *);
+
+#endif /* MMH3_H */
diff --git a/pam-configs/pam_bloom b/pam-configs/pam_bloom
new file mode 100644
index 0000000..5bff319
--- /dev/null
+++ b/pam-configs/pam_bloom
@@ -0,0 +1,7 @@
+Name: pam_bloom password filter
+Default: yes
+Priority: 1024
+
+Password-Type: Primary
+Password:
+ requisite pam_bloom.so
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;
+}
diff --git a/pam_bloom_create.c b/pam_bloom_create.c
new file mode 100644
index 0000000..fb59a24
--- /dev/null
+++ b/pam_bloom_create.c
@@ -0,0 +1,236 @@
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <math.h>
+
+#include "bloom.h"
+
+
+static const size_t PAM_BLOOM_MAX_ENTRY_LENGTH = 4096;
+
+
+static uint64_t
+count_lines(FILE *fp)
+{
+ char *line = NULL;
+ size_t cap = 0;
+ uint64_t count = 0;
+
+ while (getline(&line, &cap, fp) != -1) {
+ count++;
+ }
+
+ free(line);
+
+ if (ferror(fp)) {
+ perror("getline");
+ exit(EXIT_FAILURE);
+ }
+
+ return count;
+}
+
+static size_t
+trim_newline(char *line, size_t len)
+{
+ if (len > 0 && line[len - 1] == '\n') {
+ len--;
+
+ if (len > 0 && line[len - 1] == '\r') {
+ len--;
+ }
+ }
+
+ return len;
+}
+
+static int
+validate_entry(const char *line, size_t len, uint64_t line_number)
+{
+ if (len == 0) {
+ fprintf(stderr, "line %llu: empty entry\n", (unsigned long long)line_number);
+ return -1;
+ }
+
+ if (len > PAM_BLOOM_MAX_ENTRY_LENGTH) {
+ fprintf(
+ stderr,
+ "line %llu: entry exceeds maximum length of %zu bytes\n",
+ (unsigned long long)line_number,
+ PAM_BLOOM_MAX_ENTRY_LENGTH
+ );
+
+ return -1;
+ }
+
+ if (memchr(line, '\0', len) != NULL) {
+ fprintf(stderr, "line %llu: entry contains NUL byte\n", (unsigned long long)line_number);
+ return -1;
+ }
+
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ double fpr = 0.001;
+ int opt;
+
+ while ((opt = getopt(argc, argv, "p:")) != -1) {
+ switch (opt) {
+ case 'p': {
+ char *end = NULL;
+
+ errno = 0;
+
+ double value = strtod(optarg, &end);
+
+ if (errno != 0 || end == optarg || *end != '\0' ||
+ !isfinite(value) || value <= 0.0 || value >= 1.0) {
+ fprintf(stderr, "invalid false-positive rate: %s\n", optarg);
+ return EXIT_FAILURE;
+ }
+
+ fpr = value;
+ break;
+ }
+
+ default:
+ fprintf(stderr, "usage: %s [-p FPR] WORDLIST OUTPUT NAME\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (argc - optind != 3) {
+ fprintf(stderr, "usage: %s [-p FPR] WORDLIST OUTPUT NAME\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *input = argv[optind];
+ const char *output = argv[optind + 1];
+ const char *name = argv[optind + 2];
+
+ if (!bloom_valid_filter_name((const uint8_t *)name, strlen(name) + 1)) {
+ fprintf(stderr, "invalid filter name\n");
+ return EXIT_FAILURE;
+ }
+
+ FILE *fp = fopen(input, "rb");
+ if (!fp) {
+ perror(input);
+ return EXIT_FAILURE;
+ }
+
+ uint64_t count = count_lines(fp);
+
+ if (count == 0) {
+ fprintf(stderr, "%s: empty wordlist\n", input);
+ fclose(fp);
+ return EXIT_FAILURE;
+ }
+
+ printf("entries: %llu\n", (unsigned long long)count);
+
+ if (fseeko(fp, 0, SEEK_SET) == -1) {
+ perror("fseeko");
+ fclose(fp);
+ return EXIT_FAILURE;
+ }
+
+ if (count > SIZE_MAX) {
+ fprintf(stderr, "%s: too many entries for this platform\n", input);
+ fclose(fp);
+ return EXIT_FAILURE;
+ }
+
+ bloomfilter bf = {0};
+ bloom_error_t err = bloom_init(&bf, (size_t)count, fpr);
+
+ if (err != BF_SUCCESS) {
+ fprintf(stderr, "bloom_init: %s\n", bloom_strerror(err));
+ fclose(fp);
+ return EXIT_FAILURE;
+ }
+
+ if (!bloom_set_name(&bf, name)) {
+ fprintf(stderr, "filter name too long\n");
+ bloom_destroy(&bf);
+ fclose(fp);
+ return EXIT_FAILURE;
+ }
+
+ char *line = NULL;
+ size_t capacity = 0;
+ ssize_t nread;
+
+ uint64_t inserted = 0;
+ uint64_t line_number = 0;
+
+ while ((nread = getline(&line, &capacity, fp)) != -1) {
+ line_number++;
+
+ size_t len = trim_newline(line, (size_t)nread);
+
+ if (validate_entry(line, len, line_number) < 0) {
+ free(line);
+ bloom_destroy(&bf);
+ fclose(fp);
+
+ return EXIT_FAILURE;
+ }
+
+ bloom_add(&bf, line, len);
+
+ inserted++;
+ }
+
+ if (ferror(fp)) {
+ perror("getline");
+
+ free(line);
+ bloom_destroy(&bf);
+ fclose(fp);
+
+ return EXIT_FAILURE;
+ }
+
+ free(line);
+ fclose(fp);
+
+ if (inserted != count) {
+ fprintf(stderr,
+ "input changed while processing (counted %llu, read %llu)\n",
+ (unsigned long long)count,
+ (unsigned long long)inserted);
+
+ bloom_destroy(&bf);
+ return EXIT_FAILURE;
+ }
+
+ err = bloom_save(&bf, output);
+
+ if (err != BF_SUCCESS) {
+ fprintf(stderr, "bloom_save: %s\n", bloom_strerror(err));
+
+ bloom_destroy(&bf);
+ return EXIT_FAILURE;
+ }
+
+ printf("filter: %s\n", name);
+ printf("elements: %llu\n", (unsigned long long)inserted);
+ printf("bits: %zu\n", bf.size);
+ printf("bytes: %zu\n", bf.bitmap_size);
+ printf("hashes: %zu\n", bf.hashcount);
+ printf("fpr: %.10g\n", fpr);
+ printf("saturation: %.2f%%\n", bloom_saturation(&bf));
+
+ bloom_destroy(&bf);
+
+ return EXIT_SUCCESS;
+}
diff --git a/rockyou.txt.bloom b/rockyou.txt.bloom
new file mode 100644
index 0000000..3f608a8
--- /dev/null
+++ b/rockyou.txt.bloom
Binary files differ
diff --git a/test_mmh3.c b/test_mmh3.c
new file mode 100644
index 0000000..8946f70
--- /dev/null
+++ b/test_mmh3.c
@@ -0,0 +1,173 @@
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mmh3.h"
+
+typedef struct {
+ const char *name;
+ const void *data;
+ size_t len;
+ uint64_t seed;
+ uint64_t expected0;
+ uint64_t expected1;
+} test_vector;
+
+static int
+test_mmh3_128_vector(const test_vector *test)
+{
+ uint64_t out[2] = {0, 0};
+
+ mmh3_128(
+ test->data,
+ test->len,
+ test->seed,
+ out
+ );
+
+ if (out[0] != test->expected0 ||
+ out[1] != test->expected1) {
+
+ fprintf(
+ stderr,
+ "FAIL: %s\n"
+ " expected: %016" PRIx64 " %016" PRIx64 "\n"
+ " actual: %016" PRIx64 " %016" PRIx64 "\n",
+ test->name,
+ test->expected0,
+ test->expected1,
+ out[0],
+ out[1]
+ );
+
+ return 1;
+ }
+
+ printf("PASS: %s\n", test->name);
+ return 0;
+}
+
+static int
+test_mmh3_128_unaligned(void)
+{
+ static const char text[] =
+ "The quick brown fox jumps over the lazy dog";
+
+ uint8_t buffer[sizeof(text) + 1];
+
+ /*
+ * Force the input to begin at an address offset by one byte.
+ * This catches implementations that incorrectly cast arbitrary
+ * byte pointers to uint64_t * and perform aligned word reads.
+ */
+ memcpy(buffer + 1, text, sizeof(text) - 1);
+
+ const test_vector test = {
+ .name = "x64_128 unaligned input",
+ .data = buffer + 1,
+ .len = sizeof(text) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0xe34bbc7bbc071b6c),
+ .expected1 = UINT64_C(0x7a433ca9c49a9347),
+ };
+
+ return test_mmh3_128_vector(&test);
+}
+
+
+int
+main(void)
+{
+ static const char empty[] = "";
+ static const char foo[] = "foo";
+ static const char hello[] = "hello";
+ static const char block16[] = "0123456789abcdef";
+ static const char block17[] = "0123456789abcdefg";
+ static const char quickfox[] =
+ "The quick brown fox jumps over the lazy dog";
+
+ static const test_vector tests[] = {
+ {
+ .name = "x64_128 empty, seed 0",
+ .data = empty,
+ .len = 0,
+ .seed = 0,
+ .expected0 = UINT64_C(0x0000000000000000),
+ .expected1 = UINT64_C(0x0000000000000000),
+ },
+ {
+ .name = "x64_128 foo, seed 0",
+ .data = foo,
+ .len = sizeof(foo) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0xe271865701f54561),
+ .expected1 = UINT64_C(0x7eaf87e42bba7d87),
+ },
+ {
+ .name = "x64_128 hello, seed 0",
+ .data = hello,
+ .len = sizeof(hello) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0xcbd8a7b341bd9b02),
+ .expected1 = UINT64_C(0x5b1e906a48ae1d19),
+ },
+ {
+ .name = "x64_128 exactly 16 bytes",
+ .data = block16,
+ .len = sizeof(block16) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0x4be06d94cf4ad1a7),
+ .expected1 = UINT64_C(0x87c35b5c63a708da),
+ },
+ {
+ .name = "x64_128 17 bytes",
+ .data = block17,
+ .len = sizeof(block17) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0x8e32612daa45f9de),
+ .expected1 = UINT64_C(0x0800f4c206c372ee),
+ },
+ {
+ .name = "x64_128 quick fox, seed 0",
+ .data = quickfox,
+ .len = sizeof(quickfox) - 1,
+ .seed = 0,
+ .expected0 = UINT64_C(0xe34bbc7bbc071b6c),
+ .expected1 = UINT64_C(0x7a433ca9c49a9347),
+ },
+ {
+ .name = "x64_128 foo, seed 42",
+ .data = foo,
+ .len = sizeof(foo) - 1,
+ .seed = 42,
+ .expected0 = UINT64_C(0xf4569d51637053f2),
+ .expected1 = UINT64_C(0xa279b5d8eeb09aa9),
+ },
+ };
+
+ int failures = 0;
+
+ for (size_t i = 0;
+ i < sizeof(tests) / sizeof(tests[0]);
+ i++) {
+
+ failures += test_mmh3_128_vector(&tests[i]);
+ }
+
+ failures += test_mmh3_128_unaligned();
+
+ if (failures != 0) {
+ fprintf(
+ stderr,
+ "\n%d test%s failed\n",
+ failures,
+ failures == 1 ? "" : "s"
+ );
+
+ return 1;
+ }
+
+ printf("\nAll mmh3 tests passed.\n");
+ return 0;
+}