hash-injector is a Rust library that gives you control to provide/reuse hash values. It allows you
to share hashes between equivalent instances of selected different types. That is not possible by
Rust's core::hash out of the box.
This works even for instances of type(s) that are 1:1 related/respective to instances of the other (value-bearing, "primary") type, but they do not carry any value matchable with instances of that other type. It is especially suitable for sequential/ordered/generated/unique assigned secondary/tertiary/quaternary... indexes.
When a Rust type implements (or derives) core::hash::Hash trait, it visits its value-forming fields (recursively by default) and collects their primitive values or their byte representation.
The actual hash value also depends on the initial state (ideally randomness-based state) of
core::hash::Hasher as created by
core::hash::BuildHasher. The
same BuildHasher instance creates Hasher instances with the same initial state, hence the hashes
are reproducible. However, different BuildHasher instances should create Hasher instances with
different initial state. So Hashers created by different BuildHashers should generate different
hashes (for the same objects), hence being Hash DoS-resistant.
That gives all hashable types (with a derived or well-written Hash implementation) protection
against Hash DoS attacks by default (if the BuildHasher/Hasher is Hash DoS-resistant, for
example like SipHash 1-3 used by default by
std::collections::HashMap).
However, Rust does NOT allow a type that implements Hash to provide/override its hash value
directly. Instead, the hash value is calculated by
core::Hash::Hasher::finish()
method, which is NOT called by the object being hashed, but by the code that requested the hash (for
example, HashMap). While that protects user types from mistakes, and it is secure by default, it
does limit some special cases.
How? You choose one type (value-bearing, "primary" type, not a secondary index) whose hash is
collected in the usual way (by #[derive(core::hash::Hash)] or by standard implementation of
core::hash::Hash). Its instances
are usually stored as hashed keys. When storing them, capture their hashes and store them as a part
of the the secondary index instances.
The secondary index types implement core::hash::Hash with a simple function call that
signals/injects the stored hash to a co-operating
core::hash::Hasher wrapper. At
the same time, the Hasher generates hashes for the value-bearing type and any other types as per
usual, still honoring Rust's cryptographic security.
You can combine this crate with any standard or custom
core::hash::BuildHasher (for
example,
std::collections::HashMap's
default BuildHasher:
std::hash::RandomState).
hash-injector provides a
core::hash::BuildHasher
adapter, with its
core::hash::Hasher adapter. The
Hasher adapter has two modes of behavior:
-
standard (and default): When hashing ordinary types (
core/std/3rd party), thisHashersends the data (collected byHashtrait) to the underlyingHasher, and it returns the hash generated by it, so all behaves as per usual. It is Hash DoS-resistant (if the underlyingBuildHasher/Hasheris Hash DoS-proof). -
specialized: The adapter allows custom types, selected by you, to inject the hash - rather than have the hash calculated. If the injected hash is Hash DoS-proof (like ones generated by standard
HashMap), then this use is also Hash DoS-resistant.
especially when one type implements core::borrow::Borrow
If you use this BuildHasher adapter with ordinary types (core/std/3rd party), the returned hash
value is the one calculated by the underlying BuildHasher.
The only exception is extreme and beyond any ordinary types: Hashing an array or slice of ZST
(zero-sized type) of size
core::primitive::usize::MAX
or core::primitive::usize::MAX-1 (or potentially a few more values near usize::MAX). This can
happen only for ZST's, because such array/slice lengths are not available for non-zero-sized types
(since those are limited by
core::primitive::isize::MAX
instead).
Since ZST's do not carry any data, it is fair to assume that they do not write anything in their
Hash::hash(...) - like hash(...) for
(). Well behaving ZST's could
write some constants, but the hash would still have the same zero entropy/quality. They could write
some thread-specific (Thread-local-based) constant values (if the ZST is
!core::marker::Send), and the
current Hash implementation for slices does honor that - but then all their instances (in the same
thread) are equal anyway, so the entropy/quality (within the thread that they are bound/limited to)
is zero again.
Compatible with any underlying Hashers and BuildHashers (whether they implement Rust feature
hasher_prefixfree_extras or not).
This crate does NOT use (compile time)
features to choose between various
flows. Instead, the consumer chooses the flow by providing a const generic parameter (of type
Flags).
The API is open to more flows or configuration in the future. The initial functions to create
const generic Flags parameters are new_flags_signal_first and new_flags_submit_first.
If/once we implement flows or configuration, there will be similar new functions. The existing
functions will take on defaults for any new configuration flags/options, and we may have more
similar functions for other combinations, like new_flags_signal_first_xxx and
new_flags_submit_first_xxx.
As of mid 2025, this crate needs nightly toolchain because of two Rust features:
hasher_prefixfree_extras- requiredadt_const_params- optional, used only withadt-const-paramsfeature of this crate.
- no
unsafecode (see#![forbid(unsafe_code)]inhash-injector-lib/src/lib.rs) asserts(compile time) feature (which asserts in both debug or release)- unit tests
- integration tests in hash-injector-tests, run for all combination of compile time features
- Please give thumbs up to both above features (rust-lang/rust#96762 and rust-lang/rust#95174).
- Please subscribe to peter-lyons-kehl/hash-injector#1 for low frequency announcements.