Skip to main content

anti_sec/
sec_hash.rs

1use std::fs::File;
2use std::io::{self, Read};
3use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7use hmac::{Hmac, Mac};
8use md5::Md5;
9use sha1::Sha1;
10use sha2::Digest;
11use sha2::Sha256;
12
13/// Supported hash algorithms
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HashAlgorithm {
16    Md5,
17    Sha1,
18    Sha256,
19    HmacMd5,
20    HmacSha1,
21    HmacSha256,
22}
23
24/// Compute the hash of an arbitrary byte slice.
25pub fn hash_bytes(data: &[u8], algorithm: HashAlgorithm, key: Option<&[u8]>) -> io::Result<String> {
26    let result = match algorithm {
27        HashAlgorithm::Md5 => format!("{:x}", Md5::digest(data)),
28        HashAlgorithm::Sha1 => format!("{:x}", Sha1::digest(data)),
29        HashAlgorithm::Sha256 => format!("{:x}", Sha256::digest(data)),
30        HashAlgorithm::HmacMd5 => {
31            let key =
32                key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
33            type HmacMd5 = Hmac<Md5>;
34            let mut mac = HmacMd5::new_from_slice(key)
35                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
36            mac.update(data);
37            to_hex(&mac.finalize().into_bytes())
38        }
39        HashAlgorithm::HmacSha1 => {
40            let key =
41                key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
42            type HmacSha1 = Hmac<Sha1>;
43            let mut mac = HmacSha1::new_from_slice(key)
44                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
45            mac.update(data);
46            to_hex(&mac.finalize().into_bytes())
47        }
48        HashAlgorithm::HmacSha256 => {
49            let key =
50                key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
51            type HmacSha256 = Hmac<Sha256>;
52            let mut mac = HmacSha256::new_from_slice(key)
53                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
54            mac.update(data);
55            to_hex(&mac.finalize().into_bytes())
56        }
57    };
58
59    Ok(result)
60}
61
62/// Hash data from a reader.
63pub fn hash_reader<R: Read>(
64    mut reader: R,
65    algorithm: HashAlgorithm,
66    key: Option<&[u8]>,
67) -> io::Result<String> {
68    let mut buffer = Vec::new();
69    reader.read_to_end(&mut buffer)?;
70    hash_bytes(&buffer, algorithm, key)
71}
72
73/// Compute the hash of a file using the selected algorithm. For HMAC
74/// algorithms, a secret key must be provided via the `key` argument.
75pub fn hash_file(path: &Path, algorithm: HashAlgorithm, key: Option<&[u8]>) -> io::Result<String> {
76    let file = File::open(path)?;
77    hash_reader(file, algorithm, key)
78}
79
80/// Recursively hash a directory by hashing each contained file and then
81/// hashing the concatenation of those hashes. The search depth can be
82/// limited with `max_depth`.
83pub fn hash_directory(
84    path: &Path,
85    algorithm: HashAlgorithm,
86    key: Option<&[u8]>,
87    max_depth: usize,
88) -> io::Result<String> {
89    let mut files: Vec<PathBuf> = WalkDir::new(path)
90        .max_depth(max_depth)
91        .into_iter()
92        .filter_map(|e| e.ok())
93        .filter(|e| e.file_type().is_file())
94        .map(|e| e.into_path())
95        .collect();
96
97    files.sort();
98
99    let mut combined = String::new();
100    for f in files {
101        let h = hash_file(&f, algorithm, key)?;
102        combined.push_str(&h);
103    }
104
105    hash_bytes(combined.as_bytes(), algorithm, key)
106}
107
108fn to_hex(bytes: &[u8]) -> String {
109    let mut out = String::with_capacity(bytes.len() * 2);
110    for b in bytes {
111        use std::fmt::Write as _;
112        write!(&mut out, "{:02x}", b).unwrap();
113    }
114    out
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::fs::{self, File};
121    use std::io::Write;
122    use tempfile::tempdir;
123
124    fn create_temp_file(content: &[u8]) -> std::path::PathBuf {
125        use std::time::{SystemTime, UNIX_EPOCH};
126        let mut path = std::env::temp_dir();
127        let ts = SystemTime::now()
128            .duration_since(UNIX_EPOCH)
129            .unwrap()
130            .as_nanos();
131        path.push(format!("anti_sec_test_{}", ts));
132        let mut f = File::create(&path).unwrap();
133        f.write_all(content).unwrap();
134        path
135    }
136
137    #[test]
138    fn test_md5_hash() {
139        let path = create_temp_file(b"hello");
140        let digest = hash_file(&path, HashAlgorithm::Md5, None).unwrap();
141        fs::remove_file(&path).unwrap();
142        assert_eq!(digest, "5d41402abc4b2a76b9719d911017c592");
143    }
144
145    #[test]
146    fn test_sha1_hash() {
147        let path = create_temp_file(b"hello");
148        let digest = hash_file(&path, HashAlgorithm::Sha1, None).unwrap();
149        fs::remove_file(&path).unwrap();
150        assert_eq!(digest, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
151    }
152
153    #[test]
154    fn test_sha256_hash() {
155        let path = create_temp_file(b"hello");
156        let digest = hash_file(&path, HashAlgorithm::Sha256, None).unwrap();
157        fs::remove_file(&path).unwrap();
158        assert_eq!(
159            digest,
160            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
161        );
162    }
163
164    #[test]
165    fn test_hmac_sha256_hash() {
166        let path = create_temp_file(b"hello");
167        let digest = hash_file(&path, HashAlgorithm::HmacSha256, Some(b"secret")).unwrap();
168        fs::remove_file(&path).unwrap();
169        assert_eq!(
170            digest,
171            "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b"
172        );
173    }
174
175    #[test]
176    fn test_hash_reader() {
177        let data = b"reader";
178        let digest = hash_reader(&data[..], HashAlgorithm::Sha256, None).unwrap();
179        assert_eq!(
180            digest,
181            "3d0941964aa3ebdcb00ccef58b1bb399f9f898465e9886d5aec7f31090a0fb30"
182        );
183    }
184
185    #[test]
186    fn test_hash_directory() {
187        let dir = tempdir().unwrap();
188        let file_a = dir.path().join("a.txt");
189        let file_b = dir.path().join("b.txt");
190        fs::write(&file_a, b"foo").unwrap();
191        fs::write(&file_b, b"bar").unwrap();
192        let digest = hash_directory(dir.path(), HashAlgorithm::Sha256, None, 1).unwrap();
193        // Compute expected: hash of files individually then concat and hash again
194        let h1 = hash_file(&file_a, HashAlgorithm::Sha256, None).unwrap();
195        let h2 = hash_file(&file_b, HashAlgorithm::Sha256, None).unwrap();
196        let mut concat = String::new();
197        let mut names = vec![(&file_a, h1), (&file_b, h2)];
198        names.sort_by_key(|(p, _)| p.to_path_buf());
199        for (_, h) in names {
200            concat.push_str(&h);
201        }
202        let expected = hash_bytes(concat.as_bytes(), HashAlgorithm::Sha256, None).unwrap();
203        assert_eq!(digest, expected);
204    }
205}