Skip to main content

voided_core/hash/
mod.rs

1//! Hashing module providing SHA-256, SHA-512, HMAC, and PBKDF2.
2
3use crate::{Error, Result};
4use alloc::{format, string::String, vec, vec::Vec};
5use hmac::{Hmac, Mac};
6use sha2::{Digest, Sha256, Sha512};
7
8const SALTED_HASH_DOMAIN: &[u8] = b"voided:hash-with-salt:v2";
9
10/// Largest useful SHA-256 fingerprint truncation in bytes.
11pub const MAX_FINGERPRINT_BYTES: usize = 32;
12
13/// Largest accepted grouping for the human-readable fingerprint formatter.
14pub const MAX_FINGERPRINT_GROUP_SIZE: usize = 32;
15
16/// Supported hash algorithms
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum HashAlgorithm {
19    /// SHA-256 (32 bytes output)
20    Sha256,
21    /// SHA-512 (64 bytes output)
22    Sha512,
23}
24
25impl HashAlgorithm {
26    /// Get output length in bytes
27    pub fn output_len(&self) -> usize {
28        match self {
29            HashAlgorithm::Sha256 => 32,
30            HashAlgorithm::Sha512 => 64,
31        }
32    }
33}
34
35enum StreamingHasherState {
36    Sha256(Sha256),
37    Sha512(Sha512),
38}
39
40/// Incremental SHA-256 or SHA-512 hasher.
41///
42/// This keeps hashing state inside Voided so callers can process large inputs
43/// without first assembling them into one contiguous allocation.
44pub struct StreamingHasher {
45    state: StreamingHasherState,
46    bytes_hashed: u128,
47}
48
49impl StreamingHasher {
50    /// Create an incremental hasher for `algorithm`.
51    pub fn new(algorithm: HashAlgorithm) -> Self {
52        let state = match algorithm {
53            HashAlgorithm::Sha256 => StreamingHasherState::Sha256(Sha256::new()),
54            HashAlgorithm::Sha512 => StreamingHasherState::Sha512(Sha512::new()),
55        };
56
57        Self {
58            state,
59            bytes_hashed: 0,
60        }
61    }
62
63    /// Add the next contiguous chunk of bytes to the hash.
64    pub fn update(&mut self, data: &[u8]) {
65        match &mut self.state {
66            StreamingHasherState::Sha256(hasher) => hasher.update(data),
67            StreamingHasherState::Sha512(hasher) => hasher.update(data),
68        }
69        self.bytes_hashed += data.len() as u128;
70    }
71
72    /// Return the total number of bytes supplied through [`Self::update`].
73    pub fn bytes_hashed(&self) -> u128 {
74        self.bytes_hashed
75    }
76
77    /// Consume the hasher and return the digest bytes.
78    pub fn finalize_bytes(self) -> Vec<u8> {
79        match self.state {
80            StreamingHasherState::Sha256(hasher) => hasher.finalize().to_vec(),
81            StreamingHasherState::Sha512(hasher) => hasher.finalize().to_vec(),
82        }
83    }
84
85    /// Consume the hasher and return the lowercase hexadecimal digest.
86    pub fn finalize_hex(self) -> String {
87        hex::encode(self.finalize_bytes())
88    }
89}
90
91/// Generate a hash using the specified algorithm
92pub fn hash(data: &[u8], algorithm: HashAlgorithm) -> Vec<u8> {
93    let mut hasher = StreamingHasher::new(algorithm);
94    hasher.update(data);
95    hasher.finalize_bytes()
96}
97
98/// Generate a hash and return as hex string
99pub fn hash_hex(data: &[u8], algorithm: HashAlgorithm) -> String {
100    hex::encode(hash(data, algorithm))
101}
102
103/// Generate a hash with salt
104pub fn hash_with_salt(data: &[u8], salt: &[u8], algorithm: HashAlgorithm) -> Vec<u8> {
105    // Length-prefix and domain-separate both fields. Concatenating data || salt
106    // makes distinct tuples such as ("a", "bc") and ("ab", "c") collide.
107    let mut combined =
108        Vec::with_capacity(SALTED_HASH_DOMAIN.len() + 16 + data.len().saturating_add(salt.len()));
109    combined.extend_from_slice(SALTED_HASH_DOMAIN);
110    combined.extend_from_slice(&(data.len() as u64).to_be_bytes());
111    combined.extend_from_slice(data);
112    combined.extend_from_slice(&(salt.len() as u64).to_be_bytes());
113    combined.extend_from_slice(salt);
114    hash(&combined, algorithm)
115}
116
117/// Generate a hash with salt and return as hex string
118pub fn hash_with_salt_hex(data: &[u8], salt: &[u8], algorithm: HashAlgorithm) -> String {
119    hex::encode(hash_with_salt(data, salt, algorithm))
120}
121
122/// Compare two hashes in constant time to prevent timing attacks
123pub fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
124    constant_time_eq::constant_time_eq(a, b)
125}
126
127/// Generate HMAC using the specified algorithm
128pub fn generate_hmac(data: &[u8], key: &[u8], algorithm: HashAlgorithm) -> Result<Vec<u8>> {
129    match algorithm {
130        HashAlgorithm::Sha256 => {
131            let mut mac = Hmac::<Sha256>::new_from_slice(key)
132                .map_err(|e| Error::HashFailed(e.to_string()))?;
133            mac.update(data);
134            Ok(mac.finalize().into_bytes().to_vec())
135        }
136        HashAlgorithm::Sha512 => {
137            let mut mac = Hmac::<Sha512>::new_from_slice(key)
138                .map_err(|e| Error::HashFailed(e.to_string()))?;
139            mac.update(data);
140            Ok(mac.finalize().into_bytes().to_vec())
141        }
142    }
143}
144
145/// Generate a SHA-256 HMAC without heap-allocating the digest.
146pub fn generate_hmac_sha256(data: &[u8], key: &[u8]) -> Result<[u8; 32]> {
147    let mut mac =
148        Hmac::<Sha256>::new_from_slice(key).map_err(|e| Error::HashFailed(e.to_string()))?;
149    mac.update(data);
150    let mut output = [0u8; 32];
151    output.copy_from_slice(&mac.finalize().into_bytes());
152    Ok(output)
153}
154
155/// Generate HMAC over multiple contiguous logical parts without copying them first.
156pub fn generate_hmac_parts(
157    parts: &[&[u8]],
158    key: &[u8],
159    algorithm: HashAlgorithm,
160) -> Result<Vec<u8>> {
161    match algorithm {
162        HashAlgorithm::Sha256 => {
163            let mut mac = Hmac::<Sha256>::new_from_slice(key)
164                .map_err(|e| Error::HashFailed(e.to_string()))?;
165            for part in parts {
166                mac.update(*part);
167            }
168            Ok(mac.finalize().into_bytes().to_vec())
169        }
170        HashAlgorithm::Sha512 => {
171            let mut mac = Hmac::<Sha512>::new_from_slice(key)
172                .map_err(|e| Error::HashFailed(e.to_string()))?;
173            for part in parts {
174                mac.update(*part);
175            }
176            Ok(mac.finalize().into_bytes().to_vec())
177        }
178    }
179}
180
181/// Generate a SHA-256 HMAC over multiple logical parts without heap-allocating the digest.
182pub fn generate_hmac_sha256_parts(parts: &[&[u8]], key: &[u8]) -> Result<[u8; 32]> {
183    let mut mac =
184        Hmac::<Sha256>::new_from_slice(key).map_err(|e| Error::HashFailed(e.to_string()))?;
185    for part in parts {
186        mac.update(*part);
187    }
188    let mut output = [0u8; 32];
189    output.copy_from_slice(&mac.finalize().into_bytes());
190    Ok(output)
191}
192
193/// Generate HMAC and return as hex string
194pub fn generate_hmac_hex(data: &[u8], key: &[u8], algorithm: HashAlgorithm) -> Result<String> {
195    Ok(hex::encode(generate_hmac(data, key, algorithm)?))
196}
197
198/// Verify HMAC in constant time
199pub fn verify_hmac(
200    data: &[u8],
201    expected_mac: &[u8],
202    key: &[u8],
203    algorithm: HashAlgorithm,
204) -> Result<bool> {
205    let actual_mac = generate_hmac(data, key, algorithm)?;
206    Ok(compare_hashes(&actual_mac, expected_mac))
207}
208
209/// Hash data using PBKDF2-HMAC-SHA256 with high iterations
210pub fn hash_with_pbkdf2(data: &[u8], salt: &[u8], iterations: u32) -> Result<Vec<u8>> {
211    use pbkdf2::pbkdf2_hmac;
212
213    crate::encryption::validate_pbkdf2_parameters(salt, iterations)?;
214    let mut output = [0u8; 32];
215    pbkdf2_hmac::<Sha256>(data, salt, iterations, &mut output);
216    Ok(output.to_vec())
217}
218
219/// Verify data against a PBKDF2 hash
220pub fn verify_pbkdf2(
221    data: &[u8],
222    expected_hash: &[u8],
223    salt: &[u8],
224    iterations: u32,
225) -> Result<bool> {
226    let actual_hash = hash_with_pbkdf2(data, salt, iterations)?;
227    Ok(compare_hashes(&actual_hash, expected_hash))
228}
229
230/// Generate a fingerprint (truncated hash)
231/// Returns `length` bytes as hex (so 2*length hex characters)
232pub fn generate_fingerprint(data: &[u8], length: usize) -> String {
233    let hash = hash_hex(data, HashAlgorithm::Sha256);
234    // Each byte is 2 hex chars, so we take length*2 hex chars
235    let hex_len = length.min(MAX_FINGERPRINT_BYTES) * 2;
236    hash[..hex_len].to_string()
237}
238
239/// Format a SHA-256 fingerprint for human comparison.
240///
241/// This is not the Signal Safety Number protocol and does not bind identities,
242/// devices, key order, or a session transcript.
243pub fn generate_safety_numbers(data: &[u8], group_size: usize) -> Result<String> {
244    if !(1..=MAX_FINGERPRINT_GROUP_SIZE).contains(&group_size) {
245        return Err(Error::InvalidConfiguration(format!(
246            "fingerprint group size must be between 1 and {MAX_FINGERPRINT_GROUP_SIZE}"
247        )));
248    }
249    let hash_bytes = hash(data, HashAlgorithm::Sha256);
250    Ok(format_safety_numbers(&hash_bytes, group_size))
251}
252
253fn format_safety_numbers(hash_bytes: &[u8], group_size: usize) -> String {
254    let mut groups = Vec::new();
255
256    for chunk in hash_bytes.chunks(group_size) {
257        let group: Vec<String> = chunk.iter().map(|&byte| format!("{:03}", byte)).collect();
258        groups.push(group.join(" "));
259    }
260
261    groups.join("  ")
262}
263
264/// Generate random bytes
265pub fn generate_random_bytes(length: usize) -> Vec<u8> {
266    use rand::RngCore;
267    let mut bytes = vec![0u8; length];
268    rand::thread_rng().fill_bytes(&mut bytes);
269    bytes
270}
271
272/// Generate a random salt
273pub fn generate_salt(length: usize) -> Vec<u8> {
274    generate_random_bytes(length)
275}
276
277/// Securely wipe a buffer
278pub fn secure_wipe(buffer: &mut [u8]) {
279    use zeroize::Zeroize;
280    buffer.zeroize();
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn assert_streaming_matches_one_shot(
288        data: &[u8],
289        algorithm: HashAlgorithm,
290        chunk_sizes: &[usize],
291    ) {
292        let expected_bytes = hash(data, algorithm);
293        let expected_hex = hash_hex(data, algorithm);
294
295        let mut bytes_hasher = StreamingHasher::new(algorithm);
296        let mut offset = 0;
297        let mut chunk_index = 0;
298        while offset < data.len() {
299            let chunk_size = chunk_sizes[chunk_index % chunk_sizes.len()];
300            let end = (offset + chunk_size).min(data.len());
301            bytes_hasher.update(&data[offset..end]);
302            offset = end;
303            chunk_index += 1;
304        }
305        assert_eq!(bytes_hasher.bytes_hashed(), data.len() as u128);
306        assert_eq!(bytes_hasher.finalize_bytes(), expected_bytes);
307
308        let mut hex_hasher = StreamingHasher::new(algorithm);
309        for chunk in data.chunks(113) {
310            hex_hasher.update(chunk);
311        }
312        assert_eq!(hex_hasher.bytes_hashed(), data.len() as u128);
313        assert_eq!(hex_hasher.finalize_hex(), expected_hex);
314    }
315
316    #[test]
317    fn streaming_hash_matches_one_shot_for_sha256_and_sha512() {
318        let short = b"incremental hashing across uneven chunks";
319        let multi_megabyte: Vec<u8> = (0..(3 * 1024 * 1024 + 257))
320            .map(|index| ((index * 31 + index / 251) % 256) as u8)
321            .collect();
322
323        for (algorithm, block_size) in [
324            (HashAlgorithm::Sha256, 64usize),
325            (HashAlgorithm::Sha512, 128usize),
326        ] {
327            assert_streaming_matches_one_shot(&[], algorithm, &[1]);
328            assert_streaming_matches_one_shot(short, algorithm, &[1, 2, 7, 19]);
329
330            for length in [block_size - 1, block_size, block_size + 1, block_size * 2] {
331                let block_boundary: Vec<u8> = (0..length)
332                    .map(|index| ((index * 17 + 11) % 256) as u8)
333                    .collect();
334                assert_streaming_matches_one_shot(
335                    &block_boundary,
336                    algorithm,
337                    &[1, block_size - 1, block_size + 3],
338                );
339            }
340
341            assert_streaming_matches_one_shot(&multi_megabyte, algorithm, &[1, 31, 4_096, 65_537]);
342        }
343    }
344
345    #[test]
346    fn test_sha256() {
347        let data = b"hello world";
348        let hash = hash_hex(data, HashAlgorithm::Sha256);
349        // Known SHA-256 hash of "hello world"
350        assert_eq!(
351            hash,
352            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
353        );
354    }
355
356    #[test]
357    fn test_sha512() {
358        let data = b"hello world";
359        let hash = hash_hex(data, HashAlgorithm::Sha512);
360        assert_eq!(hash.len(), 128); // 64 bytes = 128 hex chars
361    }
362
363    #[test]
364    fn test_hash_with_salt() {
365        let data = b"password";
366        let salt = b"random_salt";
367
368        let hash1 = hash_with_salt_hex(data, salt, HashAlgorithm::Sha256);
369        let hash2 = hash_with_salt_hex(data, salt, HashAlgorithm::Sha256);
370
371        // Same inputs should produce same hash
372        assert_eq!(hash1, hash2);
373
374        // Different salt should produce different hash
375        let hash3 = hash_with_salt_hex(data, b"different_salt", HashAlgorithm::Sha256);
376        assert_ne!(hash1, hash3);
377
378        assert_ne!(
379            hash_with_salt(b"a", b"bc", HashAlgorithm::Sha256),
380            hash_with_salt(b"ab", b"c", HashAlgorithm::Sha256)
381        );
382    }
383
384    #[test]
385    fn test_hmac() {
386        let data = b"message";
387        let key = b"secret_key";
388
389        let mac = generate_hmac_hex(data, key, HashAlgorithm::Sha256).unwrap();
390        assert_eq!(mac.len(), 64); // 32 bytes = 64 hex chars
391
392        // Verify should pass with correct data
393        let mac_bytes = hex::decode(&mac).unwrap();
394        assert!(verify_hmac(data, &mac_bytes, key, HashAlgorithm::Sha256).unwrap());
395
396        // Verify should fail with wrong data
397        assert!(!verify_hmac(b"wrong", &mac_bytes, key, HashAlgorithm::Sha256).unwrap());
398    }
399
400    #[test]
401    fn test_pbkdf2() {
402        let password = b"my_password";
403        let salt = b"16-byte-test-salt";
404        let iterations = crate::encryption::PBKDF2_MIN_ITERATIONS;
405
406        let hash1 = hash_with_pbkdf2(password, salt, iterations).unwrap();
407        let hash2 = hash_with_pbkdf2(password, salt, iterations).unwrap();
408
409        assert_eq!(hash1, hash2);
410        assert!(verify_pbkdf2(password, &hash1, salt, iterations).unwrap());
411        assert!(!verify_pbkdf2(b"wrong_password", &hash1, salt, iterations).unwrap());
412        assert!(hash_with_pbkdf2(password, salt, 0).is_err());
413    }
414
415    #[test]
416    fn test_compare_hashes_constant_time() {
417        let hash1 = hash(b"test", HashAlgorithm::Sha256);
418        let hash2 = hash(b"test", HashAlgorithm::Sha256);
419        let hash3 = hash(b"different", HashAlgorithm::Sha256);
420
421        assert!(compare_hashes(&hash1, &hash2));
422        assert!(!compare_hashes(&hash1, &hash3));
423    }
424
425    #[test]
426    fn test_fingerprint() {
427        let data = b"some key material";
428        // Request 8 bytes, get 16 hex characters (2 hex chars per byte)
429        let fp = generate_fingerprint(data, 8);
430        assert_eq!(fp.len(), 16);
431
432        // Request 4 bytes, get 8 hex characters
433        let fp2 = generate_fingerprint(data, 4);
434        assert_eq!(fp2.len(), 8);
435    }
436
437    #[test]
438    fn test_safety_numbers() {
439        let data = b"public key data";
440        let numbers = generate_safety_numbers(data, 5).unwrap();
441        assert!(!numbers.is_empty());
442        // Should contain groups of 3-digit numbers
443        assert!(numbers.contains(' '));
444        assert!(generate_safety_numbers(data, 0).is_err());
445    }
446}