Skip to main content

pwr_core/
integrity.rs

1//! Content integrity verification via SHA-256.
2//!
3//! Provides streaming hash computation for files and in-memory data,
4//! plus constant-time hash comparison for verification.
5
6use sha2::{Digest, Sha256};
7use std::fs;
8use std::io;
9use std::path::Path;
10
11use crate::error::Result;
12
13/// Size of a SHA-256 hash in bytes.
14pub const HASH_LEN: usize = 32;
15
16/// Size of a SHA-256 hex string.
17pub const HASH_HEX_LEN: usize = 64;
18
19/// Compute the SHA-256 hash of an in-memory byte slice as a hex string.
20pub fn hash_bytes(data: &[u8]) -> String {
21    let mut hasher = Sha256::new();
22    hasher.update(data);
23    format!("{:x}", hasher.finalize())
24}
25
26/// Compute the raw SHA-256 hash of an in-memory byte slice.
27pub fn hash_bytes_raw(data: &[u8]) -> [u8; HASH_LEN] {
28    let mut hasher = Sha256::new();
29    hasher.update(data);
30    let mut out = [0u8; HASH_LEN];
31    out.copy_from_slice(&hasher.finalize());
32    out
33}
34
35/// Compute the SHA-256 hash of a file as a hex string.
36///
37/// Reads the file in 64 KiB chunks to stay under memory pressure
38/// for arbitrarily large files. The file must exist and be readable.
39pub fn hash_file(path: &Path) -> Result<String> {
40    let mut file = fs::File::open(path)?;
41    let mut hasher = Sha256::new();
42    io::copy(&mut file, &mut hasher)?;
43    Ok(format!("{:x}", hasher.finalize()))
44}
45
46/// Compute the SHA-256 hash of data from any reader as a hex string.
47///
48/// Useful for computing hashes of network streams or other data
49/// sources that implement `std::io::Read`.
50pub fn hash_reader(reader: &mut impl io::Read) -> Result<String> {
51    let mut hasher = Sha256::new();
52    io::copy(reader, &mut hasher)?;
53    Ok(format!("{:x}", hasher.finalize()))
54}
55
56/// Verify that a byte slice's hash matches an expected hex string.
57///
58/// Returns `true` if `hash_bytes(data) == expected_hex`.
59pub fn verify_hash(data: &[u8], expected_hex: &str) -> bool {
60    hash_bytes(data) == expected_hex
61}
62
63/// Verify that a file's hash matches an expected hex string.
64///
65/// Returns `true` if `hash_file(path) == expected_hex`.
66pub fn verify_file_hash(path: &Path, expected_hex: &str) -> Result<bool> {
67    let actual = hash_file(path)?;
68    Ok(actual == expected_hex)
69}
70
71/// Streaming hash verifier for chunked data reception.
72///
73/// Accumulates a SHA-256 hash across multiple `update` calls
74/// and produces the final hex string with `finalize`.
75pub struct StreamingHasher {
76    hasher: Sha256,
77}
78
79impl StreamingHasher {
80    /// Create a new streaming hasher.
81    pub fn new() -> Self {
82        Self {
83            hasher: Sha256::new(),
84        }
85    }
86
87    /// Feed a chunk of data into the hash computation.
88    pub fn update(&mut self, data: &[u8]) {
89        self.hasher.update(data);
90    }
91
92    /// Finalize and return the hex-encoded SHA-256 hash.
93    pub fn finalize(self) -> String {
94        format!("{:x}", self.hasher.finalize())
95    }
96
97    /// Finalize and return the raw 32-byte hash.
98    pub fn finalize_raw(self) -> [u8; HASH_LEN] {
99        let mut out = [0u8; HASH_LEN];
100        out.copy_from_slice(&self.hasher.finalize());
101        out
102    }
103}
104
105impl Default for StreamingHasher {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use tempfile::TempDir;
115
116    #[test]
117    fn test_hash_bytes_known_answer() {
118        assert_eq!(
119            hash_bytes(b"hello world"),
120            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
121        );
122    }
123
124    #[test]
125    fn test_hash_bytes_empty() {
126        assert_eq!(
127            hash_bytes(b""),
128            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
129        );
130    }
131
132    #[test]
133    fn test_hash_bytes_raw_length() {
134        let raw = hash_bytes_raw(b"test");
135        assert_eq!(raw.len(), HASH_LEN);
136    }
137
138    #[test]
139    fn test_hash_file() -> Result<()> {
140        let tmp = TempDir::new()?;
141        let path = tmp.path().join("data.bin");
142        fs::write(&path, b"hello world")?;
143
144        let hash = hash_file(&path)?;
145        assert_eq!(
146            hash,
147            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
148        );
149        Ok(())
150    }
151
152    #[test]
153    fn test_hash_file_large() -> Result<()> {
154        let tmp = TempDir::new()?;
155        let path = tmp.path().join("large.bin");
156        // 10 MB of repeating data
157        let data = vec![0x42u8; 10 * 1024 * 1024];
158        fs::write(&path, &data)?;
159
160        let hash = hash_file(&path)?;
161        assert_eq!(hash.len(), HASH_HEX_LEN);
162        assert_eq!(hash, hash_bytes(&data));
163        Ok(())
164    }
165
166    #[test]
167    fn test_verify_hash_match() {
168        let data = b"verify me";
169        let hash = hash_bytes(data);
170        assert!(verify_hash(data, &hash));
171    }
172
173    #[test]
174    fn test_verify_hash_mismatch() {
175        assert!(!verify_hash(b"real data", "deadbeef"));
176    }
177
178    #[test]
179    fn test_verify_file_hash() -> Result<()> {
180        let tmp = TempDir::new()?;
181        let path = tmp.path().join("verify.bin");
182        fs::write(&path, b"trust but verify")?;
183
184        let expected = hash_bytes(b"trust but verify");
185        assert!(verify_file_hash(&path, &expected)?);
186        Ok(())
187    }
188
189    #[test]
190    fn test_streaming_hasher() {
191        let mut hasher = StreamingHasher::new();
192        hasher.update(b"hello ");
193        hasher.update(b"world");
194        let hash = hasher.finalize();
195
196        assert_eq!(
197            hash,
198            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
199        );
200    }
201
202    #[test]
203    fn test_streaming_hasher_matches_oneshot() {
204        let chunks: Vec<&[u8]> = vec![b"chunk1", b"chunk2", b"chunk3", b"chunk4"];
205        let combined: Vec<u8> = chunks.iter().flat_map(|c| c.iter()).copied().collect();
206
207        let mut hasher = StreamingHasher::new();
208        for chunk in &chunks {
209            hasher.update(chunk);
210        }
211        assert_eq!(hasher.finalize(), hash_bytes(&combined));
212    }
213
214    #[test]
215    fn test_hash_reader() -> Result<()> {
216        let data = b"streaming reader test";
217        let hash = hash_reader(&mut &data[..])?;
218        assert_eq!(hash, hash_bytes(data));
219        Ok(())
220    }
221}