Skip to main content

cyber_hemera/
lib.rs

1//! Poseidon2 hash over the Goldilocks field (Hemera parameters).
2//!
3//! This crate provides a streaming hash API backed by the Poseidon2
4//! algebraic hash function operating over the Goldilocks prime field
5//! (p = 2^64 - 2^32 + 1).
6//!
7//! # Hemera Parameters
8//!
9//! - **Field**: Goldilocks (p = 2^64 - 2^32 + 1)
10//! - **State width**: t = 16
11//! - **Full rounds**: R_F = 8
12//! - **Partial rounds**: R_P = 64
13//! - **S-box degree**: d = 7 (x^7)
14//! - **Rate**: 8 elements (56 input bytes per block)
15//! - **Capacity**: 8 elements
16//! - **Output**: 8 elements (64 bytes)
17//! - **Padding**: 0x01 || 0x00*
18//! - **Encoding**: little-endian canonical
19//!
20//! # Examples
21//!
22//! ```
23//! use hemera::{hash, derive_key};
24//!
25//! let digest = hash(b"hello world");
26//! println!("{digest}");
27//!
28//! let key = derive_key("my app v1", b"key material");
29//! ```
30
31mod encoding;
32pub mod hazmat;
33mod params;
34mod sponge;
35
36#[cfg(feature = "gpu")]
37pub mod gpu;
38
39// Re-export all Hemera parameters so downstream crates never hardcode them.
40pub use params::{
41    CAPACITY, COLLISION_BITS, OUTPUT_BYTES, OUTPUT_ELEMENTS, RATE, RATE_BYTES, ROUNDS_F, ROUNDS_P,
42    SBOX_DEGREE, WIDTH,
43};
44pub use sponge::{Hash, Hasher, OutputReader};
45
46/// Hash the input bytes and return a 64-byte digest.
47pub fn hash(input: &[u8]) -> Hash {
48    let mut hasher = Hasher::new();
49    hasher.update(input);
50    hasher.finalize()
51}
52
53/// Hash the input bytes with a key.
54pub fn keyed_hash(key: &[u8; OUTPUT_BYTES], input: &[u8]) -> Hash {
55    let mut hasher = Hasher::new_keyed(key);
56    hasher.update(input);
57    hasher.finalize()
58}
59
60/// Derive a key from a context string and key material.
61///
62/// This is a two-phase operation:
63/// 1. Hash the context string with domain separation
64/// 2. Use the context hash to seed a second hasher that absorbs the key material
65pub fn derive_key(context: &str, key_material: &[u8]) -> [u8; OUTPUT_BYTES] {
66    let ctx_hasher = Hasher::new_derive_key_context(context);
67    let ctx_hash = ctx_hasher.finalize();
68    let mut material_hasher = Hasher::new_derive_key_material(&ctx_hash);
69    material_hasher.update(key_material);
70    let result = material_hasher.finalize();
71    *result.as_bytes()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn hash_basic() {
80        let h = hash(b"hello");
81        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
82    }
83
84    #[test]
85    fn hash_deterministic() {
86        let h1 = hash(b"test");
87        let h2 = hash(b"test");
88        assert_eq!(h1, h2);
89    }
90
91    #[test]
92    fn hash_different_inputs() {
93        assert_ne!(hash(b""), hash(b"a"));
94        assert_ne!(hash(b"a"), hash(b"b"));
95        assert_ne!(hash(b"ab"), hash(b"ba"));
96    }
97
98    #[test]
99    fn hash_matches_streaming() {
100        let data = b"streaming consistency test with enough data to cross boundaries!!";
101        let direct = hash(data);
102        let streamed = {
103            let mut h = Hasher::new();
104            h.update(&data[..10]);
105            h.update(&data[10..]);
106            h.finalize()
107        };
108        assert_eq!(direct, streamed);
109    }
110
111    #[test]
112    fn keyed_hash_differs_from_plain() {
113        let data = b"test";
114        assert_ne!(hash(data), keyed_hash(&[0u8; OUTPUT_BYTES], data));
115    }
116
117    #[test]
118    fn keyed_hash_different_keys() {
119        let data = b"test";
120        let h1 = keyed_hash(&[0u8; OUTPUT_BYTES], data);
121        let h2 = keyed_hash(&[1u8; OUTPUT_BYTES], data);
122        assert_ne!(h1, h2);
123    }
124
125    #[test]
126    fn derive_key_basic() {
127        let key = derive_key("my context", b"material");
128        assert_ne!(key, [0u8; OUTPUT_BYTES]);
129    }
130
131    #[test]
132    fn derive_key_differs_from_hash() {
133        let data = b"material";
134        let h = hash(data);
135        let k = derive_key("context", data);
136        assert_ne!(h.as_bytes(), &k);
137    }
138
139    #[test]
140    fn derive_key_different_contexts() {
141        let k1 = derive_key("context A", b"material");
142        let k2 = derive_key("context B", b"material");
143        assert_ne!(k1, k2);
144    }
145
146    #[test]
147    fn derive_key_different_materials() {
148        let k1 = derive_key("context", b"material A");
149        let k2 = derive_key("context", b"material B");
150        assert_ne!(k1, k2);
151    }
152
153    #[test]
154    fn xof_extends_hash() {
155        let mut xof = Hasher::new().update(b"xof test").finalize_xof();
156        let mut out = [0u8; OUTPUT_BYTES * 2];
157        xof.fill(&mut out);
158        // First OUTPUT_BYTES match finalize.
159        let h = hash(b"xof test");
160        assert_eq!(&out[..OUTPUT_BYTES], h.as_bytes());
161    }
162
163    #[test]
164    fn large_input() {
165        let data = vec![0x42u8; 10_000];
166        let h = hash(&data);
167        assert_ne!(h.as_bytes(), &[0u8; OUTPUT_BYTES]);
168
169        // Streaming equivalence.
170        let mut hasher = Hasher::new();
171        for chunk in data.chunks(137) {
172            hasher.update(chunk);
173        }
174        assert_eq!(h, hasher.finalize());
175    }
176}