sponge_hash_aes256/
sponge_hash.rs

1// SPDX-License-Identifier: 0BSD
2// SpongeHash-AES256
3// Copyright (C) 2025-2026 by LoRd_MuldeR <mulder2@gmx.de>
4
5use crate::utilities::{length, Aes256Crypto, BlockType, BLOCK_SIZE};
6use core::ops::Range;
7
8/// Default digest size, in bytes
9///
10/// The default digest size is currently defined as **32** bytes, i.e., **256** bits.
11pub const DEFAULT_DIGEST_SIZE: usize = 2usize * BLOCK_SIZE;
12
13/// Default number of permutation rounds to be performed
14///
15/// The default number of permutation rounds is currently defined as **1**.
16pub const DEFAULT_PERMUTE_ROUNDS: usize = 1usize;
17
18/// Pre-define round keys
19static ROUND_KEY_X: BlockType = BlockType::new::<0x5Cu8>();
20static ROUND_KEY_Y: BlockType = BlockType::new::<0x36u8>();
21static ROUND_KEY_Z: BlockType = BlockType::new::<0x6Au8>();
22
23// ---------------------------------------------------------------------------
24// Tracing
25// ---------------------------------------------------------------------------
26
27#[cfg(feature = "tracing")]
28macro_rules! trace {
29    ($self:tt, $arg:tt) => {
30        log::trace!("SpongeHash256@{:p}: {} --> {:02X?} {:02X?} {:02X?}", &$self, $arg, &$self.state.0, &$self.state.1, &$self.state.2);
31    };
32}
33
34#[cfg(not(feature = "tracing"))]
35macro_rules! trace {
36    ($self:tt, $arg:tt) => {};
37}
38
39// ---------------------------------------------------------------------------
40// Non-zero argument constraint
41// ---------------------------------------------------------------------------
42
43/// Validates that the const generic parameter is non-zero
44struct NoneZeroArg<const N: usize>;
45
46impl<const N: usize> NoneZeroArg<N> {
47    const OK: () = assert!(N > 0, "Const generic argument must be a non-zero value!");
48}
49
50// ---------------------------------------------------------------------------
51// Scratch buffer
52// ---------------------------------------------------------------------------
53
54/// Encapsulates the temporary computation state.
55#[repr(align(32))]
56struct Scratch {
57    aes256: Aes256Crypto,
58    temp: (BlockType, BlockType, BlockType),
59}
60
61impl Default for Scratch {
62    fn default() -> Self {
63        Self { aes256: Aes256Crypto::default(), temp: (BlockType::uninit(), BlockType::uninit(), BlockType::uninit()) }
64    }
65}
66
67// ---------------------------------------------------------------------------
68// Streaming API
69// ---------------------------------------------------------------------------
70
71/// This struct encapsulates the state for a “streaming” (incremental) SpongeHash-AES256 computation.
72///
73/// The const generic parameter `R` specifies the number of permutation rounds to be performed, which must be a *positive* value. The default number of permutation rounds is given by [`DEFAULT_PERMUTE_ROUNDS`]. Using a greater value slows down the hash calculation, which helps to increase the security in some usage scenarios, e.g., password hashing.
74///
75/// ### Usage Example
76///
77/// The easiest way to use the **`SpongeHash256`** structure is as follows:
78///
79/// ```rust
80/// use hex::encode_to_slice;
81/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, SpongeHash256};
82///
83/// fn main() {
84///     // Create new hash instance
85///     let mut hash = SpongeHash256::default();
86///
87///     // Process message
88///     hash.update(b"The quick brown fox jumps over the lazy dog");
89///
90///     // Retrieve the final digest
91///     let digest = hash.digest::<DEFAULT_DIGEST_SIZE>();
92///
93///     // Encode to hex
94///     let mut hex_buffer = [0u8; 2usize * DEFAULT_DIGEST_SIZE];
95///     encode_to_slice(&digest, &mut hex_buffer).unwrap();
96///
97///     // Print the digest (hex format)
98///     println!("0x{}", core::str::from_utf8(&hex_buffer).unwrap());
99/// }
100/// ```
101///
102/// ### Context information
103///
104/// Optionally, additional “context” information may be provided via the `info` parameter:
105///
106/// ```rust
107/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, SpongeHash256};
108///
109/// fn main() {
110///     // Create new hash instance with “info”
111///     let mut hash: SpongeHash256 = SpongeHash256::with_info("my_application");
112///
113///     /* ... */
114/// }
115/// ```
116///
117/// ### Important note
118///
119/// <div class="warning">
120///
121/// The [`compute()`] and [`compute_to_slice()`] convenience functions may be used as an alternative to working with the `SpongeHash256` struct directly. This is especially useful, if *all* data to be hashed is available at once.
122///
123/// </div>
124///
125/// ### Algorithm
126///
127/// This section provides additional details about the SpongeHash-AES256 algorithm.
128///
129/// #### Internal state
130///
131/// The state has a total size of 384 bits, consisting of three 128-bit blocks, and is initialized to all zeros at the start of the computation. Only the upper 128 bits are directly used for input and output operations, as described below.
132///
133/// #### Update function
134///
135/// The “update” function, which *absorbs* input blocks into the state and *squeezes* the corresponding output from it, is defined as follows, where `input[i]` denotes the *i*-th input block and `output[k]` the *k*-th output block:
136///
137/// ![Update](https://github.com/lordmulder/sponge-hash-aes256/raw/master/.assets/images/function-update.png)
138///
139/// #### Permutation function
140///
141/// The “permutation” function, applied to scramble the state after each absorbing or squeezing step, is defined as follows, where `AES-256` denotes the ordinary [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) block cipher with a key size of 256 bits and a block size of 128 bits.
142///
143/// ![Permutation](https://github.com/lordmulder/sponge-hash-aes256/raw/master/.assets/images/function-permutation.png)
144///
145/// The constants `const_0` and `const_1` are defined as full blocks filled with `0x5C` and `0x36`, respectively.
146///
147/// ### Finalization
148///
149/// The padding of the final input block is performed by first appending a single `1` bit, followed by the minimal number of `0` bits needed to make the total message length a multiple of the block size.
150///
151/// Following the final input block, a 128-bit block filled entirely with `0x6A` bytes is absorbed into the state.
152#[repr(align(32))]
153pub struct SpongeHash256<const R: usize = DEFAULT_PERMUTE_ROUNDS> {
154    state: (BlockType, BlockType, BlockType),
155    offset: usize,
156}
157
158impl<const R: usize> SpongeHash256<R> {
159    /// Creates a new SpongeHash-AES256 instance and initializes the hash computation.
160    ///
161    /// **Note:** This function implies an *empty* [`info`](Self::with_info()) string.
162    pub fn new() -> Self {
163        Self::with_info(Default::default())
164    }
165
166    /// Creates a new SpongeHash-AES256 instance and initializes the hash computation with the given `info` string.
167    ///
168    /// **Note:** The length of the `info` string **must not** exceed a length of 255 characters!
169    pub fn with_info(info: &str) -> Self {
170        let () = NoneZeroArg::<R>::OK;
171        let mut hash = Self { state: (BlockType::zero(), BlockType::zero(), BlockType::zero()), offset: 0usize };
172        hash.initialize(info.as_bytes());
173        hash
174    }
175
176    /// Initializes the internal state with the given `info` string
177    #[inline]
178    fn initialize(&mut self, info_data: &[u8]) {
179        trace!(self, "initlz::enter");
180
181        match info_data.len().try_into() {
182            Ok(length) => {
183                self.update(u8::to_be_bytes(length));
184                self.update(info_data);
185            }
186            Err(_) => panic!("Info length exceeds the allowable maximum!"),
187        };
188
189        trace!(self, "initlz::leave");
190    }
191
192    /// Processes the next chunk of the message, as given by the `chunk` parameter.
193    ///
194    /// A `chunk` can be of *any* type that implements the [`AsRef<[u8]>`](AsRef<T>) trait, e.g., `&[u8]`, `&str` or `String`.
195    ///
196    /// The internal state of the hash computation is updated by this function.
197    #[inline]
198    pub fn update<T: AsRef<[u8]>>(&mut self, chunk: T) {
199        trace!(self, "update::enter");
200
201        let source = chunk.as_ref().as_ptr_range();
202        if !source.is_empty() {
203            unsafe {
204                self.update_range(source);
205            }
206        }
207
208        trace!(self, "update::leave");
209    }
210
211    /// Processes the next chunk of "raw" bytes, as specified by the [`Range<*const u8>`](slice::as_ptr_range) in the `source` parameter.
212    ///
213    /// The internal state of the hash computation is updated by this function.
214    ///
215    /// # Safety
216    ///
217    /// The caller **must** ensure that *all* byte addresses in the range from `source.start` up to but excluding `source.end` are valid!
218    #[inline]
219    pub unsafe fn update_range(&mut self, source: Range<*const u8>) {
220        let mut source_next = source.start;
221        let mut scratch_buffer = Scratch::default();
222
223        while (self.offset != 0usize) && (source_next < source.end) {
224            self.state.0[self.offset] ^= *source_next;
225            self.offset += 1usize;
226            source_next = source_next.add(1usize);
227
228            if self.offset >= BLOCK_SIZE {
229                self.permute(&mut scratch_buffer);
230                self.offset = 0usize;
231            }
232        }
233
234        if source_next < source.end {
235            debug_assert_eq!(self.offset, 0usize);
236
237            while length(source_next, source.end) >= BLOCK_SIZE {
238                self.state.0.xor_with_u8_ptr(source_next);
239                self.permute(&mut scratch_buffer);
240                source_next = source_next.add(BLOCK_SIZE);
241            }
242
243            while source_next < source.end {
244                self.state.0[self.offset] ^= *source_next;
245                self.offset += 1usize;
246                source_next = source_next.add(1usize);
247            }
248        }
249
250        debug_assert!(self.offset < BLOCK_SIZE);
251    }
252
253    /// Concludes the hash computation and returns the final digest.
254    ///
255    /// The hash value (digest) of the concatenation of all processed message chunks is returned as an new array of size `N`.
256    ///
257    /// The returned array is filled completely, generating a hash value (digest) of the appropriate size.
258    ///
259    /// **Note:** The digest output size `N`, in bytes, must be a *positive* value! &#x1F6A8;
260    pub fn digest<const N: usize>(self) -> [u8; N] {
261        let () = NoneZeroArg::<N>::OK;
262        let mut digest = [0u8; N];
263        self.digest_to_slice(&mut digest);
264        digest
265    }
266
267    /// Concludes the hash computation and returns the final digest.
268    ///
269    /// The hash value (digest) of the concatenation of all processed message chunks is written into the slice `digest_out`.
270    ///
271    /// The output slice is filled completely, generating a hash value (digest) of the appropriate size.
272    ///
273    /// **Note:** The specified digest output size, i.e., `digest_out.len()`, in bytes, must be a *positive* value! &#x1F6A8;
274    pub fn digest_to_slice(mut self, digest_out: &mut [u8]) {
275        trace!(self, "digest::enter");
276        assert!(!digest_out.is_empty(), "Digest output size must be positive!");
277
278        let mut scratch_buffer = Scratch::default();
279
280        self.state.0[self.offset] ^= 0x80u8;
281        self.permute(&mut scratch_buffer);
282        self.state.0.xor_with(&ROUND_KEY_Z);
283
284        let mut pos = 0usize;
285
286        while pos < digest_out.len() {
287            self.permute(&mut scratch_buffer);
288            let copy_len = BLOCK_SIZE.min(digest_out.len() - pos);
289            digest_out[pos..(pos + copy_len)].copy_from_slice(&self.state.0[..copy_len]);
290            pos += copy_len;
291        }
292
293        trace!(self, "digest::leave");
294    }
295
296    /// Pseudorandom permutation, based on the AES-256 block cipher
297    #[inline]
298    fn permute(&mut self, work: &mut Scratch) {
299        trace!(self, "permfn::enter");
300
301        for _ in 0..R {
302            work.aes256.encrypt(&mut work.temp.0, &self.state.0, &self.state.1, &self.state.2);
303            work.aes256.encrypt(&mut work.temp.1, &self.state.1, &self.state.2, &self.state.0);
304            work.aes256.encrypt(&mut work.temp.2, &self.state.2, &self.state.0, &self.state.1);
305
306            self.state.0.xor_with(&work.temp.0);
307            self.state.1.xor_with(&work.temp.1);
308            self.state.2.xor_with(&work.temp.2);
309
310            self.state.1.xor_with(&ROUND_KEY_X);
311            self.state.2.xor_with(&ROUND_KEY_Y);
312        }
313
314        trace!(self, "permfn::leave");
315    }
316}
317
318impl Default for SpongeHash256 {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324// ---------------------------------------------------------------------------
325// One-Shot API
326// ---------------------------------------------------------------------------
327
328/// Convenience function for “one-shot” SpongeHash-AES256 computation
329///
330/// The hash value (digest) of the given `message` is returned as an new array of type `[u8; N]`.
331///
332/// A `message` can be of *any* type that implements the [`AsRef<[u8]>`](AsRef<T>) trait, e.g., `&[u8]`, `&str` or `String`.
333///
334/// Optionally, an additional `info` string may be specified.
335///
336/// The returned array is filled completely, generating a hash value (digest) of the appropriate size.
337///
338/// This function uses the default number of permutation rounds, as is given by [`DEFAULT_PERMUTE_ROUNDS`].
339///
340/// **Note:** The digest output size `N`, in bytes, must be a *positive* value! &#x1F6A8;
341///
342/// ### Usage Example
343///
344/// The **`compute()`** function can be used as follows:
345///
346/// ```rust
347/// use hex::encode_to_slice;
348/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, compute};
349///
350/// fn main() {
351///     // Compute the digest using the “one-shot” function
352///     let digest: [u8; DEFAULT_DIGEST_SIZE] = compute(
353///         None,
354///         b"The quick brown fox jumps over the lazy dog");
355///
356///     // Encode to hex
357///     let mut hex_buffer = [0u8; 2usize * DEFAULT_DIGEST_SIZE];
358///     encode_to_slice(&digest, &mut hex_buffer).unwrap();
359///
360///     // Print the digest (hex format)
361///     println!("0x{}", core::str::from_utf8(&hex_buffer).unwrap());
362/// }
363/// ```
364///
365/// ### Context information
366///
367/// Optionally, additional “context” information may be provided via the `info` parameter:
368///
369/// ```rust
370/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, compute};
371///
372/// fn main() {
373///     // Compute the digest using the “one-shot” function with additional “info”
374///     let digest: [u8; DEFAULT_DIGEST_SIZE] = compute(
375///         Some("my_application"),
376///         b"The quick brown fox jumps over the lazy dog");
377///     /* ... */
378/// }
379/// ```
380///
381/// ### Important note
382///
383/// <div class="warning">
384///
385/// Applications that need to process *large* messages are recommended to use the [streaming API](SpongeHash256), which does **not** require *all* message data to be held in memory at once and which allows for an *incremental* hash computation.
386///
387/// </div>
388pub fn compute<const N: usize, T: AsRef<[u8]>>(info: Option<&str>, message: T) -> [u8; N] {
389    assert!(!info.is_some_and(str::is_empty), "Info must not be empty!");
390    let mut state: SpongeHash256 = SpongeHash256::with_info(info.unwrap_or_default());
391    state.update(message);
392    state.digest()
393}
394
395/// Convenience function for “one-shot” SpongeHash-AES256 computation
396///
397/// The hash value (digest) of the given `message` is written into the slice `digest_out`.
398///
399/// A `message` can be of *any* type that implements the [`AsRef<[u8]>`](AsRef<T>) trait, e.g., `&[u8]`, `&str` or `String`.
400///
401/// Optionally, an additional `info` string may be specified.
402///
403/// The output slice is filled completely, generating a hash value (digest) of the appropriate size.
404///
405/// This function uses the default number of permutation rounds, as is given by [`DEFAULT_PERMUTE_ROUNDS`].
406///
407/// **Note:** The digest output size, i.e., `digest_out.len()`, in bytes, must be a *positive* value! &#x1F6A8;
408///
409/// ### Usage Example
410///
411/// The **`compute_to_slice()`** function can be used as follows:
412///
413/// ```rust
414/// use hex::encode_to_slice;
415/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, compute_to_slice};
416///
417/// fn main() {
418///     // Compute digest using the “one-shot” function
419///     let mut digest = [0u8; DEFAULT_DIGEST_SIZE];
420///     compute_to_slice(&mut digest, None, b"The quick brown fox jumps over the lazy dog");
421///
422///     // Encode to hex
423///     let mut hex_buffer = [0u8; 2usize * DEFAULT_DIGEST_SIZE];
424///     encode_to_slice(&digest, &mut hex_buffer).unwrap();
425///
426///     // Print the digest (hex format)
427///     println!("0x{}", core::str::from_utf8(&hex_buffer).unwrap());
428/// }
429///
430/// ```
431/// ### Context information
432///
433/// Optionally, additional “context” information may be provided via the `info` parameter:
434///
435/// ```rust
436/// use sponge_hash_aes256::{DEFAULT_DIGEST_SIZE, compute_to_slice};
437///
438/// fn main() {
439///     // Compute digest using the “one-shot” function with additional “info”
440///     let mut digest = [0u8; DEFAULT_DIGEST_SIZE];
441///     compute_to_slice(
442///             &mut digest,
443///             Some("my_application"),
444///             b"The quick brown fox jumps over the lazy dog");
445///     /* ... */
446/// }
447/// ```
448///
449/// ### Important note
450///
451/// <div class="warning">
452///
453/// Applications that need to process *large* messages are recommended to use the [streaming API](SpongeHash256), which does **not** require *all* message data to be held in memory at once and which allows for an *incremental* hash computation.
454///
455/// </div>
456pub fn compute_to_slice<T: AsRef<[u8]>>(digest_out: &mut [u8], info: Option<&str>, message: T) {
457    assert!(!info.is_some_and(str::is_empty), "Info must not be empty!");
458    let mut state: SpongeHash256 = SpongeHash256::with_info(info.unwrap_or_default());
459    state.update(message);
460    state.digest_to_slice(digest_out);
461}