si_crypto_hashes/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2//! This crate provides a reusable functionality for working with typical cryptographic
3//! hashes.
4//!
5//! ```rust
6//! # use std::str::FromStr;
7//! # use std::sync::Arc;
8//! # use si_crypto_hashes::{HashAlgorithm, HashDigest};
9//! #
10//! let expected = "sha256_dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f";
11//!
12//! // Parse the string representation of the expected hash.
13//! let digest = HashDigest::<Arc<[u8]>>::from_str(expected).unwrap();
14//! assert_eq!(digest.algorithm(), HashAlgorithm::Sha256);
15//! assert_eq!(digest.to_string(), expected);
16//!
17//! // Compute a digest.
18//! let mut hasher = digest.algorithm().hasher();
19//! hasher.update(b"Hello, World!");
20//! assert_eq!(hasher.finalize(), digest);
21//! ```
22//!
23//! For parsing, the string representation is expected to be in the format
24//! `<algorithm>_<digest>` or `<algorithm>:<digest>`.
25//!
26//! The algorithm must be one of the following:
27//!
28//! - `sha256`
29//! - `sha512_256` or `sha512-256`
30//! - `sha512`
31//!
32//! Note that the underscore representation is preferred as it can be selected by double
33//! clicking in most applications.
34//!
35//! In the future, we may add additional hash algorithms.
36//!
37//! # Features
38//!
39//! This crate supports the following features:
40//!
41//! - `serde`: Enable serialization and deserialization support using Serde.
42//! - `legacy`: Use legacy formatting and algorithm names for compatibility with older
43//!   parsers.
44
45use std::fmt::Write;
46use std::str::FromStr;
47use std::sync::Arc;
48
49use sha2::Digest;
50
51#[cfg(feature = "serde")]
52mod serde;
53
54/// Define the data structures for the hash algorithms.
55macro_rules! define_hash_algorithms {
56    ($($variant:ident, $name:literal, [$($alias:literal),*], $size:literal, $hasher:ty;)*) => {
57        /// Cryptographic hash algorithms.
58        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59        #[non_exhaustive]
60        pub enum HashAlgorithm {
61            $(
62                $variant,
63            )*
64        }
65
66        impl HashAlgorithm {
67            /// Name of the algorithm.
68            #[must_use]
69            pub const fn name(self) -> &'static str {
70                match self {
71                    $(
72                        Self::$variant => $name,
73                    )*
74                }
75            }
76
77            /// Names of the algorithm.
78            #[must_use]
79            pub const fn names(self) -> &'static [&'static str] {
80                match self {
81                    $(
82                        Self::$variant => &[$name $(, $alias)*],
83                    )*
84                }
85            }
86
87            /// Create a fresh hasher.
88            pub fn hasher(self) -> Hasher {
89                match self {
90                    $(
91                        Self::$variant => Hasher {
92                            algorithm: self,
93                            inner: HasherInner::$variant(<$hasher>::new())
94                        },
95                    )*
96                }
97            }
98
99            /// Size of the hash.
100            #[must_use]
101            pub const fn hash_size(self) -> usize {
102                match self {
103                    $(
104                        Self::$variant => $size,
105                    )*
106                }
107            }
108        }
109
110        impl FromStr for HashAlgorithm {
111            type Err = InvalidAlgorithmError;
112
113            fn from_str(s: &str) -> Result<Self, Self::Err> {
114                match s {
115                    $(
116                        $name $(| $alias)* => Ok(Self::$variant),
117                    )*
118                    _ => Err(InvalidAlgorithmError),
119                }
120            }
121        }
122
123        /// Internal hasher representation.
124        #[derive(Debug, Clone)]
125        enum HasherInner {
126            $(
127                $variant($hasher),
128            )*
129        }
130
131        impl HasherInner {
132            /// Update the hash with the given bytes.
133            fn update(&mut self, bytes: &[u8]) {
134                match self {
135                    $(
136                        HasherInner::$variant(hasher) => hasher.update(bytes),
137                    )*
138                }
139            }
140
141            /// Finalize the hash.
142            #[must_use]
143            fn finalize<D>(self) -> D
144            where
145                D: for<'slice> From<&'slice [u8]>
146            {
147                match self {
148                    $(
149                        HasherInner::$variant(hasher) => hasher.finalize().as_slice().into(),
150                    )*
151                }
152            }
153        }
154    };
155}
156
157define_hash_algorithms! {
158    Sha256, "sha256", [], 32, sha2::Sha256;
159    Sha512_256, "sha512_256", ["sha512-256"], 32, sha2::Sha512_256;
160    Sha512, "sha512", [], 64, sha2::Sha512;
161}
162
163impl HashAlgorithm {
164    /// Hash the given bytes.
165    #[must_use]
166    pub fn hash<D>(self, bytes: &[u8]) -> HashDigest<D>
167    where
168        D: for<'slice> From<&'slice [u8]>,
169    {
170        let mut hasher = self.hasher();
171        hasher.update(bytes);
172        hasher.finalize()
173    }
174}
175
176impl std::fmt::Display for HashAlgorithm {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        if f.alternate() {
179            f.write_str(self.names().last().expect("algorithm has names"))
180        } else {
181            match self {
182                HashAlgorithm::Sha512_256 if cfg!(feature = "legacy") => f.write_str("sha512-256"),
183                _ => f.write_str(self.name()),
184            }
185        }
186    }
187}
188
189/// Invalid hash algorithm error.
190#[derive(Debug)]
191pub struct InvalidAlgorithmError;
192
193impl std::fmt::Display for InvalidAlgorithmError {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.write_str("invalid hash algorithm")
196    }
197}
198
199impl std::error::Error for InvalidAlgorithmError {}
200
201/// Hasher for computing hashes.
202#[derive(Debug, Clone)]
203#[must_use]
204pub struct Hasher {
205    algorithm: HashAlgorithm,
206    inner: HasherInner,
207}
208
209impl Hasher {
210    /// Algorithm of the hasher.
211    #[must_use]
212    pub const fn algorithm(&self) -> HashAlgorithm {
213        self.algorithm
214    }
215
216    /// Update the hasher with the given bytes.
217    pub fn update(&mut self, bytes: &[u8]) {
218        self.inner.update(bytes);
219    }
220
221    /// Finalize the hasher and return the digest.
222    #[must_use]
223    pub fn finalize<D>(self) -> HashDigest<D>
224    where
225        D: for<'slice> From<&'slice [u8]>,
226    {
227        HashDigest {
228            algorithm: self.algorithm,
229            raw: self.inner.finalize(),
230        }
231    }
232}
233
234/// Hash digest.
235#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
236pub struct HashDigest<D = Arc<[u8]>> {
237    /// Algorithm used to compute the digest.
238    algorithm: HashAlgorithm,
239    /// Raw digest.
240    raw: D,
241}
242
243impl<D> HashDigest<D>
244where
245    D: AsRef<[u8]>,
246{
247    /// Create [`HashDigest`] from the provided algorithm and raw digest.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the length of the raw digest does not match the expected size
252    /// for the algorithm.
253    pub fn new(algorithm: HashAlgorithm, raw: D) -> Result<Self, InvalidDigestError> {
254        if raw.as_ref().len() != algorithm.hash_size() {
255            return Err(InvalidDigestError("invalid digest size"));
256        }
257        Ok(Self::new_unchecked(algorithm, raw))
258    }
259
260    /// Create [`HashDigest`] from the provided algorithm and raw digest without checking
261    /// the digest's length.
262    pub const fn new_unchecked(algorithm: HashAlgorithm, raw: D) -> Self {
263        Self { algorithm, raw }
264    }
265
266    /// Algorithm used to compute the digest.
267    pub const fn algorithm(&self) -> HashAlgorithm {
268        self.algorithm
269    }
270
271    /// Raw digest.
272    pub fn raw(&self) -> &[u8] {
273        self.raw.as_ref()
274    }
275
276    /// Convert the raw digest to a hex string.
277    pub fn raw_hex_string(&self) -> String {
278        hex::encode(&self.raw)
279    }
280
281    /// Convert the digest back to its raw representation.
282    pub fn into_inner(self) -> D {
283        self.raw
284    }
285}
286
287impl<D> std::fmt::Display for HashDigest<D>
288where
289    D: AsRef<[u8]>,
290{
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        self.algorithm.fmt(f)?;
293        if cfg!(feature = "legacy") {
294            f.write_char(':')?;
295        } else {
296            f.write_char('_')?;
297        }
298        f.write_str(&self.raw_hex_string())?;
299        Ok(())
300    }
301}
302
303impl<D> AsRef<[u8]> for HashDigest<D>
304where
305    D: AsRef<[u8]>,
306{
307    fn as_ref(&self) -> &[u8] {
308        self.raw.as_ref()
309    }
310}
311
312impl<D: From<Vec<u8>>> FromStr for HashDigest<D> {
313    type Err = InvalidDigestError;
314
315    fn from_str(s: &str) -> Result<Self, Self::Err> {
316        let Some((algorithm, digest)) = s.rsplit_once([':', '_']) else {
317            return Err(InvalidDigestError("missing delimiter, expected ':' or '_'"));
318        };
319        let algorithm = HashAlgorithm::from_str(algorithm)?;
320        let Ok(raw) = hex::decode(digest) else {
321            return Err(InvalidDigestError("digest is not a hex string"));
322        };
323        if raw.len() != algorithm.hash_size() {
324            return Err(InvalidDigestError("invalid digest size"));
325        }
326        Ok(Self {
327            algorithm,
328            raw: raw.into(),
329        })
330    }
331}
332
333/// Invalid hash digest error.
334#[derive(Debug)]
335pub struct InvalidDigestError(pub(crate) &'static str);
336
337impl std::fmt::Display for InvalidDigestError {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        f.write_str(self.0)
340    }
341}
342
343impl std::error::Error for InvalidDigestError {}
344
345impl From<InvalidAlgorithmError> for InvalidDigestError {
346    fn from(_: InvalidAlgorithmError) -> Self {
347        InvalidDigestError("invalid hash algorithm")
348    }
349}