Skip to main content

dryoc/classic/
crypto_generichash.rs

1//! # Generic hashing
2//!
3//! Implements libsodium's generic hashing functions with BLAKE2b. With a secret
4//! key, BLAKE2b acts as a message authentication code (MAC) or pseudorandom
5//! function (PRF); it is not HMAC.
6//!
7//! See the [libsodium documentation](https://doc.libsodium.org/hashing/generic_hashing)
8//! for details.
9//!
10//! # Classic API example, single-part interface
11//!
12//! ```
13//! use base64::Engine as _;
14//! use base64::engine::general_purpose;
15//! use dryoc::classic::crypto_generichash::*;
16//! use dryoc::constants::CRYPTO_GENERICHASH_BYTES;
17//!
18//! // Use the default hash length
19//! let mut output = [0u8; CRYPTO_GENERICHASH_BYTES];
20//! // Compute the hash using the single-part interface
21//! crypto_generichash(&mut output, b"a string of bytes", None).ok();
22//!
23//! assert_eq!(
24//!     general_purpose::STANDARD.encode(output),
25//!     "GdztjR9nU/rLh8VJt8e74+/seKTUnHgBexhGSpxLau0="
26//! );
27//! ```
28//!
29//! # Classic API example, incremental interface
30//!
31//! ```
32//! use base64::Engine as _;
33//! use base64::engine::general_purpose;
34//! use dryoc::classic::crypto_generichash::*;
35//! use dryoc::constants::CRYPTO_GENERICHASH_BYTES;
36//!
37//! // Use the default hash length
38//! let mut output = [0u8; CRYPTO_GENERICHASH_BYTES];
39//! // Initialize the state for the incremental interface
40//! let mut state = crypto_generichash_init(None, CRYPTO_GENERICHASH_BYTES).expect("state");
41//! // Update the hash
42//! crypto_generichash_update(&mut state, b"a string of bytes");
43//! // Finalize, compute the hash and copy it into `output`
44//! crypto_generichash_final(state, &mut output).expect("final failed");
45//!
46//! assert_eq!(
47//!     general_purpose::STANDARD.encode(output),
48//!     "GdztjR9nU/rLh8VJt8e74+/seKTUnHgBexhGSpxLau0="
49//! );
50//! ```
51use super::generichash_blake2b::*;
52use crate::blake2b;
53use crate::constants::CRYPTO_GENERICHASH_KEYBYTES;
54use crate::error::Error;
55
56/**
57Computes a hash from `input` and `key`, copying the result into `output`.
58
59| Parameter | Typical length | Recommended minimum | Accepted lengths |
60|-|-|-|-|
61| `output` | [`CRYPTO_GENERICHASH_BYTES`](crate::constants::CRYPTO_GENERICHASH_BYTES) | [`CRYPTO_GENERICHASH_BYTES_MIN`](crate::constants::CRYPTO_GENERICHASH_BYTES_MIN) | 1 to [`CRYPTO_GENERICHASH_BYTES_MAX`](crate::constants::CRYPTO_GENERICHASH_BYTES_MAX) |
62| `key` | [`CRYPTO_GENERICHASH_KEYBYTES`] | [`CRYPTO_GENERICHASH_KEYBYTES_MIN`](crate::constants::CRYPTO_GENERICHASH_KEYBYTES_MIN) | 0 to [`CRYPTO_GENERICHASH_KEYBYTES_MAX`](crate::constants::CRYPTO_GENERICHASH_KEYBYTES_MAX) |
63
64As in libsodium, the `*_MIN` constants are recommendations rather than
65limits, and an empty key (`Some(&[])`) computes the same unkeyed hash as
66`None`.
67
68Compatible with libsodium's `crypto_generichash`.
69
70# Errors
71
72Returns an error if the output or key length is outside the accepted range.
73*/
74#[inline]
75pub fn crypto_generichash(
76    output: &mut [u8],
77    input: &[u8],
78    key: Option<&[u8]>,
79) -> Result<(), Error> {
80    crypto_generichash_blake2b(output, input, key)
81}
82
83/// State struct for the generic hash algorithm, based on BLAKE2B.
84///
85/// Cloning copies the in-progress state, so both copies can be finished
86/// independently; each copy is wiped when dropped.
87#[derive(Clone)]
88pub struct GenericHashState {
89    state: blake2b::State,
90}
91
92/**
93Initializes the state for the generic hash function using `outlen` for the expected hash output length, and optional `key`, returning it upon success.
94
95| Parameter | Typical length | Recommended minimum | Accepted lengths |
96|-|-|-|-|
97| `outlen` | [`CRYPTO_GENERICHASH_BYTES`](crate::constants::CRYPTO_GENERICHASH_BYTES) | [`CRYPTO_GENERICHASH_BYTES_MIN`](crate::constants::CRYPTO_GENERICHASH_BYTES_MIN) | 1 to [`CRYPTO_GENERICHASH_BYTES_MAX`](crate::constants::CRYPTO_GENERICHASH_BYTES_MAX) |
98| `key` | [`CRYPTO_GENERICHASH_KEYBYTES`] | [`CRYPTO_GENERICHASH_KEYBYTES_MIN`](crate::constants::CRYPTO_GENERICHASH_KEYBYTES_MIN) | 0 to [`CRYPTO_GENERICHASH_KEYBYTES_MAX`](crate::constants::CRYPTO_GENERICHASH_KEYBYTES_MAX) |
99
100As in libsodium, the `*_MIN` constants are recommendations rather than
101limits, and an empty key (`Some(&[])`) is the same as `None`.
102
103Equivalent to libsodium's `crypto_generichash_init`.
104
105# Errors
106
107Returns an error if `outlen` or the key length is outside the accepted range.
108*/
109#[inline]
110pub fn crypto_generichash_init(
111    key: Option<&[u8]>,
112    outlen: usize,
113) -> Result<GenericHashState, Error> {
114    let state = crypto_generichash_blake2b_init(key, outlen, None, None)?;
115    Ok(GenericHashState { state })
116}
117
118/// Updates the internal hash state with `input`.
119///
120/// Equivalent to libsodium's `crypto_generichash_update`
121#[inline]
122pub fn crypto_generichash_update(state: &mut GenericHashState, input: &[u8]) {
123    crypto_generichash_blake2b_update(&mut state.state, input)
124}
125
126/// Finalizes the hash computation, copying the result into `output`, whose
127/// length should equal `outlen` from the call to [`crypto_generichash_init`].
128///
129/// As in libsodium, a different length is not rejected: `output` receives its
130/// length's worth (1 to 64 bytes) of the BLAKE2b chaining value computed for
131/// the `init` length, so a shorter `output` truncates the digest and a longer
132/// one appends chaining-value bytes that are not part of it.
133///
134/// Equivalent to libsodium's `crypto_generichash_final`
135///
136/// # Errors
137///
138/// Returns an error if `output` is empty or longer than 64 bytes, lengths
139/// for which libsodium aborts.
140#[inline]
141pub fn crypto_generichash_final(state: GenericHashState, output: &mut [u8]) -> Result<(), Error> {
142    crypto_generichash_blake2b_final(state.state, output)
143}
144
145/// Generates a random hash key using the OS's random number source.
146///
147/// Equivalent to libsodium's `crypto_generichash_keygen`
148#[must_use]
149pub fn crypto_generichash_keygen() -> [u8; CRYPTO_GENERICHASH_KEYBYTES] {
150    let mut key = [0u8; CRYPTO_GENERICHASH_KEYBYTES];
151    crate::rng::copy_randombytes(&mut key);
152    key
153}
154
155#[cfg(all(test, dryoc_native_tests))]
156mod tests {
157    use super::*;
158    use crate::constants::CRYPTO_GENERICHASH_KEYBYTES_MAX;
159    use crate::native_test_util;
160    use crate::test_prelude::*;
161
162    /// Output lengths around libsodium's accepted range (1 to 64) and the
163    /// recommended minimum (16).
164    const OUTLENS: [usize; 6] = [0, 1, 15, 16, 64, 65];
165    /// Key lengths around libsodium's accepted range (0 to 64) and the
166    /// recommended minimum (16).
167    const KEYLENS: [usize; 6] = [0, 1, 15, 16, 64, 65];
168
169    fn message(len: usize) -> Vec<u8> {
170        (0..len as u32).map(|i| (i * 31 % 251) as u8).collect()
171    }
172
173    /// `None` and keys of every length in [`KEYLENS`].
174    fn keys(key: &[u8]) -> impl Iterator<Item = Option<&[u8]>> {
175        core::iter::once(None).chain(KEYLENS.into_iter().map(|len| Some(&key[..len])))
176    }
177
178    /// `input` cut at the BLAKE2b block boundaries, with empty updates
179    /// between the pieces.
180    fn parts(input: &[u8]) -> Vec<&[u8]> {
181        let mut parts = Vec::new();
182        let mut start = 0;
183        for cut in [0usize, 1, 127, 128, 129, input.len()] {
184            let cut = cut.clamp(start, input.len());
185            parts.push(&input[start..cut]);
186            parts.push(&[][..]);
187            start = cut;
188        }
189        parts
190    }
191
192    fn ours_incremental(
193        key: Option<&[u8]>,
194        init_outlen: usize,
195        parts: &[&[u8]],
196        final_outlen: usize,
197    ) -> Result<Vec<u8>, Error> {
198        let mut state = crypto_generichash_init(key, init_outlen)?;
199        for part in parts {
200            crypto_generichash_update(&mut state, part);
201        }
202        let mut output = vec![0u8; final_outlen];
203        crypto_generichash_final(state, &mut output)?;
204        Ok(output)
205    }
206
207    /// One-shot and incremental hashing accept exactly the output and key
208    /// lengths libsodium accepts (below the recommended minimums included),
209    /// treat an empty key as no key, and produce libsodium's digests,
210    /// including across BLAKE2b block boundaries.
211    #[test]
212    fn output_and_key_lengths_match_libsodium() {
213        let key: Vec<u8> = (0..=CRYPTO_GENERICHASH_KEYBYTES_MAX as u8)
214            .map(|i| i.wrapping_mul(37).wrapping_add(11))
215            .collect();
216        for len in [0usize, 1, 127, 128, 129, 300] {
217            let input = message(len);
218            let parts = parts(&input);
219            for outlen in OUTLENS {
220                for key in keys(&key) {
221                    let context =
222                        format!("len {len}, outlen {outlen}, key {:?}", key.map(<[u8]>::len));
223                    let expected = native_test_util::generichash(outlen, &input, key);
224
225                    let mut one_shot = vec![0u8; outlen];
226                    let actual = crypto_generichash(&mut one_shot, &input, key).map(|()| one_shot);
227                    assert_eq!(actual.map_err(drop), expected, "one-shot {context}");
228
229                    // libsodium aborts on an out-of-range final length, so the
230                    // incremental oracle finalizes with a valid one; `init`
231                    // rejects the same lengths as the one-shot call.
232                    let expected = native_test_util::generichash_multipart(
233                        key,
234                        outlen,
235                        &parts,
236                        outlen.clamp(1, 64),
237                    );
238                    let actual = ours_incremental(key, outlen, &parts, outlen);
239                    assert_eq!(actual.map_err(drop), expected, "incremental {context}");
240                }
241            }
242        }
243    }
244
245    /// Like libsodium, `crypto_generichash_final` does not check the output
246    /// length against the one given to `crypto_generichash_init`: it writes
247    /// the first `output.len()` bytes (1 to 64) of the BLAKE2b chaining value,
248    /// which is parameterized by the `init` length. dryoc rejects final
249    /// lengths outside 1 to 64 with an error, where libsodium aborts.
250    #[test]
251    fn final_with_mismatched_output_length_matches_libsodium() {
252        let input = message(200);
253        let parts = parts(&input);
254        let key = message(32);
255        for key in [None, Some(&key[..])] {
256            for (init_outlen, final_outlen) in [(32, 64), (64, 16), (16, 1), (1, 64), (15, 16)] {
257                let expected =
258                    native_test_util::generichash_multipart(key, init_outlen, &parts, final_outlen)
259                        .expect("libsodium final");
260                let actual =
261                    ours_incremental(key, init_outlen, &parts, final_outlen).expect("final");
262                assert_eq!(actual, expected, "init {init_outlen}, final {final_outlen}");
263            }
264            for final_outlen in [0, 65] {
265                assert!(matches!(
266                    ours_incremental(key, 32, &parts, final_outlen),
267                    Err(Error::InvalidLength { actual, .. }) if actual == final_outlen
268                ));
269            }
270        }
271    }
272}