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