rust_sign/
hash.rs

1//! BLAKE3 hashing utilities for document signing.
2
3use crate::error::Result;
4use std::fs::File;
5use std::io::{BufReader, Read};
6use std::path::Path;
7
8/// The size of a BLAKE3 hash output in bytes.
9pub const HASH_SIZE: usize = 32;
10
11/// A BLAKE3 hash of document content.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DocumentHash([u8; HASH_SIZE]);
14
15impl DocumentHash {
16    /// Create a hash from raw bytes.
17    pub fn from_bytes(bytes: [u8; HASH_SIZE]) -> Self {
18        Self(bytes)
19    }
20
21    /// Get the raw bytes of the hash.
22    pub fn as_bytes(&self) -> &[u8; HASH_SIZE] {
23        &self.0
24    }
25
26    /// Encode the hash as a base64 string.
27    pub fn to_base64(&self) -> String {
28        use base64::Engine;
29        base64::engine::general_purpose::STANDARD.encode(self.0)
30    }
31
32    /// Decode a hash from a base64 string.
33    pub fn from_base64(s: &str) -> Result<Self> {
34        use base64::Engine;
35        let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
36        if bytes.len() != HASH_SIZE {
37            return Err(crate::error::SignError::InvalidFormat(format!(
38                "Invalid hash length: expected {}, got {}",
39                HASH_SIZE,
40                bytes.len()
41            )));
42        }
43        let mut arr = [0u8; HASH_SIZE];
44        arr.copy_from_slice(&bytes);
45        Ok(Self(arr))
46    }
47
48    /// Encode the hash as a hexadecimal string.
49    pub fn to_hex(&self) -> String {
50        self.0.iter().map(|b| format!("{:02x}", b)).collect()
51    }
52}
53
54/// Compute the BLAKE3 hash of a byte slice.
55pub fn hash_bytes(data: &[u8]) -> DocumentHash {
56    let hash = blake3::hash(data);
57    DocumentHash(*hash.as_bytes())
58}
59
60/// Compute the BLAKE3 hash of a file using streaming (memory efficient).
61pub fn hash_file<P: AsRef<Path>>(path: P) -> Result<DocumentHash> {
62    let file = File::open(path)?;
63    let mut reader = BufReader::new(file);
64    hash_reader(&mut reader)
65}
66
67/// Compute the BLAKE3 hash from any reader using streaming.
68pub fn hash_reader<R: Read>(reader: &mut R) -> Result<DocumentHash> {
69    let mut hasher = blake3::Hasher::new();
70    let mut buffer = [0u8; 8192];
71    
72    loop {
73        let bytes_read = reader.read(&mut buffer)?;
74        if bytes_read == 0 {
75            break;
76        }
77        hasher.update(&buffer[..bytes_read]);
78    }
79    
80    let hash = hasher.finalize();
81    Ok(DocumentHash(*hash.as_bytes()))
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_hash_bytes() {
90        let data = b"Hello, World!";
91        let hash = hash_bytes(data);
92        
93        // Verify hash is consistent
94        let hash2 = hash_bytes(data);
95        assert_eq!(hash, hash2);
96        
97        // Different data should produce different hash
98        let hash3 = hash_bytes(b"Different data");
99        assert_ne!(hash, hash3);
100    }
101
102    #[test]
103    fn test_base64_roundtrip() {
104        let data = b"Test data for hashing";
105        let hash = hash_bytes(data);
106        
107        let encoded = hash.to_base64();
108        let decoded = DocumentHash::from_base64(&encoded).unwrap();
109        
110        assert_eq!(hash, decoded);
111    }
112
113    #[test]
114    fn test_hex_encoding() {
115        let data = b"Test";
116        let hash = hash_bytes(data);
117        let hex = hash.to_hex();
118        
119        // Hex string should be 64 characters (32 bytes * 2)
120        assert_eq!(hex.len(), 64);
121        assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
122    }
123}
124