Skip to content

Commit

Permalink
Make GxHash DOS resistant thanks to seed randomization
Browse files Browse the repository at this point in the history
  • Loading branch information
ogxd committed Nov 18, 2023
1 parent 5270d7d commit 031d322
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "gxhash"
authors = ["Olivier Giniaux"]
version = "2.1.0"
version = "2.2.0"
edition = "2021"
description = "GxHash non-cryptographic algorithm"
license = "MIT"
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ let mut hashset = GxHashSet::default();
hashset.insert("hello world");
```

> **Warning**
> This is a non-cryptographic hashing algorithm, thus it is not recommended to use it as a cryptographic algorithm (it is not a replacement for SHA).
## Compatibility
- ARM 64-bit using `NEON` intrinsics.
- x86-64 bit using `SSE2` + `AES` intrinsics.
Expand All @@ -40,6 +37,14 @@ hashset.insert("hello world");
> **Warning**
> Other platforms are currently not supported (there is no fallback)
## Security
### DOS Resistance ✅
GxHash is a seeded hashing algorithm, meaning that depending on the seed used, it will generate completely different hashes. The default `Hasher` (`GxHasher::default()`) randomizes the seed on creation, making it any `HashMap`/`HashSet` using this `Hasher` DOS resistant, as no attacker will be able to predict which hashes may collide without knowing the seed used.
### Multicollisions Resistance ✅
GxHash uses a 128-bit internal state (and even 256-bit with the `avx2` feature). This makes GxHash [a widepipe construction](https://en.wikipedia.org/wiki/Merkle%E2%80%93Damg%C3%A5rd_construction#Wide_pipe_construction) when generating hashes of size 64-bit or smaller, which had amongst other properties to be inherently more resistant to multicollision attacks. See [this paper](https://www.iacr.org/archive/crypto2004/31520306/multicollisions.pdf) for more details.
### Cryptographic Properties ❌
GxHash is a non-cryptographic hashing algorithm, thus it is not recommended to use it as a cryptographic algorithm (it is not a replacement for SHA). It has not been assessed if GxHash is preimage resistant and whether how likely it is to be reversed.

## Benchmarks
Displayed numbers are throughput in Mibibytes of data hashed per second. Higher is better.
To run the benchmarks: `cargo bench --bench throughput`.
Expand Down
113 changes: 107 additions & 6 deletions src/hasher.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
use std::hash::{Hasher, BuildHasherDefault};
use std::hash::{Hasher, BuildHasher};
use std::collections::{HashMap, HashSet};

use rand::Rng;

use crate::gxhash::*;
use crate::gxhash::platform::*;

/// A `Hasher` for hashing an arbitrary stream of bytes.
/// # Features
/// - The fastest [`Hasher`] of its class<sup>1</sup>, for all input sizes
/// - Highly collision resitant
/// - DOS resistant thanks to seed randomization when using [`GxHasher::default()`]
///
/// *<sup>1</sup>There might me faster alternatives, such as `fxhash` for very small input sizes, but that usually have low quality properties.*
pub struct GxHasher(State);

impl Default for GxHasher {
/// Creates a new hasher with a empty seed.
///
/// # Warning ⚠️
/// Not using a seed may make your [`Hasher`] vulnerable to DOS attacks.
/// It is recommended to use [`GxBuildHasher::default()`] to create a DOS resistant [`Hasher`]s.
///
/// # Example
///
/// ```
/// use std::hash::Hasher;
/// use gxhash::GxHasher;
///
/// let mut hasher = GxHasher::default();
///
/// hasher.write(b"Hello");
/// hasher.write_u32(123);
/// hasher.write_u8(42);
///
/// println!("Hash is {:x}!", hasher.finish());
/// ```
#[inline]
fn default() -> GxHasher {
GxHasher(unsafe { create_empty() })
Expand All @@ -15,6 +44,10 @@ impl Default for GxHasher {

impl GxHasher {
/// Creates a new hasher using the provided seed.
///
/// # Warning ⚠️
/// Hardcoding a seed may make your [`Hasher`] vulnerable to DOS attacks.
/// It is recommended to use [`GxBuildHasher::default()`] to create a DOS resistant [`Hasher`]s.
///
/// # Example
///
Expand Down Expand Up @@ -65,13 +98,30 @@ impl Hasher for GxHasher {
}
}

/// A builder for default GxHash hashers.
pub type GxBuildHasher = BuildHasherDefault<GxHasher>;
/// A builder for building DOS-resistant [`GxHasher`]s.
pub struct GxBuildHasher(i64);

impl Default for GxBuildHasher {
#[inline]
fn default() -> GxBuildHasher {
let mut rng = rand::thread_rng();
let seed: i64 = rng.gen::<i64>();
GxBuildHasher(seed)
}
}

impl BuildHasher for GxBuildHasher {
type Hasher = GxHasher;

fn build_hasher(&self) -> GxHasher {
GxHasher::with_seed(self.0)
}
}

/// A `HashMap` using a default GxHash hasher.
/// A `HashMap` using a (DOS-resistant) [`GxBuildHasher`].
pub type GxHashMap<K, V> = HashMap<K, V, GxBuildHasher>;

/// A `HashSet` using a default GxHash hasher.
/// A `HashSet` using a (DOS-resistant) [`GxBuildHasher`].
pub type GxHashSet<T> = HashSet<T, GxBuildHasher>;

#[cfg(test)]
Expand All @@ -80,7 +130,7 @@ mod tests {
use super::*;

#[test]
fn hasher_works() {
fn hasher_produces_stable_hashes() {
let mut hashset = GxHashSet::default();
assert!(hashset.insert(1234));
assert!(!hashset.insert(1234));
Expand All @@ -92,4 +142,55 @@ mod tests {
assert!(!hashset.insert("hello"));
assert!(hashset.insert("bye"));
}

// This is important for DOS resistance
#[test]
fn gxhashset_uses_default_gxhasherbuilder() {
let hashset_1 = GxHashSet::<u32>::default();
let hashset_2 = GxHashSet::<u32>::default();

let mut hasher_1 = hashset_1.hasher().build_hasher();
let mut hasher_2 = hashset_2.hasher().build_hasher();

hasher_1.write_i32(42);
let hash_1 = hasher_1.finish();

hasher_2.write_i32(42);
let hash_2 = hasher_2.finish();

assert_ne!(hash_1, hash_2);
}

// This is important for DOS resistance
#[test]
fn default_gxhasherbuilder_is_randomly_seeded() {
let buildhasher_1 = GxBuildHasher::default();
let buildhasher_2 = GxBuildHasher::default();

let mut hasher_1 = buildhasher_1.build_hasher();
let mut hasher_2 = buildhasher_2.build_hasher();

hasher_1.write_i32(42);
let hash_1 = hasher_1.finish();

hasher_2.write_i32(42);
let hash_2 = hasher_2.finish();

assert_ne!(hash_1, hash_2);
}

#[test]
fn gxhasherbuilder_builds_same_hashers() {
let buildhasher = GxBuildHasher::default();

let mut hasher = buildhasher.build_hasher();

hasher.write_i32(42);
let hash = hasher.finish();

let mut hasher = buildhasher.build_hasher();

hasher.write_i32(42);
assert_eq!(hash, hasher.finish());
}
}

0 comments on commit 031d322

Please sign in to comment.