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