gix_hash/
hasher.rs

1/// The error returned by [`Hasher::try_finalize()`](crate::Hasher::try_finalize()).
2#[derive(Debug, thiserror::Error)]
3#[allow(missing_docs)]
4pub enum Error {
5    #[error("Detected SHA-1 collision attack with digest {digest}")]
6    CollisionAttack { digest: crate::ObjectId },
7}
8
9pub(super) mod _impl {
10    use sha1_checked::{CollisionResult, Digest};
11
12    use crate::hasher::Error;
13
14    /// Hash implementations that can be used once.
15    #[derive(Clone)]
16    pub enum Hasher {
17        /// An implementation of the SHA1 hash.
18        ///
19        /// We use [`sha1_checked`] to implement the same collision detection algorithm as Git.
20        Sha1(sha1_checked::Sha1),
21        /// An implementation of the SHA256 hash.
22        #[cfg(feature = "sha256")]
23        Sha256(sha2::Sha256),
24    }
25
26    impl Hasher {
27        /// Let's not make this public to force people to go through [`hasher()`].
28        fn new_sha1() -> Self {
29            // This matches the configuration used by Git, which only uses
30            // the collision detection to bail out, rather than computing
31            // alternate “safe hashes” for inputs where a collision attack
32            // was detected.
33            Self::Sha1(sha1_checked::Builder::default().safe_hash(false).build())
34        }
35
36        /// Let's not make this public to force people to go through [`hasher()`].
37        #[cfg(feature = "sha256")]
38        fn new_sha256() -> Self {
39            Self::Sha256(sha2::Sha256::new())
40        }
41    }
42
43    impl Hasher {
44        /// Digest the given `bytes`.
45        pub fn update(&mut self, bytes: &[u8]) {
46            match self {
47                Hasher::Sha1(sha1) => sha1.update(bytes),
48                #[cfg(feature = "sha256")]
49                Hasher::Sha256(sha256) => sha256.update(bytes),
50            }
51        }
52
53        /// Finalize the hash and produce an object id.
54        ///
55        /// Returns [`Error`] if a collision attack is detected.
56        // TODO: Since SHA-256 has an infallible `finalize`, it might be worth investigating
57        //       turning the return type into `Result<crate::ObjectId, Infallible>` when this crate is
58        //       compiled with SHA-256 support only.
59        #[inline]
60        pub fn try_finalize(self) -> Result<crate::ObjectId, Error> {
61            match self {
62                Hasher::Sha1(sha1) => match sha1.try_finalize() {
63                    CollisionResult::Ok(digest) => Ok(crate::ObjectId::Sha1(digest.into())),
64                    CollisionResult::Mitigated(_) => {
65                        // SAFETY: `CollisionResult::Mitigated` is only
66                        // returned when `safe_hash()` is on. `Hasher`’s field
67                        // is private, and we only construct the SHA-1 variant
68                        // via `Hasher::new_sha1()` (and thus through `hasher()`),
69                        // which configures the builder with `safe_hash(false)`.
70                        //
71                        // As of Rust 1.84.1, the compiler can’t figure out
72                        // this function cannot panic without this.
73                        #[allow(unsafe_code)]
74                        unsafe {
75                            std::hint::unreachable_unchecked()
76                        }
77                    }
78                    CollisionResult::Collision(digest) => Err(Error::CollisionAttack {
79                        digest: crate::ObjectId::Sha1(digest.into()),
80                    }),
81                },
82                #[cfg(feature = "sha256")]
83                Hasher::Sha256(sha256) => Ok(crate::ObjectId::Sha256(sha256.finalize().into())),
84            }
85        }
86    }
87
88    /// Produce a hasher suitable for the given `kind` of hash.
89    #[inline]
90    pub fn hasher(kind: crate::Kind) -> Hasher {
91        match kind {
92            crate::Kind::Sha1 => Hasher::new_sha1(),
93            #[cfg(feature = "sha256")]
94            crate::Kind::Sha256 => Hasher::new_sha256(),
95        }
96    }
97}