Skip to main content

hsh_digest/
lib.rs

1#![forbid(unsafe_code)]
2#![cfg_attr(
3    test,
4    allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)
5)]
6// Copyright © 2023-2026 Hash (HSH) library contributors. All rights reserved.
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9//! # `hsh-digest` — general-purpose hashing primitives
10//!
11//! **⚠️ This crate is NOT for password storage.** Hashing passwords
12//! requires a memory-hard / iteration-hard KDF (Argon2id, scrypt,
13//! bcrypt, PBKDF2). For that, use `hsh::api::hash` from the `hsh`
14//! crate, not the primitives here.
15//!
16//! Use `hsh-digest` when you need a standard cryptographic digest for
17//! things like:
18//!
19//! - Content addressing (Git-style, IPFS-style content hashes).
20//! - Message authentication codes (with the `hmac` crate on top).
21//! - Random-oracle / commitment schemes.
22//! - PHC-string parsing for non-`hsh` hashes.
23//! - Building blocks for higher-level protocols (e.g. Merkle trees).
24//!
25//! ## Algorithms
26//!
27//! | Family | Members | Feature flag |
28//! | ------ | ------- | ------------ |
29//! | SHA-2  | SHA-256, SHA-384, SHA-512 | `sha2` (default) |
30//! | SHA-3  | SHA3-256, SHA3-384, SHA3-512 | `sha3` (default) |
31//! | BLAKE3 | BLAKE3-256 | `blake3` (default) |
32//! | K12    | KangarooTwelve, TurboSHAKE128/256 | `k12` (stub) |
33//! | Ascon  | Ascon-Hash256, Ascon-XOF128 | `ascon` (stub) |
34//!
35//! ## Example
36//!
37//! ### One-shot
38//!
39//! ```
40//! use hsh_digest::{Algorithm, hash};
41//!
42//! let digest = hash(Algorithm::Sha256, b"hello, world").unwrap();
43//! assert_eq!(digest.len(), 32);
44//! ```
45//!
46//! ### Streaming
47//!
48//! ```
49//! use hsh_digest::{Algorithm, Hasher};
50//!
51//! let mut hasher = Hasher::new(Algorithm::Blake3).unwrap();
52//! hasher.update(b"hello, ");
53//! hasher.update(b"world");
54//! let digest = hasher.finalize();
55//! assert_eq!(digest.len(), 32);
56//! ```
57
58// At least one algorithm feature must be enabled — the `Algorithm`
59// enum and `HasherInner` would otherwise be uninhabited, producing
60// downstream `unreachable_code` errors.
61#[cfg(not(any(
62    feature = "sha2",
63    feature = "sha3",
64    feature = "blake3"
65)))]
66compile_error!(
67    "hsh-digest requires at least one algorithm feature: `sha2`, `sha3`, or `blake3`."
68);
69
70pub mod error;
71
72pub use error::DigestError;
73
74// Bring the `digest::Digest` trait into scope at module level so
75// `update` / `finalize` methods resolve on the sha2 / sha3 hashers.
76// Hoisted out of each function body so static analysers don't trip
77// on cfg-gated `use` statements interleaved with parameter usage
78// (CodeQL Rust extractor false-positive on `rust/unused-variable`).
79#[cfg(any(feature = "sha2", feature = "sha3"))]
80use digest::Digest as _;
81
82/// Supported general-purpose hash algorithms.
83///
84/// Variants are gated by feature flag — see crate-level docs.
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86#[non_exhaustive]
87pub enum Algorithm {
88    /// SHA-256 (FIPS 180-4). 32-byte output.
89    #[cfg(feature = "sha2")]
90    Sha256,
91    /// SHA-384 (FIPS 180-4). 48-byte output.
92    #[cfg(feature = "sha2")]
93    Sha384,
94    /// SHA-512 (FIPS 180-4). 64-byte output.
95    #[cfg(feature = "sha2")]
96    Sha512,
97    /// SHA3-256 (FIPS 202). 32-byte output.
98    #[cfg(feature = "sha3")]
99    Sha3_256,
100    /// SHA3-384 (FIPS 202). 48-byte output.
101    #[cfg(feature = "sha3")]
102    Sha3_384,
103    /// SHA3-512 (FIPS 202). 64-byte output.
104    #[cfg(feature = "sha3")]
105    Sha3_512,
106    /// BLAKE3, 32-byte output.
107    #[cfg(feature = "blake3")]
108    Blake3,
109}
110
111impl Algorithm {
112    /// Returns the output length in bytes for this algorithm.
113    #[must_use]
114    pub const fn output_len(self) -> usize {
115        match self {
116            #[cfg(feature = "sha2")]
117            Self::Sha256 => 32,
118            #[cfg(feature = "sha2")]
119            Self::Sha384 => 48,
120            #[cfg(feature = "sha2")]
121            Self::Sha512 => 64,
122            #[cfg(feature = "sha3")]
123            Self::Sha3_256 => 32,
124            #[cfg(feature = "sha3")]
125            Self::Sha3_384 => 48,
126            #[cfg(feature = "sha3")]
127            Self::Sha3_512 => 64,
128            #[cfg(feature = "blake3")]
129            Self::Blake3 => 32,
130        }
131    }
132
133    /// Returns the standard algorithm identifier (e.g. `"sha256"`).
134    #[must_use]
135    pub const fn id(self) -> &'static str {
136        match self {
137            #[cfg(feature = "sha2")]
138            Self::Sha256 => "sha256",
139            #[cfg(feature = "sha2")]
140            Self::Sha384 => "sha384",
141            #[cfg(feature = "sha2")]
142            Self::Sha512 => "sha512",
143            #[cfg(feature = "sha3")]
144            Self::Sha3_256 => "sha3-256",
145            #[cfg(feature = "sha3")]
146            Self::Sha3_384 => "sha3-384",
147            #[cfg(feature = "sha3")]
148            Self::Sha3_512 => "sha3-512",
149            #[cfg(feature = "blake3")]
150            Self::Blake3 => "blake3",
151        }
152    }
153}
154
155/// One-shot convenience: hashes `data` with `algorithm` and returns
156/// the digest bytes.
157///
158/// # Errors
159///
160/// Returns [`DigestError::Unavailable`] if `algorithm` was compiled
161/// out via Cargo features. This is unreachable when the corresponding
162/// `Algorithm` variant exists — the function signature simply mirrors
163/// `Hasher::new` for consistency.
164pub fn hash(
165    algorithm: Algorithm,
166    data: &[u8],
167) -> Result<Vec<u8>, DigestError> {
168    let mut h = Hasher::new(algorithm)?;
169    h.update(data);
170    Ok(h.finalize())
171}
172
173/// Streaming hasher — call [`Hasher::update`] one or more times, then
174/// [`Hasher::finalize`].
175pub struct Hasher {
176    inner: HasherInner,
177}
178
179impl std::fmt::Debug for Hasher {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.debug_struct("Hasher")
182            .field("algorithm", &self.algorithm())
183            .finish()
184    }
185}
186
187// blake3::Hasher is ~1KB while sha2 state is ~96B; we deliberately
188// accept the size delta because boxing every variant would force a
189// heap allocation per hash, which dwarfs the cost of a few extra
190// stack bytes in the streaming path.
191#[allow(clippy::large_enum_variant)]
192enum HasherInner {
193    #[cfg(feature = "sha2")]
194    Sha256(sha2::Sha256),
195    #[cfg(feature = "sha2")]
196    Sha384(sha2::Sha384),
197    #[cfg(feature = "sha2")]
198    Sha512(sha2::Sha512),
199    #[cfg(feature = "sha3")]
200    Sha3_256(sha3::Sha3_256),
201    #[cfg(feature = "sha3")]
202    Sha3_384(sha3::Sha3_384),
203    #[cfg(feature = "sha3")]
204    Sha3_512(sha3::Sha3_512),
205    #[cfg(feature = "blake3")]
206    Blake3(blake3::Hasher),
207}
208
209impl Hasher {
210    /// Creates a new streaming hasher for the given algorithm.
211    ///
212    /// # Errors
213    ///
214    /// Currently infallible because `Algorithm` variants are themselves
215    /// feature-gated; kept as `Result` for forward compatibility when
216    /// runtime-selectable algorithms land.
217    pub fn new(algorithm: Algorithm) -> Result<Self, DigestError> {
218        let inner = match algorithm {
219            #[cfg(feature = "sha2")]
220            Algorithm::Sha256 => {
221                HasherInner::Sha256(sha2::Sha256::new())
222            }
223            #[cfg(feature = "sha2")]
224            Algorithm::Sha384 => {
225                HasherInner::Sha384(sha2::Sha384::new())
226            }
227            #[cfg(feature = "sha2")]
228            Algorithm::Sha512 => {
229                HasherInner::Sha512(sha2::Sha512::new())
230            }
231            #[cfg(feature = "sha3")]
232            Algorithm::Sha3_256 => {
233                HasherInner::Sha3_256(sha3::Sha3_256::new())
234            }
235            #[cfg(feature = "sha3")]
236            Algorithm::Sha3_384 => {
237                HasherInner::Sha3_384(sha3::Sha3_384::new())
238            }
239            #[cfg(feature = "sha3")]
240            Algorithm::Sha3_512 => {
241                HasherInner::Sha3_512(sha3::Sha3_512::new())
242            }
243            #[cfg(feature = "blake3")]
244            Algorithm::Blake3 => {
245                HasherInner::Blake3(blake3::Hasher::new())
246            }
247        };
248        Ok(Self { inner })
249    }
250
251    /// Returns the algorithm this hasher was created with.
252    #[must_use]
253    pub fn algorithm(&self) -> Algorithm {
254        match &self.inner {
255            #[cfg(feature = "sha2")]
256            HasherInner::Sha256(_) => Algorithm::Sha256,
257            #[cfg(feature = "sha2")]
258            HasherInner::Sha384(_) => Algorithm::Sha384,
259            #[cfg(feature = "sha2")]
260            HasherInner::Sha512(_) => Algorithm::Sha512,
261            #[cfg(feature = "sha3")]
262            HasherInner::Sha3_256(_) => Algorithm::Sha3_256,
263            #[cfg(feature = "sha3")]
264            HasherInner::Sha3_384(_) => Algorithm::Sha3_384,
265            #[cfg(feature = "sha3")]
266            HasherInner::Sha3_512(_) => Algorithm::Sha3_512,
267            #[cfg(feature = "blake3")]
268            HasherInner::Blake3(_) => Algorithm::Blake3,
269        }
270    }
271
272    /// Feeds bytes into the hasher state.
273    pub fn update(&mut self, bytes: &[u8]) {
274        match &mut self.inner {
275            #[cfg(feature = "sha2")]
276            HasherInner::Sha256(h) => h.update(bytes),
277            #[cfg(feature = "sha2")]
278            HasherInner::Sha384(h) => h.update(bytes),
279            #[cfg(feature = "sha2")]
280            HasherInner::Sha512(h) => h.update(bytes),
281            #[cfg(feature = "sha3")]
282            HasherInner::Sha3_256(h) => h.update(bytes),
283            #[cfg(feature = "sha3")]
284            HasherInner::Sha3_384(h) => h.update(bytes),
285            #[cfg(feature = "sha3")]
286            HasherInner::Sha3_512(h) => h.update(bytes),
287            #[cfg(feature = "blake3")]
288            HasherInner::Blake3(h) => {
289                let _ = h.update(bytes);
290            }
291        }
292    }
293
294    /// Consumes the hasher and returns the digest bytes.
295    #[must_use]
296    pub fn finalize(self) -> Vec<u8> {
297        match self.inner {
298            #[cfg(feature = "sha2")]
299            HasherInner::Sha256(h) => h.finalize().to_vec(),
300            #[cfg(feature = "sha2")]
301            HasherInner::Sha384(h) => h.finalize().to_vec(),
302            #[cfg(feature = "sha2")]
303            HasherInner::Sha512(h) => h.finalize().to_vec(),
304            #[cfg(feature = "sha3")]
305            HasherInner::Sha3_256(h) => h.finalize().to_vec(),
306            #[cfg(feature = "sha3")]
307            HasherInner::Sha3_384(h) => h.finalize().to_vec(),
308            #[cfg(feature = "sha3")]
309            HasherInner::Sha3_512(h) => h.finalize().to_vec(),
310            #[cfg(feature = "blake3")]
311            HasherInner::Blake3(h) => h.finalize().as_bytes().to_vec(),
312        }
313    }
314}
315
316/// Constant-time comparison of two byte slices.
317///
318/// Useful for MAC verification, content-hash comparison, or any other
319/// place where a timing side-channel on a non-secret-but-sensitive
320/// comparison would help an attacker.
321#[must_use]
322pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
323    use subtle::ConstantTimeEq;
324    if a.len() != b.len() {
325        return false;
326    }
327    bool::from(a.ct_eq(b))
328}