diff options
| author | Daniel Roberson <daniel@badoperation.net> | 2026-09-13 07:44:32 -0700 |
|---|---|---|
| committer | Daniel Roberson <daniel@badoperation.net> | 2026-09-13 07:44:32 -0700 |
| commit | f70ce379de6261733dc71279c76fc76a210285e1 (patch) | |
| tree | 07704c8156c03df696460b6c0edae6673174ecdf | |
initial commit
| -rw-r--r-- | .clang-format | 12 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | Makefile | 83 | ||||
| -rw-r--r-- | README.md | 290 | ||||
| -rw-r--r-- | bloom.c | 439 | ||||
| -rw-r--r-- | bloom.h | 76 | ||||
| -rw-r--r-- | mmh3.c | 105 | ||||
| -rw-r--r-- | mmh3.h | 11 | ||||
| -rw-r--r-- | pam-configs/pam_bloom | 7 | ||||
| -rw-r--r-- | pam_bloom.c | 246 | ||||
| -rw-r--r-- | pam_bloom_create.c | 236 | ||||
| -rw-r--r-- | rockyou.txt.bloom | bin | 0 -> 25780030 bytes | |||
| -rw-r--r-- | test_mmh3.c | 173 |
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 @@ -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. @@ -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; +} @@ -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 */ @@ -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; +} @@ -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 Binary files differnew file mode 100644 index 0000000..3f608a8 --- /dev/null +++ b/rockyou.txt.bloom 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; +} |
