Skip to main content

vitaminc_hmac/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::{any::Any, borrow::Cow, convert::Infallible};
5
6use hmac::{
7    digest::{
8        common::{Key, KeySizeUser},
9        FixedOutput, KeyInit, Output, OutputSizeUser, Update,
10    },
11    Hmac,
12};
13use sha2::Sha256;
14use vitaminc_protected::{Acceptable, Controlled, DefaultScope, Protected, ProtectedDigest};
15use zeroize::{ZeroizeOnDrop, Zeroizing};
16
17use vitaminc_prf::{
18    Context, MapPrf, Prf, PrfBuildError, PrfEncoding, PrfError, PrfKeyInit, PrfValue, PrfVisitor,
19    ReadyPrf, ResolvedPrf, ResolvedVisitor, SeqPrf,
20};
21
22type PassthroughValue = Box<dyn Any + Send + 'static>;
23
24const SHA256_BLOCK_SIZE: usize = 64;
25const SHA256_OUTPUT_SIZE: usize = 32;
26const IPAD: u8 = 0x36;
27const OPAD: u8 = 0x5C;
28
29/// Length of the key accepted by [`PrfKeyInit::new`] on [`HmacSha256Prf`].
30pub const KEY_LEN: usize = 32;
31
32/// Shortest key accepted by [`PrfKeyInit::try_from_bytes`] on [`HmacSha256Prf`].
33pub const MIN_KEY_LEN: usize = KEY_LEN;
34
35/// Key material offered to [`PrfKeyInit::try_from_bytes`] was too short to
36/// key the PRF safely.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct WeakKeyError {
39    len: usize,
40}
41
42impl WeakKeyError {
43    /// Length of the rejected key, in bytes.
44    pub fn len(&self) -> usize {
45        self.len
46    }
47
48    /// Whether the rejected key was empty.
49    pub fn is_empty(&self) -> bool {
50        self.len == 0
51    }
52}
53
54impl std::fmt::Display for WeakKeyError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(
57            f,
58            "HMAC-SHA256 PRF key must be at least {MIN_KEY_LEN} bytes, got {}",
59            self.len
60        )
61    }
62}
63
64impl std::error::Error for WeakKeyError {}
65
66/// HMAC-SHA256 keyed without leaving unwiped copies of key material.
67///
68/// RustCrypto's `Hmac` state wipes on drop when the `hmac` and `sha2`
69/// `zeroize` features are enabled, but its constructor normalizes the key
70/// through temporaries that are never wiped: `get_der_key`'s padded block and
71/// the ipad/opad buffer in `new_from_slice`. This type performs the same
72/// RFC 2104 keying with every key-derived buffer held in a [`Zeroizing`]
73/// allocation; the two digest states wipe themselves on drop via `sha2`'s
74/// `zeroize` feature.
75struct ZeroizingHmacSha256 {
76    /// Inner hash, initialized with `key ^ ipad`.
77    digest: Sha256,
78    /// Outer hash, initialized with `key ^ opad`.
79    opad_digest: Sha256,
80}
81
82impl KeySizeUser for ZeroizingHmacSha256 {
83    type KeySize = <Hmac<Sha256> as KeySizeUser>::KeySize;
84}
85
86impl KeyInit for ZeroizingHmacSha256 {
87    fn new(key: &Key<Self>) -> Self {
88        Self::new_from_slice(key.as_slice()).expect("HMAC-SHA256 accepts keys of any length")
89    }
90
91    fn new_from_slice(key: &[u8]) -> Result<Self, hmac::digest::InvalidLength> {
92        let mut block = Zeroizing::new([0_u8; SHA256_BLOCK_SIZE]);
93        if key.len() <= SHA256_BLOCK_SIZE {
94            block[..key.len()].copy_from_slice(key);
95        } else {
96            let mut hashed = Zeroizing::new([0_u8; SHA256_OUTPUT_SIZE]);
97            let mut hasher = Sha256::default();
98            Update::update(&mut hasher, key);
99            FixedOutput::finalize_into(hasher, (&mut *hashed).into());
100            block[..hashed.len()].copy_from_slice(hashed.as_slice());
101        }
102
103        block.iter_mut().for_each(|byte| *byte ^= IPAD);
104        let mut digest = Sha256::default();
105        Update::update(&mut digest, block.as_slice());
106
107        block.iter_mut().for_each(|byte| *byte ^= IPAD ^ OPAD);
108        let mut opad_digest = Sha256::default();
109        Update::update(&mut opad_digest, block.as_slice());
110
111        Ok(Self {
112            digest,
113            opad_digest,
114        })
115    }
116}
117
118impl OutputSizeUser for ZeroizingHmacSha256 {
119    type OutputSize = <Hmac<Sha256> as OutputSizeUser>::OutputSize;
120}
121
122impl Update for ZeroizingHmacSha256 {
123    fn update(&mut self, data: &[u8]) {
124        Update::update(&mut self.digest, data);
125    }
126}
127
128impl FixedOutput for ZeroizingHmacSha256 {
129    fn finalize_into(self, out: &mut Output<Self>) {
130        let Self {
131            digest,
132            mut opad_digest,
133        } = self;
134        // The inner hash permits forgeries if disclosed, so it is buffered in
135        // a wiped allocation.
136        let mut inner = Zeroizing::new([0_u8; SHA256_OUTPUT_SIZE]);
137        FixedOutput::finalize_into(digest, (&mut *inner).into());
138        Update::update(&mut opad_digest, inner.as_slice());
139        FixedOutput::finalize_into(opad_digest, out);
140    }
141}
142
143impl ZeroizeOnDrop for ZeroizingHmacSha256 {}
144
145/// Local HMAC-SHA256 structured PRF.
146///
147/// The PRF owns its key outright. Construction goes through [`PrfKeyInit`]
148/// and takes the key by value; the key lives in a single [`Protected`]
149/// allocation for the life of the PRF and is wiped when the PRF drops,
150/// unconditionally. Derivation borrows the PRF (`&self`), so one instance
151/// serves any number of derivations.
152///
153/// Each leaf derives `HMAC-SHA256(key, PAE(encoding, context, input))`.
154///
155/// # Sharing
156///
157/// `HmacSha256Prf` is deliberately not `Clone`. A clone would either copy the
158/// key or, worse, share it behind a hidden `Arc`, which turns "wiped when
159/// this PRF drops" into "wiped when the last clone drops" with nothing at
160/// the call site to say so. Sharing is still supported; it is just spelled
161/// out by the caller. Wrap the PRF in an [`Arc`](std::sync::Arc) where it
162/// needs to be shared, and the type at every call site then says exactly
163/// when the key is wiped: when the last `Arc` goes away.
164///
165/// ```
166/// use std::sync::Arc;
167/// use vitaminc_hmac::HmacSha256Prf;
168/// use vitaminc_prf::{PrfKeyInit, PrfValue};
169/// use vitaminc_protected::Protected;
170///
171/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
172/// let prf = Arc::new(HmacSha256Prf::new(Protected::new([7; 32])));
173///
174/// // Hand a handle to each place that derives; `Arc<T>` clones without
175/// // `T: Clone`, and `&*handle` is the `&HmacSha256Prf` that derivation
176/// // borrows.
177/// let for_worker = Arc::clone(&prf);
178/// let handle = std::thread::spawn(move || {
179///     "alice@example.com".prf_with_context(&*for_worker, "users/email/v1")
180/// });
181///
182/// let here = "alice@example.com"
183///     .prf_with_context(&*prf, "users/email/v1")
184///     .into_result()?;
185/// let there = handle.join().expect("worker panicked").into_result()?;
186/// assert_eq!(here, there);
187///
188/// // Dropping the last handle wipes the key.
189/// drop(prf);
190/// # Ok(())
191/// # }
192/// # example().unwrap();
193/// ```
194// Derived, not hand-written: the derive only compiles while every field
195// wipes on drop, so caching key-derived state (say, expanded ipad/opad
196// bytes) in a new field without zeroizing it is a compile error, not a
197// silent leak. `ZeroizingHmacSha256` covers the expanded state each
198// derivation builds from the key.
199#[derive(ZeroizeOnDrop)]
200pub struct HmacSha256Prf {
201    key: Protected<Vec<u8>>,
202}
203
204impl PrfKeyInit for HmacSha256Prf {
205    type Key = Protected<[u8; KEY_LEN]>;
206    type KeyError = WeakKeyError;
207
208    /// Key the PRF with a full-strength key.
209    ///
210    /// The array type carries the length guarantee, so this cannot fail.
211    fn new(key: Self::Key) -> Self {
212        // `risky_ref` + drop, not `map`: `map` moves the array out through
213        // `risky_unwrap`, and a bare `[u8; 32]` has no destructor to wipe it.
214        // Borrowing leaves the array inside its `Protected`, which wipes it
215        // when `key` drops at the end of this call.
216        Self::from_vec(Protected::new(key.risky_ref().to_vec()))
217    }
218
219    /// Key the PRF with key material whose length is only known at runtime,
220    /// such as a KMS response or an environment variable.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`WeakKeyError`] if the key is shorter than [`MIN_KEY_LEN`].
225    /// HMAC itself accepts any key length, including an empty one, which
226    /// would silently produce derivations that anybody can recompute.
227    fn try_from_bytes(key: Protected<Vec<u8>>) -> Result<Self, Self::KeyError> {
228        let len = key.risky_ref().len();
229        if len < MIN_KEY_LEN {
230            return Err(WeakKeyError { len });
231        }
232        Ok(Self::from_vec(key))
233    }
234}
235
236impl HmacSha256Prf {
237    fn from_vec(key: Protected<Vec<u8>>) -> Self {
238        Self { key }
239    }
240
241    fn derive<T>(&self, data: &T, encoding: PrfEncoding, context: &Context<'_>) -> [u8; 32]
242    where
243        T: Controlled + Acceptable<DefaultScope>,
244        T::Inner: AsRef<[u8]>,
245    {
246        let mut hmac: ProtectedDigest<ZeroizingHmacSha256> =
247            ProtectedDigest::new_with_key(&self.key)
248                .expect("HMAC-SHA256 accepts keys of any length");
249
250        // Stream PAE directly into the digest. Building a framed Vec here would
251        // create an ordinary, unwiped copy of the protected input.
252        hmac.update_public(&3_u64.to_le_bytes());
253        hmac.update_public(&(encoding.as_bytes().len() as u64).to_le_bytes());
254        hmac.update_public(encoding.as_bytes());
255        hmac.update_public(&(context.as_bytes().len() as u64).to_le_bytes());
256        hmac.update_public(context.as_bytes());
257        hmac.update_public(&(data.risky_ref().as_ref().len() as u64).to_le_bytes());
258        hmac.update(data);
259
260        let mut block = [0_u8; 32];
261        hmac.finalize_public_into(&mut block);
262        block
263    }
264
265    fn resolved<T: Send + 'static>(
266        result: Result<T, PrfError<Infallible>>,
267    ) -> ReadyPrf<T, Infallible> {
268        ReadyPrf::new(result)
269    }
270}
271
272impl Prf for HmacSha256Prf {
273    type Block = [u8; 32];
274    type BackendError = Infallible;
275    type Passthrough = PassthroughValue;
276    type SeqPrf<'a> = HmacSeqPrf<'a>;
277    type MapPrf<'a> = HmacMapPrf<'a>;
278    type Ok<T>
279        = ReadyPrf<T, Infallible>
280    where
281        T: Send + 'static;
282
283    fn prf_bytes_vec<V>(
284        &self,
285        data: Protected<Vec<u8>>,
286        encoding: PrfEncoding,
287        context: Context<'static>,
288        visitor: V,
289    ) -> Self::Ok<V::Value>
290    where
291        V: PrfVisitor<Self::Block, Self::Passthrough>,
292    {
293        let block = self.derive(&data, encoding, &context);
294        Self::resolved(visitor.visit_block(block).map_err(PrfError::Visitor))
295    }
296
297    // Overrides the Vec-copying default: fixed-size leaves stream into the
298    // digest without an intermediate heap allocation.
299    fn prf_bytes_array<const N: usize, V>(
300        &self,
301        data: Protected<[u8; N]>,
302        encoding: PrfEncoding,
303        context: Context<'static>,
304        visitor: V,
305    ) -> Self::Ok<V::Value>
306    where
307        V: PrfVisitor<Self::Block, Self::Passthrough>,
308    {
309        let block = self.derive(&data, encoding, &context);
310        Self::resolved(visitor.visit_block(block).map_err(PrfError::Visitor))
311    }
312
313    fn prf_seq(&self, size_hint: Option<usize>) -> Self::SeqPrf<'_> {
314        HmacSeqPrf {
315            backend: self,
316            values: Vec::with_capacity(size_hint.unwrap_or(0)),
317            error: None,
318        }
319    }
320
321    fn prf_map(&self, size_hint: Option<usize>) -> Self::MapPrf<'_> {
322        HmacMapPrf {
323            backend: self,
324            entries: Vec::with_capacity(size_hint.unwrap_or(0)),
325            pending_key: None,
326            error: None,
327        }
328    }
329
330    fn prf_none<V>(&self, _context: Context<'static>, visitor: V) -> Self::Ok<V::Value>
331    where
332        V: PrfVisitor<Self::Block, Self::Passthrough>,
333    {
334        Self::resolved(visitor.visit_absent().map_err(PrfError::Visitor))
335    }
336
337    fn passthrough<V>(&self, value: Self::Passthrough, visitor: V) -> Self::Ok<V::Value>
338    where
339        V: PrfVisitor<Self::Block, Self::Passthrough>,
340    {
341        Self::resolved(visitor.visit_passthrough(value).map_err(PrfError::Visitor))
342    }
343
344    fn passthrough_boxed<V>(
345        &self,
346        value: Box<dyn Any + Send + 'static>,
347        visitor: V,
348    ) -> Self::Ok<V::Value>
349    where
350        V: PrfVisitor<Self::Block, Self::Passthrough>,
351    {
352        self.passthrough(value, visitor)
353    }
354
355    fn failure<T>(&self, error: PrfError<Self::BackendError>) -> Self::Ok<T>
356    where
357        T: Send + 'static,
358    {
359        Self::resolved(Err(error))
360    }
361}
362
363/// Sequence driver borrowing its [`HmacSha256Prf`] for one derivation.
364pub struct HmacSeqPrf<'a> {
365    backend: &'a HmacSha256Prf,
366    values: Vec<ResolvedPrf<[u8; 32], PassthroughValue>>,
367    error: Option<PrfError<Infallible>>,
368}
369
370impl SeqPrf for HmacSeqPrf<'_> {
371    type Prf = HmacSha256Prf;
372    type Block = [u8; 32];
373    type BackendError = Infallible;
374    type Passthrough = PassthroughValue;
375
376    fn prf_next<T>(mut self, value: T, context: Context<'static>) -> Self
377    where
378        T: PrfValue,
379    {
380        if self.error.is_none() {
381            match value
382                .prf_visit_with_context(self.backend, context, ResolvedVisitor)
383                .into_result()
384            {
385                Ok(value) => self.values.push(value),
386                Err(error) => self.error = Some(error),
387            }
388        }
389        self
390    }
391
392    fn passthrough_next(mut self, value: Self::Passthrough) -> Self {
393        if self.error.is_none() {
394            self.values.push(ResolvedPrf::Passthrough(value));
395        }
396        self
397    }
398
399    fn passthrough_next_boxed(self, value: Box<dyn Any + Send + 'static>) -> Self {
400        self.passthrough_next(value)
401    }
402
403    fn end<V>(self, visitor: V) -> <Self::Prf as Prf>::Ok<V::Value>
404    where
405        V: PrfVisitor<Self::Block, Self::Passthrough>,
406    {
407        if let Some(error) = self.error {
408            return self.backend.failure(error);
409        }
410        HmacSha256Prf::resolved(
411            ResolvedPrf::Sequence(self.values)
412                .visit(visitor)
413                .map_err(PrfError::Visitor),
414        )
415    }
416}
417
418/// Map driver borrowing its [`HmacSha256Prf`] for one derivation.
419pub struct HmacMapPrf<'a> {
420    backend: &'a HmacSha256Prf,
421    entries: Vec<(String, ResolvedPrf<[u8; 32], PassthroughValue>)>,
422    pending_key: Option<String>,
423    error: Option<PrfError<Infallible>>,
424}
425
426impl HmacMapPrf<'_> {
427    fn set_build_error(&mut self, error: PrfBuildError) {
428        if self.error.is_none() {
429            self.error = Some(PrfError::Build(error));
430        }
431    }
432
433    fn is_duplicate_key(&self, key: &str) -> bool {
434        self.entries.iter().any(|(existing, _)| existing == key)
435    }
436}
437
438impl MapPrf for HmacMapPrf<'_> {
439    type Prf = HmacSha256Prf;
440    type Block = [u8; 32];
441    type BackendError = Infallible;
442    type Passthrough = PassthroughValue;
443
444    fn prf_key<K>(mut self, key: K) -> Self
445    where
446        K: Into<Cow<'static, str>>,
447    {
448        if self.pending_key.is_some() {
449            self.set_build_error(PrfBuildError::KeyWithoutValue);
450        } else if self.error.is_none() {
451            self.pending_key = Some(key.into().into_owned());
452        }
453        self
454    }
455
456    fn prf_value<T>(mut self, value: T, context: Context<'static>) -> Self
457    where
458        T: PrfValue,
459    {
460        let Some(key) = self.pending_key.take() else {
461            self.set_build_error(PrfBuildError::ValueWithoutKey);
462            return self;
463        };
464        if self.error.is_some() {
465            return self;
466        }
467        if self.is_duplicate_key(&key) {
468            self.set_build_error(PrfBuildError::DuplicateKey);
469            return self;
470        }
471        let entry_context = context.for_map_entry(&key);
472        match value
473            .prf_visit_with_context(self.backend, entry_context, ResolvedVisitor)
474            .into_result()
475        {
476            Ok(value) => self.entries.push((key, value)),
477            Err(error) => self.error = Some(error),
478        }
479        self
480    }
481
482    fn passthrough_entry<K>(mut self, key: K, value: Self::Passthrough) -> Self
483    where
484        K: Into<Cow<'static, str>>,
485    {
486        if self.pending_key.is_some() {
487            self.set_build_error(PrfBuildError::KeyWithoutValue);
488        } else if self.error.is_none() {
489            let key = key.into().into_owned();
490            if self.is_duplicate_key(&key) {
491                self.set_build_error(PrfBuildError::DuplicateKey);
492            } else {
493                self.entries.push((key, ResolvedPrf::Passthrough(value)));
494            }
495        }
496        self
497    }
498
499    fn passthrough_entry_boxed<K>(self, key: K, value: Box<dyn Any + Send + 'static>) -> Self
500    where
501        K: Into<Cow<'static, str>>,
502    {
503        self.passthrough_entry(key, value)
504    }
505
506    fn end<V>(mut self, visitor: V) -> <Self::Prf as Prf>::Ok<V::Value>
507    where
508        V: PrfVisitor<Self::Block, Self::Passthrough>,
509    {
510        if self.pending_key.is_some() {
511            self.set_build_error(PrfBuildError::DanglingKey);
512        }
513        if let Some(error) = self.error {
514            return self.backend.failure(error);
515        }
516        HmacSha256Prf::resolved(
517            ResolvedPrf::Map(self.entries)
518                .visit(visitor)
519                .map_err(PrfError::Visitor),
520        )
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::ZeroizingHmacSha256;
527    use hmac::{
528        digest::{FixedOutput, KeyInit, Update},
529        Hmac, Mac,
530    };
531    use quickcheck_macros::quickcheck;
532    use sha2::Sha256;
533    use vitaminc_protected::ProtectedDigest;
534    use zeroize::ZeroizeOnDrop;
535
536    #[test]
537    fn hmac_sha256_state_zeroizes_on_drop() {
538        fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
539        assert_zeroize_on_drop::<ProtectedDigest<ZeroizingHmacSha256>>();
540        // The marker impl on ZeroizingHmacSha256 is honest only while its
541        // digest states wipe themselves.
542        assert_zeroize_on_drop::<Sha256>();
543    }
544
545    fn zeroizing_hmac(key: &[u8], data: &[u8]) -> [u8; 32] {
546        let mut hmac = ZeroizingHmacSha256::new_from_slice(key).unwrap();
547        Update::update(&mut hmac, data);
548        let mut out = [0_u8; 32];
549        FixedOutput::finalize_into(hmac, (&mut out).into());
550        out
551    }
552
553    #[quickcheck]
554    fn matches_rustcrypto_hmac(key: Vec<u8>, data: Vec<u8>) -> bool {
555        let reference = {
556            let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(&key).unwrap();
557            Mac::update(&mut mac, &data);
558            mac.finalize().into_bytes()
559        };
560        zeroizing_hmac(&key, &data).as_slice() == reference.as_slice()
561    }
562
563    #[test]
564    fn matches_rustcrypto_hmac_at_key_normalization_boundaries() {
565        // Exercises both branches of key normalization deterministically:
566        // block-sized-or-smaller keys are padded, larger keys are hashed.
567        for key_len in [0, 1, 63, 64, 65, 131] {
568            let key = vec![0xaa_u8; key_len];
569            let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(&key).unwrap();
570            Mac::update(&mut mac, b"boundary");
571            assert_eq!(
572                zeroizing_hmac(&key, b"boundary").as_slice(),
573                mac.finalize().into_bytes().as_slice(),
574                "key length {key_len}"
575            );
576        }
577    }
578}