Skip to main content

cyber_hemera/
lib.rs

1//! Poseidon2 hash over the Goldilocks field (Hemera parameters).
2//!
3//! # WARNING
4//!
5//! **This is novel, unaudited cryptography.** The parameter set, sponge
6//! construction, and self-bootstrapping round constant generation have not
7//! been reviewed by third-party cryptographers. Do not use in production
8//! systems where cryptographic guarantees are required. Use at your own risk.
9//!
10//! **The Hemera hash is not finalized and may change.** Hash outputs, round
11//! constants, and the sponge construction are subject to breaking changes
12//! until a stable release.
13//!
14//! ---
15//!
16//! This crate provides a streaming hash API backed by the Poseidon2
17//! algebraic hash function operating over the Goldilocks prime field
18//! (p = 2^64 - 2^32 + 1).
19//!
20//! # Hemera Parameters
21//!
22//! - **Field**: Goldilocks (p = 2^64 - 2^32 + 1)
23//! - **State width**: t = 16
24//! - **Full rounds**: R_F = 8
25//! - **Partial rounds**: R_P = 64
26//! - **S-box degree**: d = 7 (x^7)
27//! - **Rate**: 8 elements (56 input bytes per block)
28//! - **Capacity**: 8 elements
29//! - **Output**: 8 elements (64 bytes)
30//! - **Padding**: 0x01 || 0x00*
31//! - **Encoding**: little-endian canonical
32//!
33//! # Examples
34//!
35//! ```
36//! use cyber_hemera::{hash, derive_key};
37//!
38//! let digest = hash(b"hello world");
39//! println!("{digest}");
40//!
41//! let key = derive_key("my app v1", b"key material");
42//! ```
43
44#[cfg(test)]
45mod bootstrap;
46mod constants;
47mod encoding;
48pub(crate) mod field;
49mod params;
50mod permutation;
51mod sponge;
52pub mod tree;
53
54#[cfg(feature = "gpu")]
55pub mod gpu;
56
57// Re-export all Hemera parameters so downstream crates never hardcode them.
58pub use params::{
59    CAPACITY, CHUNK_SIZE, COLLISION_BITS, OUTPUT_BYTES, OUTPUT_ELEMENTS, RATE, RATE_BYTES,
60    ROUNDS_F, ROUNDS_P, SBOX_DEGREE, WIDTH,
61};
62pub use sponge::{Hash, Hasher, OutputReader};
63
64/// Hash the input bytes and return a 64-byte digest.
65pub fn hash(input: &[u8]) -> Hash {
66    let mut hasher = Hasher::new();
67    hasher.update(input);
68    hasher.finalize()
69}
70
71/// Hash the input bytes with a key.
72pub fn keyed_hash(key: &[u8; OUTPUT_BYTES], input: &[u8]) -> Hash {
73    let mut hasher = Hasher::new_keyed(key);
74    hasher.update(input);
75    hasher.finalize()
76}
77
78/// Derive a key from a context string and key material.
79///
80/// This is a two-phase operation:
81/// 1. Hash the context string with domain separation
82/// 2. Use the context hash to seed a second hasher that absorbs the key material
83pub fn derive_key(context: &str, key_material: &[u8]) -> [u8; OUTPUT_BYTES] {
84    let ctx_hasher = Hasher::new_derive_key_context(context);
85    let ctx_hash = ctx_hasher.finalize();
86    let mut material_hasher = Hasher::new_derive_key_material(&ctx_hash);
87    material_hasher.update(key_material);
88    let result = material_hasher.finalize();
89    *result.as_bytes()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn hash_basic() {
98        let h = hash(b"hello");
99        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
100    }
101
102    #[test]
103    fn hash_deterministic() {
104        let h1 = hash(b"test");
105        let h2 = hash(b"test");
106        assert_eq!(h1, h2);
107    }
108
109    #[test]
110    fn hash_different_inputs() {
111        assert_ne!(hash(b""), hash(b"a"));
112        assert_ne!(hash(b"a"), hash(b"b"));
113        assert_ne!(hash(b"ab"), hash(b"ba"));
114    }
115
116    #[test]
117    fn hash_matches_streaming() {
118        let data = b"streaming consistency test with enough data to cross boundaries!!";
119        let direct = hash(data);
120        let streamed = {
121            let mut h = Hasher::new();
122            h.update(&data[..10]);
123            h.update(&data[10..]);
124            h.finalize()
125        };
126        assert_eq!(direct, streamed);
127    }
128
129    #[test]
130    fn keyed_hash_differs_from_plain() {
131        let data = b"test";
132        assert_ne!(hash(data), keyed_hash(&[0u8; OUTPUT_BYTES], data));
133    }
134
135    #[test]
136    fn keyed_hash_different_keys() {
137        let data = b"test";
138        let h1 = keyed_hash(&[0u8; OUTPUT_BYTES], data);
139        let h2 = keyed_hash(&[1u8; OUTPUT_BYTES], data);
140        assert_ne!(h1, h2);
141    }
142
143    #[test]
144    fn derive_key_basic() {
145        let key = derive_key("my context", b"material");
146        assert_ne!(key, [0u8; OUTPUT_BYTES]);
147    }
148
149    #[test]
150    fn derive_key_differs_from_hash() {
151        let data = b"material";
152        let h = hash(data);
153        let k = derive_key("context", data);
154        assert_ne!(h.as_bytes(), &k);
155    }
156
157    #[test]
158    fn derive_key_different_contexts() {
159        let k1 = derive_key("context A", b"material");
160        let k2 = derive_key("context B", b"material");
161        assert_ne!(k1, k2);
162    }
163
164    #[test]
165    fn derive_key_different_materials() {
166        let k1 = derive_key("context", b"material A");
167        let k2 = derive_key("context", b"material B");
168        assert_ne!(k1, k2);
169    }
170
171    #[test]
172    fn xof_extends_hash() {
173        let mut xof = Hasher::new().update(b"xof test").finalize_xof();
174        let mut out = [0u8; OUTPUT_BYTES * 2];
175        xof.fill(&mut out);
176        // First OUTPUT_BYTES match finalize.
177        let h = hash(b"xof test");
178        assert_eq!(&out[..OUTPUT_BYTES], h.as_bytes());
179    }
180
181    #[test]
182    fn large_input() {
183        let data = vec![0x42u8; 10_000];
184        let h = hash(&data);
185        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
186
187        // Streaming equivalence.
188        let mut hasher = Hasher::new();
189        for chunk in data.chunks(137) {
190            hasher.update(chunk);
191        }
192        assert_eq!(h, hasher.finalize());
193    }
194
195    #[test]
196    fn hash_empty() {
197        let h = hash(b"");
198        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
199    }
200
201    #[test]
202    fn hash_single_byte_avalanche() {
203        // Each single-byte input should produce a wildly different hash
204        let hashes: Vec<_> = (0..=255u8).map(|b| hash(&[b])).collect();
205        for i in 0..256 {
206            for j in (i + 1)..256 {
207                assert_ne!(hashes[i], hashes[j], "collision at bytes {i} and {j}");
208            }
209        }
210    }
211
212    #[test]
213    fn keyed_hash_empty_input() {
214        let h = keyed_hash(&[0u8; OUTPUT_BYTES], b"");
215        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
216    }
217
218    #[test]
219    fn derive_key_long_context() {
220        // Context longer than one rate block
221        let long_ctx = "a]".repeat(100);
222        let k = derive_key(&long_ctx, b"material");
223        assert_ne!(k, [0u8; OUTPUT_BYTES]);
224    }
225
226    #[test]
227    fn derive_key_long_material() {
228        // Material longer than one rate block
229        let material = vec![0x42u8; 1000];
230        let k = derive_key("ctx", &material);
231        assert_ne!(k, [0u8; OUTPUT_BYTES]);
232    }
233}
234
235/// Property-based tests using proptest.
236#[cfg(test)]
237mod proptests {
238    use super::*;
239    use proptest::prelude::*;
240
241    proptest! {
242        #[test]
243        fn hash_is_deterministic(data in proptest::collection::vec(any::<u8>(), 0..500)) {
244            prop_assert_eq!(hash(&data), hash(&data));
245        }
246
247        #[test]
248        fn streaming_matches_oneshot(data in proptest::collection::vec(any::<u8>(), 0..500)) {
249            let oneshot = hash(&data);
250            let mut hasher = Hasher::new();
251            // Feed in random-ish chunk sizes
252            let mut pos = 0;
253            let mut chunk_size = 1;
254            while pos < data.len() {
255                let end = (pos + chunk_size).min(data.len());
256                hasher.update(&data[pos..end]);
257                pos = end;
258                chunk_size = (chunk_size * 3 + 1) % 71; // pseudo-random sizes
259                if chunk_size == 0 { chunk_size = 1; }
260            }
261            prop_assert_eq!(oneshot, hasher.finalize());
262        }
263
264        #[test]
265        fn xof_prefix_matches_finalize(data in proptest::collection::vec(any::<u8>(), 0..200)) {
266            let hash_result = hash(&data);
267            let mut xof = {
268                let mut h = Hasher::new();
269                h.update(&data);
270                h.finalize_xof()
271            };
272            let mut xof_bytes = [0u8; OUTPUT_BYTES];
273            xof.fill(&mut xof_bytes);
274            prop_assert_eq!(hash_result.as_bytes(), &xof_bytes);
275        }
276
277        #[test]
278        fn keyed_hash_differs_from_plain(
279            data in proptest::collection::vec(any::<u8>(), 1..200),
280            key in proptest::collection::vec(any::<u8>(), OUTPUT_BYTES..=OUTPUT_BYTES),
281        ) {
282            let key_arr: [u8; OUTPUT_BYTES] = key.try_into().unwrap();
283            let plain = hash(&data);
284            let keyed = keyed_hash(&key_arr, &data);
285            prop_assert_ne!(plain, keyed);
286        }
287
288        #[test]
289        fn clone_consistency(
290            prefix in proptest::collection::vec(any::<u8>(), 0..100),
291            suffix in proptest::collection::vec(any::<u8>(), 0..100),
292        ) {
293            let mut h1 = Hasher::new();
294            h1.update(&prefix);
295            let mut h2 = h1.clone();
296            h1.update(&suffix);
297            h2.update(&suffix);
298            prop_assert_eq!(h1.finalize(), h2.finalize());
299        }
300    }
301}