// SPDX-License-Identifier: MIT OR Apache-2.0 // Copyright 2026 Thomas #[cfg(all( not(target_feature = "atomics"), target_family = "wasm", feature = "talc" ))] #[global_allocator] static TALC: talc::wasm::WasmDynamicTalc = talc::wasm::new_wasm_dynamic_allocator(); const MAX_XOF_LEN: usize = 1 << 16; // 65_536 #[derive(Debug)] pub struct Blake3Error(pub String); impl core::fmt::Display for Blake3Error { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.0) } } impl std::error::Error for Blake3Error {} fn err(msg: &str) -> Blake3Error { Blake3Error(msg.to_string()) } fn checked_xof_len(out_len: usize) -> Result { if out_len == 0 || out_len > MAX_XOF_LEN { return Err(err("out_len must be between 1 and 65536")); } Ok(out_len) } /// Hash data and return a 32-byte BLAKE3 digest. pub fn hash(value: &[u8]) -> [u8; 32] { *blake3::hash(value).as_bytes() } /// Hash data with variable-length output (XOF mode). /// Returns `out_len` bytes of BLAKE3 extended output. pub fn hash_xof(data: &[u8], out_len: usize) -> Result, Blake3Error> { let out_len = checked_xof_len(out_len)?; let mut out = vec![0u8; out_len]; blake3::Hasher::new() .update(data) .finalize_xof() .fill(&mut out); Ok(out) } /// Compute a keyed BLAKE3 hash (MAC). Key must be exactly 32 bytes. /// Throws if the key length is wrong. pub fn keyed_hash(data: &[u8], key: &[u8]) -> Result<[u8; 32], Blake3Error> { let key: &[u8; 32] = key .try_into() .map_err(|_| err("key must be exactly 32 bytes"))?; Ok(*blake3::keyed_hash(key, data).as_bytes()) } /// Derive a 32-byte key from a context string and key material. /// Context should be a hardcoded, globally unique, application-specific string. pub fn derive_key(context: &str, key_material: &[u8]) -> [u8; 32] { blake3::derive_key(context, key_material) } pub struct Hasher(blake3::Hasher); impl Hasher { /// Create a new hasher for unkeyed hashing. pub fn new() -> Self { Hasher(blake3::Hasher::new()) } /// Create a keyed hasher (MAC mode). Key must be exactly 32 bytes. /// Throws if the key length is wrong. pub fn new_keyed(key: &[u8]) -> Result { let key: &[u8; 32] = key .try_into() .map_err(|_| err("key must be exactly 32 bytes"))?; Ok(Hasher(blake3::Hasher::new_keyed(key))) } /// Create a hasher in derive-key mode. /// Context should be a hardcoded, globally unique, application-specific string. /// Feed key material via `update()`, then call `finalize()`. pub fn new_derive_key(context: &str) -> Self { Hasher(blake3::Hasher::new_derive_key(context)) } /// Feed data into the hasher. Can be called multiple times for streaming. pub fn update(&mut self, data: &[u8]) { self.0.update(data); } /// Return the 32-byte hash digest. Non-destructive. Can be called multiple times. pub fn finalize(&self) -> [u8; 32] { *self.0.finalize().as_bytes() } /// Return `out_len` bytes of extended output (XOF mode). Non-destructive. pub fn finalize_xof(&self, out_len: usize) -> Result, Blake3Error> { let out_len = checked_xof_len(out_len)?; let mut out = vec![0u8; out_len]; self.0.finalize_xof().fill(&mut out); Ok(out) } /// Finalize the hash and reset the hasher in one call. /// Useful for hashing multiple inputs sequentially without creating new instances. pub fn finalize_and_reset(&mut self) -> [u8; 32] { let out = *self.0.finalize().as_bytes(); self.0.reset(); out } /// Reset the hasher to its initial state. Preserves the mode (keyed/derive-key). pub fn reset(&mut self) { self.0.reset(); } } impl Default for Hasher { fn default() -> Self { Self::new() } } #[cfg(feature = "wasm")] mod wasm { use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn hash(value: &[u8]) -> Vec { super::hash(value).to_vec() } #[wasm_bindgen(js_name = "hashXof")] pub fn hash_xof(data: &[u8], out_len: usize) -> Result, JsError> { super::hash_xof(data, out_len).map_err(|e| JsError::new(&e.0)) } #[wasm_bindgen(js_name = "keyedHash")] pub fn keyed_hash(data: &[u8], key: &[u8]) -> Result, JsError> { super::keyed_hash(data, key) .map(|h| h.to_vec()) .map_err(|e| JsError::new(&e.0)) } #[wasm_bindgen(js_name = "deriveKey")] pub fn derive_key(context: &str, key_material: &[u8]) -> Vec { super::derive_key(context, key_material).to_vec() } /// Incremental BLAKE3 hasher for streaming data. /// /// ```js /// const hasher = new Hasher(); /// hasher.update(chunk1); /// hasher.update(chunk2); /// const digest = hasher.finalize(); // 32 bytes /// ``` #[wasm_bindgen] pub struct Hasher(blake3::Hasher); #[wasm_bindgen] impl Hasher { /// Create a new hasher for unkeyed hashing. #[wasm_bindgen(constructor)] pub fn new() -> Self { Hasher(blake3::Hasher::new()) } /// Create a keyed hasher (MAC mode). Key must be exactly 32 bytes. /// Throws if the key length is wrong. #[wasm_bindgen(js_name = "newKeyed")] pub fn new_keyed(key: &[u8]) -> Result { let key: &[u8; 32] = key .try_into() .map_err(|_| JsError::new("key must be exactly 32 bytes"))?; Ok(Hasher(blake3::Hasher::new_keyed(key))) } /// Create a hasher in derive-key mode. /// Context should be a hardcoded, globally unique, application-specific string. /// Feed key material via `update()`, then call `finalize()`. #[wasm_bindgen(js_name = "newDeriveKey")] pub fn new_derive_key(context: &str) -> Self { Hasher(blake3::Hasher::new_derive_key(context)) } /// Feed data into the hasher. Can be called multiple times for streaming. pub fn update(&mut self, data: &[u8]) { self.0.update(data); } /// Return the 32-byte hash digest. Non-destructive. Can be called multiple times. pub fn finalize(&self) -> Vec { self.0.finalize().as_bytes().to_vec() } /// Return `out_len` bytes of extended output (XOF mode). Non-destructive. #[wasm_bindgen(js_name = "finalizeXof")] pub fn finalize_xof(&self, out_len: usize) -> Result, JsError> { let len = super::checked_xof_len(out_len).map_err(|e| JsError::new(&e.0))?; let mut out = vec![0u8; len]; self.0.finalize_xof().fill(&mut out); Ok(out) } /// Finalize the hash and reset the hasher in one call. /// Useful for hashing multiple inputs sequentially without creating new instances. #[wasm_bindgen(js_name = "finalizeAndReset")] pub fn finalize_and_reset(&mut self) -> Vec { let out = self.0.finalize().as_bytes().to_vec(); self.0.reset(); out } /// Reset the hasher to its initial state. Preserves the mode (keyed/derive-key). pub fn reset(&mut self) { self.0.reset(); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_hash_deterministic() { assert_eq!(hash(b"hello"), hash(b"hello")); } #[test] fn test_hash_length() { assert_eq!(hash(b"hello").len(), 32); } #[test] fn test_hash_xof_length() { assert_eq!(hash_xof(b"hello", 64).unwrap().len(), 64); assert_eq!(hash_xof(b"hello", 16).unwrap().len(), 16); } #[test] fn test_keyed_hash_valid() { let key = [0u8; 32]; assert!(keyed_hash(b"hello", &key).is_ok()); } #[test] fn test_keyed_hash_bad_key() { assert!(keyed_hash(b"hello", &[0u8; 16]).is_err()); } #[test] fn test_derive_key() { let a = derive_key("ctx", b"material"); let b = derive_key("ctx", b"material"); assert_eq!(a, b); assert_eq!(a.len(), 32); let c = derive_key("other", b"material"); assert_ne!(a, c); } #[test] fn test_hasher_streaming() { let oneshot = hash(b"helloworld"); let mut h = Hasher::new(); h.update(b"hello"); h.update(b"world"); assert_eq!(h.finalize(), oneshot); } #[test] fn test_hasher_derive_key() { let oneshot = derive_key("my-ctx", b"material"); let mut h = Hasher::new_derive_key("my-ctx"); h.update(b"material"); assert_eq!(h.finalize(), oneshot); } #[test] fn test_hasher_finalize_and_reset() { let mut h = Hasher::new(); h.update(b"hello"); let first = h.finalize_and_reset(); assert_eq!(first, hash(b"hello")); // After reset, hashing different data should produce different output h.update(b"world"); let second = h.finalize(); assert_eq!(second, hash(b"world")); assert_ne!(first, second); } #[test] fn test_keyed_hash_matches_streaming() { let key = [42u8; 32]; let oneshot = keyed_hash(b"hello world", &key).unwrap(); let mut h = Hasher::new_keyed(&key).unwrap(); h.update(b"hello "); h.update(b"world"); assert_eq!(h.finalize(), oneshot); } #[test] fn test_hasher_finalize_xof() { let oneshot = hash_xof(b"hello", 64).unwrap(); let mut h = Hasher::new(); h.update(b"hello"); assert_eq!(h.finalize_xof(64).unwrap(), oneshot); } #[test] fn test_hasher_finalize_xof_prefix_matches_hash() { // First 32 bytes of XOF output should equal the standard hash let standard = hash(b"test data"); let xof = hash_xof(b"test data", 64).unwrap(); assert_eq!(&xof[..32], standard.as_slice()); } #[test] fn test_hasher_reset() { let mut h = Hasher::new(); h.update(b"junk"); h.reset(); h.update(b"hello"); assert_eq!(h.finalize(), hash(b"hello")); } #[test] fn test_hasher_keyed_bad_key() { assert!(Hasher::new_keyed(&[0u8; 10]).is_err()); } #[test] fn test_hash_xof_zero_len() { assert!(hash_xof(b"hello", 0).is_err()); } #[test] fn test_hash_xof_over_max() { assert!(hash_xof(b"hello", 65_537).is_err()); } #[test] fn test_hasher_keyed() { let key = [1u8; 32]; let h = Hasher::new_keyed(&key).unwrap(); assert_ne!(h.finalize(), hash(b"").as_slice()); } // BLAKE3 official test vectors (from BLAKE3 spec) // Reference: https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json // Input: 0x00..0xfa repeating pattern, first N bytes fn test_input(len: usize) -> Vec { (0..len).map(|i| (i % 251) as u8).collect() } #[test] fn test_vector_empty() { let expected = hex::decode("af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262") .unwrap(); assert_eq!(hash(&[]).as_slice(), expected.as_slice()); } #[test] fn test_vector_1_byte() { let expected = hex::decode("2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213") .unwrap(); assert_eq!(hash(&test_input(1)).as_slice(), expected.as_slice()); } #[test] fn test_vector_1025_bytes() { let expected = hex::decode("d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444") .unwrap(); assert_eq!(hash(&test_input(1025)).as_slice(), expected.as_slice()); } #[test] fn test_vector_keyed_empty() { let key = b"whats the Elvish word for friend"; let expected = hex::decode("92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26") .unwrap(); assert_eq!( keyed_hash(&[], key).unwrap().as_slice(), expected.as_slice() ); } } #[cfg(all(target_arch = "wasm32", test))] mod wasm_tests { use super::wasm::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] fn test_hash_roundtrip() { let a = hash(b"hello"); let b = hash(b"hello"); assert_eq!(a, b); assert_eq!(a.len(), 32); } #[wasm_bindgen_test] fn test_keyed_hash_bad_key() { assert!(keyed_hash(b"hello", &[0u8; 16]).is_err()); } #[wasm_bindgen_test] fn test_hasher_keyed_bad_key() { assert!(Hasher::new_keyed(&[0u8; 10]).is_err()); } #[wasm_bindgen_test] fn test_hash_xof_zero_len() { assert!(hash_xof(b"hello", 0).is_err()); } #[wasm_bindgen_test] fn test_hash_xof_over_max() { assert!(hash_xof(b"hello", 65_537).is_err()); } #[wasm_bindgen_test] fn test_hasher_finalize_xof_zero_len() { let h = Hasher::new(); assert!(h.finalize_xof(0).is_err()); } #[wasm_bindgen_test] fn test_hasher_finalize_xof_over_max() { let h = Hasher::new(); assert!(h.finalize_xof(65_537).is_err()); } }