Skip to main content

atlas_common/hash/
mod.rs

1//! Cryptographic hash functionality
2//!
3//! This module provides secure cryptographic hashing with support for SHA-256, SHA-384, and SHA-512.
4//! It includes utilities for file hashing, hash verification, and incremental hashing.
5//!
6//! # Features
7//!
8//! - Multiple hash algorithms (SHA-256, SHA-384, SHA-512)
9//! - File and data hashing
10//! - Constant-time hash comparison for security
11//! - Incremental hashing with `HashBuilder`
12//! - Hash combination for merkle-tree-like structures
13//!
14//! # Example
15//!
16//! ```rust
17//! use atlas_common::hash::{calculate_hash, verify_hash, HashAlgorithm};
18//!
19//! let data = b"important data";
20//! let hash = calculate_hash(data);
21//! assert!(verify_hash(data, &hash));
22//!
23//! // Use specific algorithm
24//! let sha256_hash = atlas_common::hash::calculate_hash_with_algorithm(
25//!     data,
26//!     &HashAlgorithm::Sha256
27//! );
28//! ```
29mod algorithms;
30
31pub use algorithms::{
32    calculate_hash_optimized, get_hardware_capabilities, BatchHasher, HardwareCapabilities,
33    HashAlgorithm, HashBuilder, Hasher,
34};
35
36use crate::error::{Error, Result};
37use sha2::{Digest, Sha256, Sha384, Sha512};
38use std::io::Read;
39use std::path::Path;
40use subtle::ConstantTimeEq;
41
42/// Calculate hash using the default algorithm (SHA-384)
43pub fn calculate_hash(data: &[u8]) -> String {
44    calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384)
45}
46
47/// Calculate hash with specific algorithm
48pub fn calculate_hash_with_algorithm(data: &[u8], algorithm: &HashAlgorithm) -> String {
49    match algorithm {
50        HashAlgorithm::Sha256 => hex::encode(Sha256::digest(data)),
51        HashAlgorithm::Sha384 => hex::encode(Sha384::digest(data)),
52        HashAlgorithm::Sha512 => hex::encode(Sha512::digest(data)),
53    }
54}
55
56/// Calculate hash using the default algorithm (SHA-384)
57///
58/// # Example
59///
60/// ```rust
61/// use atlas_common::hash::calculate_hash;
62///
63/// let data = b"test data";
64/// let hash = calculate_hash(data);
65/// assert_eq!(hash.len(), 96); // SHA-384 produces 96 hex characters
66/// ```
67pub fn calculate_file_hash(path: impl AsRef<Path>) -> Result<String> {
68    calculate_file_hash_with_algorithm(path, &HashAlgorithm::Sha384)
69}
70
71/// Calculate hash with a specific algorithm
72///
73/// # Example
74///
75/// ```rust
76/// use atlas_common::hash::{calculate_hash_with_algorithm, HashAlgorithm};
77///
78/// let data = b"test data";
79/// let sha256_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
80/// assert_eq!(sha256_hash.len(), 64);
81/// ```
82pub fn calculate_file_hash_with_algorithm(
83    path: impl AsRef<Path>,
84    algorithm: &HashAlgorithm,
85) -> Result<String> {
86    let file = std::fs::File::open(path)?;
87
88    match algorithm {
89        HashAlgorithm::Sha256 => hash_reader::<Sha256, _>(file),
90        HashAlgorithm::Sha384 => hash_reader::<Sha384, _>(file),
91        HashAlgorithm::Sha512 => hash_reader::<Sha512, _>(file),
92    }
93}
94
95/// Combine multiple hashes into a single hash
96///
97/// Useful for creating merkle-tree-like structures or combining multiple asset hashes.
98///
99/// # Errors
100///
101/// Returns an error if any hash string is invalid hex.
102///
103/// # Example
104///
105/// ```rust
106/// use atlas_common::hash::{calculate_hash, combine_hashes};
107///
108/// let hash1 = calculate_hash(b"data1");
109/// let hash2 = calculate_hash(b"data2");
110/// let combined = combine_hashes(&[&hash1, &hash2])?;
111/// # Ok::<(), atlas_common::Error>(())
112/// ```
113pub fn combine_hashes(hashes: &[&str]) -> Result<String> {
114    combine_hashes_with_algorithm(hashes, &HashAlgorithm::Sha384)
115}
116
117/// Combine hashes with a specific algorithm
118///
119/// # Errors
120///
121/// Returns an error if any hash string is invalid hex.
122pub fn combine_hashes_with_algorithm(hashes: &[&str], algorithm: &HashAlgorithm) -> Result<String> {
123    let mut combined = Vec::new();
124    for hash in hashes {
125        let bytes = hex::decode(hash)?;
126        combined.extend_from_slice(&bytes);
127    }
128    Ok(calculate_hash_with_algorithm(&combined, algorithm))
129}
130
131/// Verify data against an expected hash
132///
133/// Automatically detects the hash algorithm from the hash length.
134/// Uses constant-time comparison to prevent timing attacks.
135///
136/// # Example
137///
138/// ```rust
139/// use atlas_common::hash::{calculate_hash, verify_hash};
140///
141/// let data = b"test data";
142/// let hash = calculate_hash(data);
143/// assert!(verify_hash(data, &hash));
144/// ```
145pub fn verify_hash(data: &[u8], expected_hash: &str) -> bool {
146    let algorithm = detect_hash_algorithm(expected_hash);
147    verify_hash_with_algorithm(data, expected_hash, &algorithm)
148}
149
150/// Verify hash with a specific algorithm
151///
152/// Uses constant-time comparison to prevent timing attacks.
153pub fn verify_hash_with_algorithm(
154    data: &[u8],
155    expected_hash: &str,
156    algorithm: &HashAlgorithm,
157) -> bool {
158    let calculated_hash = calculate_hash_with_algorithm(data, algorithm);
159    constant_time_compare(&calculated_hash, expected_hash)
160}
161
162/// Verify file hash
163///
164/// # Errors
165///
166/// Returns an error if the file cannot be read.
167pub fn verify_file_hash(path: impl AsRef<Path>, expected_hash: &str) -> Result<bool> {
168    let algorithm = detect_hash_algorithm(expected_hash);
169    verify_file_hash_with_algorithm(path, expected_hash, &algorithm)
170}
171
172/// Verify file hash with a specific algorithm
173///
174/// # Errors
175///
176/// Returns an error if the file cannot be read.
177pub fn verify_file_hash_with_algorithm(
178    path: impl AsRef<Path>,
179    expected_hash: &str,
180    algorithm: &HashAlgorithm,
181) -> Result<bool> {
182    let calculated_hash = calculate_file_hash_with_algorithm(path, algorithm)?;
183    Ok(constant_time_compare(&calculated_hash, expected_hash))
184}
185
186/// Detect hash algorithm from hash length
187///
188/// Returns:
189/// - SHA-256 for 64 character hashes
190/// - SHA-384 for 96 character hashes (default)
191/// - SHA-512 for 128 character hashes
192///
193/// # Example
194///
195/// ```rust
196/// use atlas_common::hash::{detect_hash_algorithm, HashAlgorithm};
197///
198/// let sha256_hash = "a".repeat(64);
199/// assert_eq!(detect_hash_algorithm(&sha256_hash), HashAlgorithm::Sha256);
200/// ```
201pub fn detect_hash_algorithm(hash: &str) -> HashAlgorithm {
202    match hash.len() {
203        64 => HashAlgorithm::Sha256,
204        96 => HashAlgorithm::Sha384,
205        128 => HashAlgorithm::Sha512,
206        _ => HashAlgorithm::Sha384, // Default
207    }
208}
209
210/// Get expected hash length in hex characters for an algorithm
211///
212/// # Example
213///
214/// ```rust
215/// use atlas_common::hash::{get_hash_length, HashAlgorithm};
216///
217/// assert_eq!(get_hash_length(&HashAlgorithm::Sha256), 64);
218/// assert_eq!(get_hash_length(&HashAlgorithm::Sha384), 96);
219/// assert_eq!(get_hash_length(&HashAlgorithm::Sha512), 128);
220/// ```
221pub fn get_hash_length(algorithm: &HashAlgorithm) -> usize {
222    match algorithm {
223        HashAlgorithm::Sha256 => 64,
224        HashAlgorithm::Sha384 => 96,
225        HashAlgorithm::Sha512 => 128,
226    }
227}
228
229/// Validate hash format
230///
231/// Checks that a hash string:
232/// - Contains only hexadecimal characters
233/// - Has a valid length (64, 96, or 128 characters)
234///
235/// # Errors
236///
237/// Returns an error if the hash format is invalid.
238///
239/// # Example
240///
241/// ```rust
242/// use atlas_common::hash::validate_hash_format;
243///
244/// assert!(validate_hash_format(&"a".repeat(96)).is_ok());
245/// assert!(validate_hash_format("not-a-hash").is_err());
246/// ```
247pub fn validate_hash_format(hash: &str) -> Result<()> {
248    if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
249        return Err(Error::Validation(
250            "Invalid hash: not hexadecimal".to_string(),
251        ));
252    }
253
254    let valid_lengths = [64, 96, 128];
255    if !valid_lengths.contains(&hash.len()) {
256        return Err(Error::Validation(format!(
257            "Invalid hash length: {} (expected 64, 96, or 128)",
258            hash.len()
259        )));
260    }
261
262    Ok(())
263}
264
265// Internal helper to hash from a reader
266fn hash_reader<D: Digest, R: Read>(mut reader: R) -> Result<String> {
267    let mut hasher = D::new();
268    let mut buffer = [0; 8192];
269
270    loop {
271        let bytes_read = reader.read(&mut buffer)?;
272        if bytes_read == 0 {
273            break;
274        }
275        hasher.update(&buffer[..bytes_read]);
276    }
277
278    Ok(hex::encode(hasher.finalize()))
279}
280
281// Constant-time comparison
282fn constant_time_compare(a: &str, b: &str) -> bool {
283    if a.len() != b.len() {
284        return false;
285    }
286
287    let a_bytes = a.as_bytes();
288    let b_bytes = b.as_bytes();
289
290    a_bytes.ct_eq(b_bytes).into()
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use tempfile::tempdir;
297
298    #[test]
299    fn test_calculate_hash() {
300        let data = b"test data";
301        let hash = calculate_hash(data);
302        assert_eq!(hash.len(), 96); // SHA-384
303    }
304
305    #[test]
306    fn test_verify_hash() {
307        let data = b"test data";
308        let hash = calculate_hash(data);
309        assert!(verify_hash(data, &hash));
310        assert!(!verify_hash(b"different data", &hash));
311    }
312
313    #[test]
314    fn test_file_hash() -> Result<()> {
315        let dir = tempdir()?;
316        let file_path = dir.path().join("test.txt");
317        std::fs::write(&file_path, b"test content")?;
318
319        let hash = calculate_file_hash(&file_path)?;
320        assert_eq!(hash.len(), 96);
321
322        assert!(verify_file_hash(&file_path, &hash)?);
323
324        Ok(())
325    }
326
327    #[test]
328    fn test_combine_hashes() -> Result<()> {
329        let hash1 = calculate_hash(b"data1");
330        let hash2 = calculate_hash(b"data2");
331
332        let combined = combine_hashes(&[&hash1, &hash2])?;
333        assert_eq!(combined.len(), 96);
334
335        // Order matters
336        let combined_reversed = combine_hashes(&[&hash2, &hash1])?;
337        assert_ne!(combined, combined_reversed);
338
339        Ok(())
340    }
341
342    #[test]
343    fn test_detect_algorithm() {
344        let sha256 = "a".repeat(64);
345        let sha384 = "b".repeat(96);
346        let sha512 = "c".repeat(128);
347
348        assert_eq!(detect_hash_algorithm(&sha256), HashAlgorithm::Sha256);
349        assert_eq!(detect_hash_algorithm(&sha384), HashAlgorithm::Sha384);
350        assert_eq!(detect_hash_algorithm(&sha512), HashAlgorithm::Sha512);
351    }
352}