Skip to main content

crypt_io/mac/
blake3_impl.rs

1//! BLAKE3 keyed-mode backend.
2//!
3//! BLAKE3's keyed mode (`blake3::keyed_hash`) takes a fixed 32-byte key
4//! and produces a 32-byte tag. It is the BLAKE3-native authenticator and
5//! is typically 4–10× faster than HMAC-SHA256 on modern hardware.
6//!
7//! Unlike HMAC, the key is type-checked as `&[u8; 32]` — there is no
8//! variable-length key surface. This matches BLAKE3's design intent (the
9//! key is a fixed-size secret derived elsewhere — from `key-vault`, from
10//! an HKDF expansion, etc.) and removes a class of "I gave it the wrong
11//! key length" bugs at compile time.
12
13use super::{BLAKE3_MAC_KEY_LEN, BLAKE3_MAC_OUTPUT_LEN};
14
15/// Compute a BLAKE3 keyed-mode tag over `data` under `key`.
16///
17/// # Example
18///
19/// ```
20/// # #[cfg(feature = "mac-blake3")] {
21/// use crypt_io::mac;
22/// let key = [0x42u8; 32];
23/// let tag = mac::blake3_keyed(&key, b"message");
24/// assert_eq!(tag.len(), 32);
25/// # }
26/// ```
27#[must_use]
28pub fn blake3_keyed(key: &[u8; BLAKE3_MAC_KEY_LEN], data: &[u8]) -> [u8; BLAKE3_MAC_OUTPUT_LEN] {
29    *::blake3::keyed_hash(key, data).as_bytes()
30}
31
32/// Verify a BLAKE3 keyed-mode tag in constant time.
33///
34/// Computes the tag for `(key, data)` and compares it to `expected_tag`
35/// using BLAKE3's [`Hash`] equality, which is constant-time (the BLAKE3
36/// crate documents this guarantee on `Hash::PartialEq`).
37///
38/// Returns `true` if the tags match, `false` otherwise (including when
39/// `expected_tag` is not 32 bytes long).
40///
41/// **Always** use this rather than `tag == expected`.
42///
43/// [`Hash`]: ::blake3::Hash
44///
45/// # Example
46///
47/// ```
48/// # #[cfg(feature = "mac-blake3")] {
49/// use crypt_io::mac;
50/// let key = [0x42u8; 32];
51/// let tag = mac::blake3_keyed(&key, b"message");
52/// assert!(mac::blake3_keyed_verify(&key, b"message", &tag));
53/// assert!(!mac::blake3_keyed_verify(&key, b"tampered", &tag));
54/// # }
55/// ```
56#[must_use]
57pub fn blake3_keyed_verify(
58    key: &[u8; BLAKE3_MAC_KEY_LEN],
59    data: &[u8],
60    expected_tag: &[u8],
61) -> bool {
62    if expected_tag.len() != BLAKE3_MAC_OUTPUT_LEN {
63        return false;
64    }
65    let computed = ::blake3::keyed_hash(key, data);
66    let mut expected = [0u8; BLAKE3_MAC_OUTPUT_LEN];
67    expected.copy_from_slice(expected_tag);
68    let expected_hash = ::blake3::Hash::from_bytes(expected);
69    computed == expected_hash
70}
71
72/// Streaming BLAKE3 keyed-mode MAC for inputs that don't fit in memory.
73///
74/// Construct with [`Blake3Mac::new`], absorb data with
75/// [`update`](Self::update), and finalise with [`finalize`](Self::finalize)
76/// (returns the 32-byte tag) or [`verify`](Self::verify) (constant-time
77/// compare against an expected tag).
78///
79/// # Example
80///
81/// ```
82/// # #[cfg(feature = "mac-blake3")] {
83/// use crypt_io::mac::Blake3Mac;
84///
85/// let key = [0x42u8; 32];
86/// let mut m = Blake3Mac::new(&key);
87/// m.update(b"first chunk ");
88/// m.update(b"second chunk");
89/// let tag = m.finalize();
90/// assert_eq!(tag.len(), 32);
91/// # }
92/// ```
93#[derive(Debug, Clone)]
94pub struct Blake3Mac {
95    inner: ::blake3::Hasher,
96}
97
98impl Blake3Mac {
99    /// Construct a fresh keyed MAC.
100    ///
101    /// Unlike HMAC this is infallible — BLAKE3's keyed mode takes a
102    /// type-checked 32-byte key, so there is no runtime length-check to
103    /// fail.
104    #[must_use]
105    pub fn new(key: &[u8; BLAKE3_MAC_KEY_LEN]) -> Self {
106        Self {
107            inner: ::blake3::Hasher::new_keyed(key),
108        }
109    }
110
111    /// Absorb `data` into the running MAC. Returns `&mut Self` so calls
112    /// can chain.
113    pub fn update(&mut self, data: &[u8]) -> &mut Self {
114        let _ = self.inner.update(data);
115        self
116    }
117
118    /// Finalise the MAC and return the 32-byte tag. Consumes the hasher.
119    #[must_use]
120    pub fn finalize(self) -> [u8; BLAKE3_MAC_OUTPUT_LEN] {
121        *self.inner.finalize().as_bytes()
122    }
123
124    /// Finalise and verify against `expected_tag` in constant time.
125    /// Returns `true` iff the computed tag matches `expected_tag` (and
126    /// `expected_tag` is the correct length).
127    /// Consumes the hasher.
128    #[must_use]
129    pub fn verify(self, expected_tag: &[u8]) -> bool {
130        if expected_tag.len() != BLAKE3_MAC_OUTPUT_LEN {
131            return false;
132        }
133        let mut expected = [0u8; BLAKE3_MAC_OUTPUT_LEN];
134        expected.copy_from_slice(expected_tag);
135        let expected_hash = ::blake3::Hash::from_bytes(expected);
136        self.inner.finalize() == expected_hash
137    }
138}
139
140#[cfg(test)]
141#[allow(clippy::unwrap_used, clippy::expect_used, unused_results)]
142mod tests {
143    use super::*;
144
145    // BLAKE3 official test set uses a 32-byte ASCII key
146    // "whats the Elvish word for friend" (exactly 32 chars). The empty-
147    // input keyed tag is pinned below. Subsequent tests verify the
148    // wrapper round-trips against the upstream primitive for known
149    // inputs.
150
151    const ELVISH_KEY: &[u8; 32] = b"whats the Elvish word for friend";
152
153    // Empty input keyed tag — value computed against the upstream `blake3`
154    // crate and pinned as a byte-array constant so we catch any future
155    // wrapper-level mistake immediately.
156    const KAT_EMPTY: [u8; 32] = [
157        0x92, 0xb2, 0xb7, 0x56, 0x04, 0xed, 0x3c, 0x76, 0x1f, 0x9d, 0x6f, 0x62, 0x39, 0x2c, 0x8a,
158        0x92, 0x27, 0xad, 0x0e, 0xa3, 0xf0, 0x95, 0x73, 0xe7, 0x83, 0xf1, 0x49, 0x8a, 0x4e, 0xd6,
159        0x0d, 0x26,
160    ];
161
162    #[test]
163    fn kat_empty_input() {
164        assert_eq!(blake3_keyed(ELVISH_KEY, b""), KAT_EMPTY);
165        assert!(blake3_keyed_verify(ELVISH_KEY, b"", &KAT_EMPTY));
166    }
167
168    #[test]
169    fn round_trip_short_input() {
170        let key = [0x01u8; 32];
171        let tag = blake3_keyed(&key, b"the quick brown fox");
172        assert!(blake3_keyed_verify(&key, b"the quick brown fox", &tag));
173    }
174
175    #[test]
176    fn different_keys_produce_different_tags() {
177        let key1 = [0x01u8; 32];
178        let key2 = [0x02u8; 32];
179        let tag1 = blake3_keyed(&key1, b"same data");
180        let tag2 = blake3_keyed(&key2, b"same data");
181        assert_ne!(tag1, tag2);
182    }
183
184    #[test]
185    fn different_data_produces_different_tags() {
186        let key = [0x01u8; 32];
187        let tag1 = blake3_keyed(&key, b"data one");
188        let tag2 = blake3_keyed(&key, b"data two");
189        assert_ne!(tag1, tag2);
190    }
191
192    #[test]
193    fn verify_rejects_wrong_tag() {
194        let key = [0x01u8; 32];
195        let tag = blake3_keyed(&key, b"message");
196        let mut tampered = tag;
197        tampered[0] ^= 0x01;
198        assert!(!blake3_keyed_verify(&key, b"message", &tampered));
199    }
200
201    #[test]
202    fn verify_rejects_wrong_key() {
203        let correct = [0x01u8; 32];
204        let wrong = [0x02u8; 32];
205        let tag = blake3_keyed(&correct, b"message");
206        assert!(!blake3_keyed_verify(&wrong, b"message", &tag));
207    }
208
209    #[test]
210    fn verify_rejects_wrong_data() {
211        let key = [0x01u8; 32];
212        let tag = blake3_keyed(&key, b"original");
213        assert!(!blake3_keyed_verify(&key, b"tampered", &tag));
214    }
215
216    #[test]
217    fn verify_rejects_truncated_tag() {
218        let key = [0x01u8; 32];
219        let tag = blake3_keyed(&key, b"message");
220        assert!(!blake3_keyed_verify(&key, b"message", &tag[..16]));
221    }
222
223    #[test]
224    fn verify_rejects_oversized_tag() {
225        let key = [0x01u8; 32];
226        let tag = blake3_keyed(&key, b"message");
227        let mut oversized = alloc::vec::Vec::from(&tag[..]);
228        oversized.push(0u8);
229        assert!(!blake3_keyed_verify(&key, b"message", &oversized));
230    }
231
232    // --- Streaming-equivalence + verify tests ---
233
234    #[test]
235    fn streaming_equals_one_shot() {
236        let key = [0x42u8; 32];
237        let data = b"the quick brown fox jumps over the lazy dog";
238        let one_shot = blake3_keyed(&key, data);
239        let mut m = Blake3Mac::new(&key);
240        m.update(&data[..10]);
241        m.update(&data[10..25]);
242        m.update(&data[25..]);
243        assert_eq!(m.finalize(), one_shot);
244    }
245
246    #[test]
247    fn streaming_chain_returns_self() {
248        let key = [0x01u8; 32];
249        let mut m = Blake3Mac::new(&key);
250        m.update(b"chain").update(b"-friendly");
251        let one_shot = blake3_keyed(&key, b"chain-friendly");
252        assert_eq!(m.finalize(), one_shot);
253    }
254
255    #[test]
256    fn streaming_verify_accepts_correct_tag() {
257        let key = [0x01u8; 32];
258        let tag = blake3_keyed(&key, b"msg");
259        let mut m = Blake3Mac::new(&key);
260        m.update(b"msg");
261        assert!(m.verify(&tag));
262    }
263
264    #[test]
265    fn streaming_verify_rejects_wrong_tag() {
266        let key = [0x01u8; 32];
267        let tag = blake3_keyed(&key, b"msg");
268        let mut tampered = tag;
269        tampered[0] ^= 0xff;
270        let mut m = Blake3Mac::new(&key);
271        m.update(b"msg");
272        assert!(!m.verify(&tampered));
273    }
274}