Crack hashes or find collisions for data hashed with the FNV-1a algorithm without exhaustive search.
This implementation uses bounded lattice enumeration with FLINT-backed reduction to efficiently crack FNV-1a hashes. It supports standard 64-bit hashes, custom FNV parameters, and arbitrary-precision variants.
FNV-1a is not a cryptographic hash. This project is intended for reverse engineering, data recovery, collision research, and similar work involving known FNV-1a parameters.
Huge thank you to Connor McCartney for his writeup and the original Python proof of concept on which this project is based. I'd also like to thank Blupper for his work on linineq as a lot of the ideas from that are used here as well.
The Python package supports Python 3.11, 3.12, 3.13, 3.14, and the following:
- Linux x86_64
- Windows AMD64
- macOS x86_64
- macOS arm64
Install the package using pip or your package manager of choice (as long as it can pull from PyPI)
pip install fnvcrackThe Python package provides an easy to use API for standard and custom FNV-1a hashes.
More runnable demos are available in the examples/ directory.
from fnvcrack import CrackContext
target_hash = 0x25DA8C1836A8D66D
ctx = CrackContext(valid_chars=b"abcdefghijklmnopqrstuvwxyz")
# `crack_len` is the number of unknown bytes. Known prefixes
# and suffixes do not count toward it.
result = ctx.crack(target_hash, crack_len=8)
if result.ok:
print(result.value)This prints:
b'abcdefgh'
ctx = CrackContext(
prefix=b"archive/",
suffix=b".bin",
valid_chars=b"0123456789abcdef",
)
result = ctx.crack(target_hash, crack_len=8)# Set `incremental=True` to search every unknown length from 1 through `crack_len`
result = ctx.crack(target_hash, crack_len=12, incremental=True)Set threads above 1 to enumerate one shared lattice with native workers:
result = ctx.crack(target_hash, crack_len=8, threads=4)The default is threads=1. The value is a maximum worker count for one shared
lattice enumeration per exact search length. Callbacks work as normal but order
is unspecified.
Note
Use this for longer searches. Short searches (<10 chars) may be slower due to worker and scheduling overhead.
Use enum_bound to control the local search radius around the lattice
solution. Larger values search more candidates but can be much slower.
result = ctx.crack(
target_hash,
crack_len=8,
enum_bound=6,
max_enum_candidates=1_000_000,
)The default enum_bound is 4 as in my testing that seems to be the best middle
ground for success rates and speed.
By default, crack() stops at the first verified match. Pass a callback to
inspect each complete verified candidate as bytes. A truthy return accepts
that candidate and stops while a falsey return rejects it and continues:
matches = []
def accept(candidate: bytes) -> bool:
matches.append(candidate)
return len(matches) == 10
result = ctx.crack(target_hash, crack_len=8, callback=accept)Returning false every time runs until the bounded search is exhausted.
batch_crack() cracks multiple targets in parallel and preserves their input
order:
from fnvcrack import CrackContext
target_hashes = [
0x25DA8C1836A8D66D,
0x27E64FF62868579D,
]
if __name__ == "__main__":
ctx = CrackContext(valid_chars=b"abcdefghijklmnopqrstuvwxyz")
values = ctx.batch_crack(target_hashes, crack_len=8)The if __name__ == "__main__" guard is required on platforms that use
spawn-based multiprocessing.
The repository also exposes the underlying C API:
#include <inttypes.h>
#include <stdio.h>
#include "context.h"
#include "crack.h"
int main(void) {
CREATE_CONTEXT(ctx);
if (!init_crack_ctx(
ctx,
UINT64_C(0xCBF29CE484222325),
UINT64_C(0x100000001B3),
64,
"abcdefghijklmnopqrstuvwxyz",
NULL,
NULL
)) {
return 1;
}
char_buffer result = {NULL, 0};
CrackResult status = crack_u64_with_len(
ctx,
UINT64_C(0x25DA8C1836A8D66D),
&result,
8
);
if (status == SUCCESS) {
printf("Cracked: %s\n", result.data);
}
clear_char_buffer(&result);
destroy_crack_ctx(ctx);
return status == SUCCESS ? 0 : 1;
}Run the test suite:
uv run --locked --group dev pytestBuild the Python extension in place:
make build-pyextRun Python and native C coverage:
make coverage-cRun the benchmark CLI:
uv run --locked --group benchmark python benchmarking/bench.py perf \
-p '[a-z]{8}' -c '[a-z]' -n 100 -b 4 -m 0 -IThe Wheels workflow builds and tests CPython 3.11 through 3.14 on Linux, Windows, and macOS. The Linux and Windows wheel builds consume committed native dependency locks. Regenerate both only when the FLINT specification changes:
make lock-nativeBuilding from source requires a C11 compiler, pkg-config, and
FLINT. FLINT depends on GMP and MPFR, which are
installed automatically by the package managers below.
sudo apt install -y build-essential pkg-config libflint-devbrew install flint pkgconfThe source build reads FLINT's pkg-config metadata, which supplies the
include and library directories for FLINT, GMP, and MPFR. For a nonstandard
FLINT installation, add the directory containing flint.pc to
PKG_CONFIG_PATH.
Clone the repository and install the development environment:
git clone https://github.com/Nico-Posada/fnv-hash-cracking.git
cd fnv-hash-cracking
uv sync --group devTo build the standalone C example:
make
./main