Skip to content

Repository files navigation

fnv-hash-cracking

Wheels Python 3.11-3.14

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.

Credits

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.

Requirements

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

Installation

Install the package using pip or your package manager of choice (as long as it can pull from PyPI)

pip install fnvcrack

Examples

The Python package provides an easy to use API for standard and custom FNV-1a hashes.

More runnable demos are available in the examples/ directory.

Basic Example

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'

Known Prefixes and Suffixes

ctx = CrackContext(
    prefix=b"archive/",
    suffix=b".bin",
    valid_chars=b"0123456789abcdef",
)
result = ctx.crack(target_hash, crack_len=8)

Unknown Length

# Set `incremental=True` to search every unknown length from 1 through `crack_len`
result = ctx.crack(target_hash, crack_len=12, incremental=True)

Multithreaded Cracking

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.

Enumeration Limits

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.

Inspecting Multiple Matches

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 Cracking

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.

C Usage

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;
}

Development

Run the test suite:

uv run --locked --group dev pytest

Build the Python extension in place:

make build-pyext

Run Python and native C coverage:

make coverage-c

Run 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 -I

The 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-native

Building From Source

Building 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.

Ubuntu and Debian

sudo apt install -y build-essential pkg-config libflint-dev

macOS with Homebrew

brew install flint pkgconf

The 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 dev

To build the standalone C example:

make
./main

About

Crack hashes or find collisions for hashes hashed with the fnv-1a algorithm without full brute force

Resources

Stars

12 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages