malwaredb_api/
digest.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::borrow::Borrow;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6use std::ops::Deref;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10// Adapted from
11// https://github.com/profianinc/steward/commit/69a4f297e06cbc95f327d271a691198230c97429#diff-adf0e917b493348b9f22a754b89ff8644fd3af28a769f75caaec2ffd47edfea4
12// Idea for this Digest struct by Roman Volosatovs <roman@profian.com>
13
14/// Digest generic in hash size `N`, serialized and deserialized as hexidecimal strings.
15#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
16pub struct Digest<const N: usize>(pub [u8; N]);
17
18impl<'de, const N: usize> Deserialize<'de> for Digest<N> {
19    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
20    where
21        D: Deserializer<'de>,
22    {
23        use serde::de::Error;
24
25        let dig: String = Deserialize::deserialize(deserializer)?;
26        let dig = hex::decode(dig).map_err(|e| Error::custom(format!("invalid hex: {e}")))?;
27        let dig = dig.try_into().map_err(|v: Vec<_>| {
28            Error::custom(format!(
29                "expected digest to have length of {N}, got {}",
30                v.len()
31            ))
32        })?;
33        Ok(Digest(dig))
34    }
35}
36
37impl<const N: usize> Serialize for Digest<N> {
38    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39    where
40        S: Serializer,
41    {
42        let hex = self.to_string();
43        serializer.serialize_str(&hex)
44    }
45}
46
47impl<const N: usize> AsRef<[u8; N]> for Digest<N> {
48    fn as_ref(&self) -> &[u8; N] {
49        &self.0
50    }
51}
52
53impl<const N: usize> Borrow<[u8; N]> for Digest<N> {
54    fn borrow(&self) -> &[u8; N] {
55        &self.0
56    }
57}
58
59impl<const N: usize> Deref for Digest<N> {
60    type Target = [u8; N];
61
62    fn deref(&self) -> &Self::Target {
63        &self.0
64    }
65}
66
67/// Digest error, generally for a hash of an unexpected size.
68#[derive(Debug, Clone)]
69pub struct DigestError(String);
70
71impl Display for DigestError {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        write!(f, "{}", self.0)
74    }
75}
76
77impl Error for DigestError {}
78
79impl<const N: usize> TryFrom<Vec<u8>> for Digest<N> {
80    type Error = DigestError;
81
82    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
83        let len = value.len();
84        let array: [u8; N] = value
85            .try_into()
86            .map_err(|_| DigestError(format!("Expected a Vec of length {N} but it was {len}")))?;
87        Ok(Digest(array))
88    }
89}
90
91impl<const N: usize> Display for Digest<N> {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", hex::encode(self.0))
94    }
95}
96
97/// The hash by which a sample is identified
98#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash)]
99pub enum HashType {
100    /// MD5
101    Md5(Digest<16>),
102
103    /// SHA-1
104    SHA1(Digest<20>),
105
106    /// SHA-256, assumed to be SHA2-256
107    SHA256(Digest<32>),
108
109    /// SHA-384, assumed to be SHA2-384
110    SHA384(Digest<48>),
111
112    /// SHA-512, assumed to be SHA2-512
113    SHA512(Digest<64>),
114}
115
116impl Display for HashType {
117    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118        match self {
119            HashType::Md5(h) => write!(f, "MD5: {h}"),
120            HashType::SHA1(h) => write!(f, "SHA-1: {h}"),
121            HashType::SHA256(h) => write!(f, "SHA-256: {h}"),
122            HashType::SHA384(h) => write!(f, "SHA-384: {h}"),
123            HashType::SHA512(h) => write!(f, "SHA-512: {h}"),
124        }
125    }
126}
127
128impl HashType {
129    /// Return the name of the hash type, used to decide
130    /// on the database field to find the match
131    #[must_use]
132    pub fn name(&self) -> &'static str {
133        match self {
134            HashType::Md5(_) => "md5",
135            HashType::SHA1(_) => "sha1",
136            HashType::SHA256(_) => "sha256",
137            HashType::SHA384(_) => "sha384",
138            HashType::SHA512(_) => "sha512",
139        }
140    }
141
142    /// Unwrap the hash from the enum's types
143    #[must_use]
144    pub fn the_hash(&self) -> String {
145        match self {
146            HashType::Md5(h) => h.to_string(),
147            HashType::SHA1(h) => h.to_string(),
148            HashType::SHA256(h) => h.to_string(),
149            HashType::SHA384(h) => h.to_string(),
150            HashType::SHA512(h) => h.to_string(),
151        }
152    }
153}
154
155impl TryFrom<String> for HashType {
156    type Error = DigestError;
157
158    fn try_from(value: String) -> Result<Self, Self::Error> {
159        let decoded = hex::decode(&value).unwrap();
160        Ok(match decoded.len() {
161            16 => HashType::Md5(Digest::try_from(decoded)?),
162            20 => HashType::SHA1(Digest::try_from(decoded)?),
163            32 => HashType::SHA256(Digest::try_from(decoded)?),
164            48 => HashType::SHA384(Digest::try_from(decoded)?),
165            64 => HashType::SHA512(Digest::try_from(decoded)?),
166            _ => return Err(DigestError(format!("unknown hash size {}", value.len()))),
167        })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn strings() {
177        let digest = Digest([0x00, 0x11, 0x22, 0x33]);
178        assert_eq!(digest.to_string(), "00112233");
179        assert!(HashType::try_from(String::from("00112233")).is_err());
180    }
181
182    #[test]
183    fn sha1() {
184        const TEST: &str = "3204c1ca863c2068214900e831fb8047b934bf88";
185
186        let digest = HashType::try_from(String::from(TEST)).unwrap();
187        assert_eq!(digest.name(), "sha1");
188
189        if let HashType::Md5(_) = digest {
190            panic!("Failed: SHA-1 hash was made into MD-5");
191        }
192
193        if let HashType::SHA256(_) = digest {
194            panic!("Failed: SHA-1 hash was made into SHA-256");
195        }
196
197        if let HashType::SHA384(_) = digest {
198            panic!("Failed: SHA-1 hash was made into SHA-384");
199        }
200
201        if let HashType::SHA512(_) = digest {
202            panic!("Failed: SHA-1 hash was made into SHA-512");
203        }
204    }
205
206    #[test]
207    fn sha256() {
208        const TEST: &str = "d154b8420fc56a629df2e6d918be53310d8ac39a926aa5f60ae59a66298969a0";
209
210        let digest = HashType::try_from(String::from(TEST)).unwrap();
211        assert_eq!(digest.name(), "sha256");
212
213        if let HashType::Md5(_) = digest {
214            panic!("Failed: SHA-256 hash was made into MD-5");
215        }
216
217        if let HashType::SHA1(_) = digest {
218            panic!("Failed: SHA-256 hash was made into SHA-1");
219        }
220
221        if let HashType::SHA384(_) = digest {
222            panic!("Failed: SHA-256 hash was made into SHA-384");
223        }
224
225        if let HashType::SHA512(_) = digest {
226            panic!("Failed: SHA-256 hash was made into SHA-512");
227        }
228    }
229
230    #[test]
231    fn sha512() {
232        const TEST: &str = "dafe60f7d02b0151909550d6f20343d0fe374b044d40221c13295a312489e1b702edbeac99ffda85f61b812b1ddd0c9394cda0c1162bffb716f04d996ff73cdf";
233
234        let digest = HashType::try_from(String::from(TEST)).unwrap();
235        assert_eq!(digest.name(), "sha512");
236
237        if let HashType::Md5(_) = digest {
238            panic!("Failed: SHA-512 hash was made into MD-5");
239        }
240
241        if let HashType::SHA1(_) = digest {
242            panic!("Failed: SHA-512 hash was made into SHA-1");
243        }
244
245        if let HashType::SHA256(_) = digest {
246            panic!("Failed: SHA-512 hash was made into SHA-256");
247        }
248
249        if let HashType::SHA384(_) = digest {
250            panic!("Failed: SHA-512 hash was made into SHA-384");
251        }
252    }
253}