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}