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.try_into().map_err(|_| {
85            DigestError(format!("Expected a Vec of length {} but it was {}", N, len))
86        })?;
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    pub fn name(&self) -> &'static str {
132        match self {
133            HashType::Md5(_) => "md5",
134            HashType::SHA1(_) => "sha1",
135            HashType::SHA256(_) => "sha256",
136            HashType::SHA384(_) => "sha384",
137            HashType::SHA512(_) => "sha512",
138        }
139    }
140
141    /// Unwrap the hash from the enum's types
142    pub fn the_hash(&self) -> String {
143        match self {
144            HashType::Md5(h) => h.to_string(),
145            HashType::SHA1(h) => h.to_string(),
146            HashType::SHA256(h) => h.to_string(),
147            HashType::SHA384(h) => h.to_string(),
148            HashType::SHA512(h) => h.to_string(),
149        }
150    }
151}
152
153impl TryFrom<String> for HashType {
154    type Error = DigestError;
155
156    fn try_from(value: String) -> Result<Self, Self::Error> {
157        let decoded = hex::decode(&value).unwrap();
158        Ok(match decoded.len() {
159            16 => HashType::Md5(Digest::try_from(decoded)?),
160            20 => HashType::SHA1(Digest::try_from(decoded)?),
161            32 => HashType::SHA256(Digest::try_from(decoded)?),
162            48 => HashType::SHA384(Digest::try_from(decoded)?),
163            64 => HashType::SHA512(Digest::try_from(decoded)?),
164            _ => return Err(DigestError(format!("unknown hash size {}", value.len()))),
165        })
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn strings() {
175        let digest = Digest([0x00, 0x11, 0x22, 0x33]);
176        assert_eq!(digest.to_string(), "00112233");
177        assert!(HashType::try_from(String::from("00112233")).is_err());
178    }
179
180    #[test]
181    fn sha1() {
182        const TEST: &str = "3204c1ca863c2068214900e831fb8047b934bf88";
183
184        let digest = HashType::try_from(String::from(TEST)).unwrap();
185        assert_eq!(digest.name(), "sha1");
186
187        if let HashType::Md5(_) = digest {
188            panic!("Failed: SHA-1 hash was made into MD-5");
189        }
190
191        if let HashType::SHA256(_) = digest {
192            panic!("Failed: SHA-1 hash was made into SHA-256");
193        }
194
195        if let HashType::SHA384(_) = digest {
196            panic!("Failed: SHA-1 hash was made into SHA-384");
197        }
198
199        if let HashType::SHA512(_) = digest {
200            panic!("Failed: SHA-1 hash was made into SHA-512");
201        }
202    }
203
204    #[test]
205    fn sha256() {
206        const TEST: &str = "d154b8420fc56a629df2e6d918be53310d8ac39a926aa5f60ae59a66298969a0";
207
208        let digest = HashType::try_from(String::from(TEST)).unwrap();
209        assert_eq!(digest.name(), "sha256");
210
211        if let HashType::Md5(_) = digest {
212            panic!("Failed: SHA-256 hash was made into MD-5");
213        }
214
215        if let HashType::SHA1(_) = digest {
216            panic!("Failed: SHA-256 hash was made into SHA-1");
217        }
218
219        if let HashType::SHA384(_) = digest {
220            panic!("Failed: SHA-256 hash was made into SHA-384");
221        }
222
223        if let HashType::SHA512(_) = digest {
224            panic!("Failed: SHA-256 hash was made into SHA-512");
225        }
226    }
227
228    #[test]
229    fn sha512() {
230        const TEST: &str = "dafe60f7d02b0151909550d6f20343d0fe374b044d40221c13295a312489e1b702edbeac99ffda85f61b812b1ddd0c9394cda0c1162bffb716f04d996ff73cdf";
231
232        let digest = HashType::try_from(String::from(TEST)).unwrap();
233        assert_eq!(digest.name(), "sha512");
234
235        if let HashType::Md5(_) = digest {
236            panic!("Failed: SHA-512 hash was made into MD-5");
237        }
238
239        if let HashType::SHA1(_) = digest {
240            panic!("Failed: SHA-512 hash was made into SHA-1");
241        }
242
243        if let HashType::SHA256(_) = digest {
244            panic!("Failed: SHA-512 hash was made into SHA-256");
245        }
246
247        if let HashType::SHA384(_) = digest {
248            panic!("Failed: SHA-512 hash was made into SHA-384");
249        }
250    }
251}