Skip to content

Commit

Permalink
refactor: Rework module and feature flag for better v4/v4.2 split
Browse files Browse the repository at this point in the history
  • Loading branch information
Bluefinger committed Apr 17, 2024
1 parent a15429d commit 24b4ec7
Show file tree
Hide file tree
Showing 19 changed files with 1,039 additions and 177 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ jobs:
- name: Run cargo check
run: cargo check --all --all-features --all-targets
if: startsWith(matrix.rust, 'nightly')
- name: Run cargo check (without dev-dependencies to catch missing feature flags)
- name: Run cargo check
if: startsWith(matrix.rust, 'nightly')
run: cargo check -Z features=dev_dep
- name: Full feature + v4.2 testing
run: cargo check
- name: Full feature testing
run: cargo test --all-features
- name: v4 compatibility testing
run: cargo test --no-default-features --features debug,rand_core,wyhash,randomised_wyhash
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ wyhash = []
randomised_wyhash = ["wyhash", "dep:getrandom"]
fully_randomised_wyhash = ["randomised_wyhash"]
threadrng_wyhash = ["dep:rand", "randomised_wyhash"]
v4_2 = []
legacy_v4 = []

[dependencies]
getrandom = { version = "0.2", optional = true }
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A fast & portable non-cryptographic pseudorandom number generator written in Rus

The implementations for both the PRNG and hasher are based on the C reference implementation [wyhash](https://github.com/wangyi-fudan/wyhash), a simple and fast hasher but **not** cryptographically secure. It's known to be extremely fast and performant while still having great statistical properties.

This crate provides both the v4.2 final implementation of the WyRand/WyHash algorithm, or the older final v4 implementation. The two versions have different outputs due to changes in the algorithm and also with the constants used. Currently by default, the older final v4 algorithm will be used. In the future, this will be changed to the newer algorithm to be the default, but the old implementation will remain for backwards compatibility reasons.
This crate provides both the v4.2 final implementation of the WyRand/WyHash algorithm, or the older final v4 implementation. The two versions have different outputs due to changes in the algorithm and also with the constants used. By default, the final v4.2 algorithm will be used. If one needs to use the older, legacy v4 implementation for compatibility/stability reasons, the legacy hasher and PRNG can be exposed by enabling the `legacy_v4` feature flag.

This crate can be used on its own or be integrated with `rand_core`/`rand`, and it is `no-std` compatible. Minimum compatible Rust version is 1.70. This crate is also implemented with no unsafe code via `#![forbid(unsafe_code)]`.

Expand Down Expand Up @@ -38,7 +38,7 @@ The crate will always export `WyRand` and will do so when set as `default-featur
- **`randomised_wyhash`** - Enables `RandomWyHashState`, a means to source a randomised state for `WyHash` for use in collections like `HashMap`/`HashSet`. Enables `wyhash` feature if it is not already enabled.
- **`fully_randomised_wyhash`** - Randomises not just the seed for `RandomWyHashState`, but also the secret. The new secret is generated once per runtime, and then is used for every subsequent new `WyHash` (with each `WyHash` instance having its own unique seed). Enables `randomised_wyhash` if not already enabled, and requires `std` environments.
- **`threadrng_wyhash`** - Enables sourcing entropy from `rand`'s `thread_rng()` method. Much quicker than `getrandom`. Enables `randomised_wyhash` if not already enabled. Requires `std` environments.
- **`v4_2`** - Switches the PRNG/Hashing algorithms to use the final v4.2 implementation.
- **`legacy_v4`** - Exposes the legacy PRNG/Hashing algorithms that use the final v4 implementation.

## Building for WASM/Web

Expand Down
31 changes: 6 additions & 25 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
#[cfg(not(feature = "v4_2"))]
mod v4 {
pub(crate) const WY0: u64 = 0xa076_1d64_78bd_642f;
pub(crate) const WY1: u64 = 0xe703_7ed1_a0b4_28db;
#[cfg(feature = "wyhash")]
pub(crate) const WY2: u64 = 0x8ebc_6af0_9c88_c6e3;
#[cfg(feature = "wyhash")]
pub(crate) const WY3: u64 = 0x5899_65cc_7537_4cc3;
}

#[cfg(feature = "v4_2")]
mod v4_2 {
pub(crate) const WY0: u64 = 0x2d35_8dcc_aa6c_78a5;
pub(crate) const WY1: u64 = 0x8bb8_4b93_962e_acc9;
#[cfg(feature = "wyhash")]
pub(crate) const WY2: u64 = 0x4b33_a62e_d433_d4a3;
#[cfg(feature = "wyhash")]
pub(crate) const WY3: u64 = 0x4d5a_2da5_1de1_aa47;
}

#[cfg(not(feature = "v4_2"))]
pub(crate) use v4::*;

#[cfg(feature = "v4_2")]
pub(crate) use v4_2::*;
pub(crate )const C_VALUES: &[u8] = &[
15, 23, 27, 29, 30, 39, 43, 45, 46, 51, 53, 54, 57, 58, 60, 71, 75, 77, 78, 83, 85, 86, 89, 90,
92, 99, 101, 102, 105, 106, 108, 113, 114, 116, 120, 135, 139, 141, 142, 147, 149, 150, 153,
154, 156, 163, 165, 166, 169, 170, 172, 177, 178, 180, 184, 195, 197, 198, 201, 202, 204, 209,
210, 212, 216, 225, 226, 228, 232, 240,
];
24 changes: 24 additions & 0 deletions src/final_v4_2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[cfg(feature = "randomised_wyhash")]
mod builder;
mod constants;
#[cfg(feature = "wyhash")]
mod hasher;
#[cfg(feature = "wyhash")]
mod primes;
#[cfg(feature = "wyhash")]
mod secret;
mod wyrand;

#[cfg(feature = "randomised_wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "randomised_wyhash")))]
pub use builder::RandomWyHashState;

#[cfg(feature = "wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "wyhash")))]
pub use hasher::WyHash;

#[cfg(feature = "wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "wyhash")))]
pub use secret::Secret;

pub use wyrand::WyRand;
32 changes: 15 additions & 17 deletions src/hasher/builder.rs → src/final_v4_2/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use core::fmt::Debug;
#[cfg(feature = "fully_randomised_wyhash")]
use std::sync::OnceLock;

use crate::WyHash;
use super::{secret::Secret, WyHash};

#[cfg(feature = "fully_randomised_wyhash")]
static SECRET: OnceLock<[u64; 4]> = OnceLock::new();
static SECRET: OnceLock<Secret> = OnceLock::new();

#[inline]
fn get_random_u64() -> u64 {
Expand Down Expand Up @@ -37,21 +37,20 @@ fn get_random_u64() -> u64 {
}
}

#[cfg_attr(docsrs, doc(cfg(feature = "randomised_wyhash")))]
#[derive(Clone, Copy)]
/// Randomised state constructor for [`WyHash`]. This builder will source entropy in order
/// to provide random seeds for [`WyHash`]. If the `fully_randomised_wyhash` feature is enabled,
/// this will yield a hasher with not just a random seed, but also a new random secret,
/// granting extra protection against DOS and prediction attacks.
pub struct RandomWyHashState {
state: u64,
secret: [u64; 4],
secret: Secret,
}

impl RandomWyHashState {
/// Create a new [`RandomWyHashState`] instance. Calling this method will attempt to
/// draw entropy from hardware/OS sources. If `fully_randomised_wyhash` feature is enabled,
/// then it will use a randomised `secret` as well, otherwise it uses the default wyhash constants.
/// then it will use a randomised `secret` as well, otherwise it uses the default wyhash constants.
///
/// # Panics
///
Expand All @@ -71,22 +70,21 @@ impl RandomWyHashState {
#[inline]
pub fn new() -> Self {
#[cfg(feature = "fully_randomised_wyhash")]
use crate::hasher::secret::make_secret;

use super::secret::make_secret;
#[cfg(not(feature = "fully_randomised_wyhash"))]
use crate::constants::{WY0, WY1, WY2, WY3};
use super::constants::{WY0, WY1, WY2, WY3};

#[cfg(feature = "fully_randomised_wyhash")]
let secret = *SECRET.get_or_init(|| make_secret(get_random_u64()));
#[cfg(not(feature = "fully_randomised_wyhash"))]
let secret = [WY0, WY1, WY2, WY3];
let secret = Secret::new(WY0, WY1, WY2, WY3);

Self::new_with_secret(secret)
}

/// Create a new [`RandomWyHashState`] instance with a provided secret. Calling this method
/// will attempt to draw entropy from hardware/OS sources, and assumes the user provided the
/// secret via [`super::secret::make_secret`].
/// secret via [`WyHash::make_secret`].
///
/// # Panics
///
Expand All @@ -95,16 +93,16 @@ impl RandomWyHashState {
/// # Examples
///
/// ```
/// use wyrand::{RandomWyHashState, make_secret};
/// use core::hash::BuildHasher;
/// use wyrand::{RandomWyHashState, WyHash};
///
/// let s = RandomWyHashState::new_with_secret(make_secret(42));
/// let s = RandomWyHashState::new_with_secret(WyHash::make_secret(42));
///
/// let mut hasher = s.build_hasher(); // Creates a WyHash instance with random state
/// ```
#[must_use]
#[inline]
pub fn new_with_secret(secret: [u64; 4]) -> Self {
pub fn new_with_secret(secret: Secret) -> Self {
Self {
state: get_random_u64(),
secret,
Expand All @@ -131,7 +129,7 @@ impl Default for RandomWyHashState {
#[cfg(feature = "debug")]
impl Debug for RandomWyHashState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RandomisedWyHashBuilder")
f.debug_struct("RandomisedWyHashState")
.finish_non_exhaustive()
}
}
Expand All @@ -151,7 +149,7 @@ mod tests {

assert_eq!(
format!("{builder:?}"),
"RandomisedWyHashBuilder { .. }",
"RandomisedWyHashState { .. }",
"Debug should not be leaking internal state"
);
}
Expand All @@ -171,9 +169,9 @@ mod tests {
// same as the default secret.
#[cfg(feature = "fully_randomised_wyhash")]
{
use crate::constants::{WY0, WY1, WY2, WY3};
use super::super::constants::{WY0, WY1, WY2, WY3};

let default_secret = [WY0, WY1, WY2, WY3];
let default_secret = Secret::new(WY0, WY1, WY2, WY3);

assert_ne!(&builder1.secret, &default_secret);
assert_ne!(&builder2.secret, &default_secret);
Expand Down
6 changes: 6 additions & 0 deletions src/final_v4_2/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(super) const WY0: u64 = 0x2d35_8dcc_aa6c_78a5;
pub(super) const WY1: u64 = 0x8bb8_4b93_962e_acc9;
#[cfg(feature = "wyhash")]
pub(super) const WY2: u64 = 0x4b33_a62e_d433_d4a3;
#[cfg(feature = "wyhash")]
pub(super) const WY3: u64 = 0x4d5a_2da5_1de1_aa47;
72 changes: 28 additions & 44 deletions src/hasher.rs → src/final_v4_2/hasher.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
#[cfg(feature = "randomised_wyhash")]
mod builder;
#[cfg(feature = "v4_2")]
mod primes;
mod read;
mod secret;

use core::hash::Hasher;

#[cfg(feature = "randomised_wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "randomised_wyhash")))]
pub use builder::RandomWyHashState;

#[cfg(feature = "debug")]
use core::fmt::Debug;

use crate::{
constants::{WY0, WY1, WY2, WY3},
read::{read_4_bytes, read_8_bytes, read_upto_3_bytes},
utils::{wymix, wymul},
};

use self::read::{is_over_48_bytes, read_4_bytes, read_8_bytes, read_upto_3_bytes};

pub use self::secret::make_secret;
use super::{
constants::{WY0, WY1, WY2, WY3},
secret::{make_secret, Secret},
};

/// The WyHash hasher, a fast & portable hashing algorithm. This implementation is
/// based on the final v4/v4.2 C reference implementations (depending on whether the
/// `v4_2` feature flag is enabled or not).
/// based on the final v4.2 C reference implementation.
///
/// ```
/// use wyrand::WyHash;
Expand All @@ -46,17 +35,23 @@ pub use self::secret::make_secret;
/// Any other sequence of events (including calls to `write_u32` or similar functions) are
/// guaranteed to have consistent results between platforms and versions of this crate, but may not
/// map well to the reference implementation.
#[cfg_attr(docsrs, doc(cfg(feature = "wyhash")))]
#[derive(Clone)]
pub struct WyHash {
seed: u64,
lo: u64,
hi: u64,
size: u64,
secret: [u64; 4],
secret: Secret,
}

impl WyHash {
/// thing
#[must_use]
#[inline]
pub fn make_secret(seed: u64) -> Secret {
make_secret(seed)
}

/// Create hasher with seeds for the state and secret (generates a new secret, expensive to compute).
pub const fn new(seed: u64, secret_seed: u64) -> Self {
Self::new_with_secret(seed, make_secret(secret_seed))
Expand All @@ -65,14 +60,14 @@ impl WyHash {
/// Create hasher with a seed and default secrets
#[inline]
pub const fn new_with_default_secret(seed: u64) -> Self {
Self::new_with_secret(seed, [WY0, WY1, WY2, WY3])
Self::new_with_secret(seed, Secret::new(WY0, WY1, WY2, WY3))
}

/// Create hasher with a seed value and a secret. Assumes the user created the secret with [`make_secret`],
/// else the hashing output will be weak/vulnerable.
#[inline]
pub const fn new_with_secret(mut seed: u64, secret: [u64; 4]) -> Self {
seed ^= wymix(seed ^ secret[0], secret[1]);
pub const fn new_with_secret(mut seed: u64, secret: Secret) -> Self {
seed ^= wymix(seed ^ secret.first(), secret.second());

WyHash {
seed,
Expand Down Expand Up @@ -100,21 +95,21 @@ impl WyHash {
let mut start = 0;
let mut seed = self.seed;

if is_over_48_bytes(length) {
if length >= 48 {
let mut seed1 = seed;
let mut seed2 = seed;

while is_over_48_bytes(index) {
while index >= 48 {
seed = wymix(
read_8_bytes(&bytes[start..]) ^ self.secret[1],
read_8_bytes(&bytes[start..]) ^ self.secret.second(),
read_8_bytes(&bytes[start + 8..]) ^ seed,
);
seed1 = wymix(
read_8_bytes(&bytes[start + 16..]) ^ self.secret[2],
read_8_bytes(&bytes[start + 16..]) ^ self.secret.third(),
read_8_bytes(&bytes[start + 24..]) ^ seed1,
);
seed2 = wymix(
read_8_bytes(&bytes[start + 32..]) ^ self.secret[3],
read_8_bytes(&bytes[start + 32..]) ^ self.secret.fourth(),
read_8_bytes(&bytes[start + 40..]) ^ seed2,
);
index -= 48;
Expand All @@ -126,7 +121,7 @@ impl WyHash {

while index > 16 {
seed = wymix(
read_8_bytes(&bytes[start..]) ^ self.secret[1],
read_8_bytes(&bytes[start..]) ^ self.secret.second(),
read_8_bytes(&bytes[start + 8..]) ^ seed,
);
index -= 16;
Expand Down Expand Up @@ -198,8 +193,11 @@ impl Hasher for WyHash {

#[inline]
fn finish(&self) -> u64 {
let (lo, hi) = wymul(self.lo ^ self.secret[1], self.hi ^ self.seed);
wymix(lo ^ self.secret[0] ^ self.size, hi ^ self.secret[1])
let (lo, hi) = wymul(self.lo ^ self.secret.second(), self.hi ^ self.seed);
wymix(
lo ^ self.secret.first() ^ self.size,
hi ^ self.secret.second(),
)
}
}

Expand Down Expand Up @@ -242,20 +240,6 @@ mod tests {
);
}

#[cfg(not(feature = "v4_2"))]
#[rustfmt::skip]
const TEST_VECTORS: [(u64, &str); 8] = [
(0x0409_638e_e2bd_e459, ""),
(0xa841_2d09_1b5f_e0a9, "a"),
(0x32dd_92e4_b291_5153, "abc"),
(0x8619_1240_89a3_a16b, "message digest"),
(0x7a43_afb6_1d7f_5f40, "abcdefghijklmnopqrstuvwxyz"),
(0xff42_329b_90e5_0d58, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"),
(0xc39c_ab13_b115_aad3, "12345678901234567890123456789012345678901234567890123456789012345678901234567890"),
(0xe44a_846b_fc65_00cd, "123456789012345678901234567890123456789012345678"),
];

#[cfg(feature = "v4_2")]
#[rustfmt::skip]
const TEST_VECTORS: [(u64, &str); 8] = [
(0x9322_8a4d_e0ee_c5a2, ""),
Expand Down
File renamed without changes.
Loading

0 comments on commit 24b4ec7

Please sign in to comment.