Skip to main content

hush_core/
hashing.rs

1//! Cryptographic hashing (SHA-256 and Keccak-256)
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest as Sha2Digest, Sha256};
5use sha3::Keccak256;
6
7use crate::error::{Error, Result};
8
9/// A 32-byte hash value
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct Hash {
13    #[serde(with = "hash_serde")]
14    bytes: [u8; 32],
15}
16
17mod hash_serde {
18    use serde::{Deserialize, Deserializer, Serializer};
19
20    pub fn serialize<S>(bytes: &[u8; 32], s: S) -> std::result::Result<S::Ok, S::Error>
21    where
22        S: Serializer,
23    {
24        s.serialize_str(&format!("0x{}", hex::encode(bytes)))
25    }
26
27    pub fn deserialize<'de, D>(d: D) -> std::result::Result<[u8; 32], D::Error>
28    where
29        D: Deserializer<'de>,
30    {
31        let hex_str = String::deserialize(d)?;
32        let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str);
33        let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?;
34        bytes
35            .try_into()
36            .map_err(|_| serde::de::Error::custom("hash must be 32 bytes"))
37    }
38}
39
40impl Hash {
41    /// Create from raw bytes
42    pub fn from_bytes(bytes: [u8; 32]) -> Self {
43        Self { bytes }
44    }
45
46    /// Create from hex string (with or without 0x prefix)
47    pub fn from_hex(hex_str: &str) -> Result<Self> {
48        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
49
50        let bytes = hex::decode(hex_str).map_err(|e| Error::InvalidHex(e.to_string()))?;
51
52        if bytes.len() != 32 {
53            return Err(Error::InvalidHashLength {
54                expected: 32,
55                actual: bytes.len(),
56            });
57        }
58
59        let mut arr = [0u8; 32];
60        arr.copy_from_slice(&bytes);
61        Ok(Self::from_bytes(arr))
62    }
63
64    /// Get raw bytes
65    pub fn as_bytes(&self) -> &[u8; 32] {
66        &self.bytes
67    }
68
69    /// Export as hex (no prefix)
70    pub fn to_hex(&self) -> String {
71        hex::encode(self.bytes)
72    }
73
74    /// Export as 0x-prefixed hex
75    pub fn to_hex_prefixed(&self) -> String {
76        format!("0x{}", self.to_hex())
77    }
78
79    /// Zero hash
80    pub fn zero() -> Self {
81        Self { bytes: [0u8; 32] }
82    }
83}
84
85impl AsRef<[u8]> for Hash {
86    fn as_ref(&self) -> &[u8] {
87        &self.bytes
88    }
89}
90
91impl std::fmt::Display for Hash {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "0x{}", self.to_hex())
94    }
95}
96
97/// Compute SHA-256 hash of data.
98///
99/// # Examples
100///
101/// ```rust
102/// use hush_core::sha256;
103///
104/// let hash = sha256(b"hello");
105/// assert_eq!(hash.as_bytes().len(), 32);
106///
107/// // Known test vector
108/// assert_eq!(
109///     hash.to_hex(),
110///     "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
111/// );
112/// ```
113pub fn sha256(data: &[u8]) -> Hash {
114    let mut hasher = Sha256::new();
115    hasher.update(data);
116    let result = hasher.finalize();
117
118    let mut bytes = [0u8; 32];
119    bytes.copy_from_slice(&result);
120    Hash::from_bytes(bytes)
121}
122
123/// Compute SHA-256 hash and return as hex string
124pub fn sha256_hex(data: &[u8]) -> String {
125    sha256(data).to_hex_prefixed()
126}
127
128/// Compute Keccak-256 hash (Ethereum-compatible).
129///
130/// # Examples
131///
132/// ```rust
133/// use hush_core::keccak256;
134///
135/// let hash = keccak256(b"hello");
136/// assert_eq!(hash.as_bytes().len(), 32);
137///
138/// // Known test vector (Ethereum keccak256)
139/// assert_eq!(
140///     hash.to_hex(),
141///     "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
142/// );
143/// ```
144pub fn keccak256(data: &[u8]) -> Hash {
145    let mut hasher = Keccak256::new();
146    hasher.update(data);
147    let result = hasher.finalize();
148
149    let mut bytes = [0u8; 32];
150    bytes.copy_from_slice(&result);
151    Hash::from_bytes(bytes)
152}
153
154/// Compute Keccak-256 hash and return as hex string
155pub fn keccak256_hex(data: &[u8]) -> String {
156    keccak256(data).to_hex_prefixed()
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_sha256() {
165        let hash = sha256(b"hello");
166        // Known SHA-256 hash of "hello"
167        assert_eq!(
168            hash.to_hex(),
169            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
170        );
171    }
172
173    #[test]
174    fn test_sha256_hex() {
175        let hash = sha256_hex(b"hello");
176        assert!(hash.starts_with("0x"));
177        assert_eq!(hash.len(), 66); // 0x + 64 hex chars
178    }
179
180    #[test]
181    fn test_keccak256() {
182        let hash = keccak256(b"hello");
183        // Known Keccak-256 hash of "hello"
184        assert_eq!(
185            hash.to_hex(),
186            "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
187        );
188    }
189
190    #[test]
191    fn test_hash_from_hex() {
192        let original = sha256(b"test");
193        let from_hex = Hash::from_hex(&original.to_hex()).unwrap();
194        let from_hex_prefixed = Hash::from_hex(&original.to_hex_prefixed()).unwrap();
195
196        assert_eq!(original, from_hex);
197        assert_eq!(original, from_hex_prefixed);
198    }
199
200    #[test]
201    fn test_hash_serde() {
202        let hash = sha256(b"test");
203        let json = serde_json::to_string(&hash).unwrap();
204        let restored: Hash = serde_json::from_str(&json).unwrap();
205
206        assert_eq!(hash, restored);
207        assert!(json.contains("0x")); // Should be prefixed in JSON
208    }
209
210    #[test]
211    fn test_concat_hashes() {
212        fn concat_hashes(left: &Hash, right: &Hash) -> Hash {
213            let mut combined = [0u8; 64];
214            combined[..32].copy_from_slice(left.as_bytes());
215            combined[32..].copy_from_slice(right.as_bytes());
216            sha256(&combined)
217        }
218
219        let h1 = sha256(b"left");
220        let h2 = sha256(b"right");
221        let combined = concat_hashes(&h1, &h2);
222
223        // Should be deterministic
224        let combined2 = concat_hashes(&h1, &h2);
225        assert_eq!(combined, combined2);
226
227        // Order matters
228        let combined_reversed = concat_hashes(&h2, &h1);
229        assert_ne!(combined, combined_reversed);
230    }
231}