Skip to main content

osdp/secure/
mac.rs

1//! CBC-MAC with rolling ICV and the S-MAC1/S-MAC2 swap on the final block.
2//!
3//! # Spec: Annex D.5
4//!
5//! ```text
6//! tmp = ICV
7//! for each 16-byte block of padded input except the last:
8//!     tmp = AES_S-MAC1(tmp XOR block)
9//! tmp = AES_S-MAC2(tmp XOR last_block)
10//! MAC = tmp        // first 4 bytes are sent on the wire
11//! ```
12
13use crate::secure::crypto::aes128_encrypt;
14use crate::secure::pad::pad_mac;
15use alloc::vec::Vec;
16
17/// XOR `dst` with `src` in place.
18#[inline]
19fn xor_in_place(dst: &mut [u8; 16], src: &[u8]) {
20    for (d, s) in dst.iter_mut().zip(src.iter()) {
21        *d ^= *s;
22    }
23}
24
25/// Compute the OSDP CBC-MAC.
26///
27/// `icv` is the previous MAC from the *other* direction (R-MAC ↔ C-MAC roll
28/// per Annex D). For the very first MAC after handshake, the ICV is the
29/// initial R-MAC computed by [`super::crypto::initial_rmac`].
30///
31/// Returns the full 16-byte MAC; only the first 4 are transmitted.
32pub fn cbc_mac(input: &[u8], icv: &[u8; 16], s_mac1: &[u8; 16], s_mac2: &[u8; 16]) -> [u8; 16] {
33    let mut padded = Vec::with_capacity(input.len() + 16);
34    padded.extend_from_slice(input);
35    pad_mac(&mut padded);
36    debug_assert_eq!(padded.len() % 16, 0);
37    debug_assert!(!padded.is_empty());
38
39    let mut state = *icv;
40    let blocks = padded.len() / 16;
41    for (i, chunk) in padded.chunks_exact(16).enumerate() {
42        xor_in_place(&mut state, chunk);
43        let key = if i + 1 == blocks { s_mac2 } else { s_mac1 };
44        state = aes128_encrypt(key, &state);
45    }
46    state
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    /// MAC is deterministic.
54    #[test]
55    fn deterministic() {
56        let icv = [0u8; 16];
57        let s1 = [1u8; 16];
58        let s2 = [2u8; 16];
59        let m1 = cbc_mac(&[1, 2, 3], &icv, &s1, &s2);
60        let m2 = cbc_mac(&[1, 2, 3], &icv, &s1, &s2);
61        assert_eq!(m1, m2);
62    }
63
64    /// Different inputs produce different MACs.
65    #[test]
66    fn distinct_inputs_distinct_macs() {
67        let icv = [0u8; 16];
68        let s1 = [1u8; 16];
69        let s2 = [2u8; 16];
70        let m1 = cbc_mac(&[1], &icv, &s1, &s2);
71        let m2 = cbc_mac(&[2], &icv, &s1, &s2);
72        assert_ne!(m1, m2);
73    }
74
75    /// ICV change shifts the MAC.
76    #[test]
77    fn icv_matters() {
78        let s1 = [1u8; 16];
79        let s2 = [2u8; 16];
80        let m1 = cbc_mac(&[7, 8, 9], &[0u8; 16], &s1, &s2);
81        let m2 = cbc_mac(&[7, 8, 9], &[1u8; 16], &s1, &s2);
82        assert_ne!(m1, m2);
83    }
84
85    /// Multi-block input uses S-MAC1 then S-MAC2.
86    #[test]
87    fn key_swap_observable() {
88        let icv = [0u8; 16];
89        let s1 = [1u8; 16];
90        let s2_a = [2u8; 16];
91        let s2_b = [3u8; 16];
92        let input = [0u8; 32];
93        let m1 = cbc_mac(&input, &icv, &s1, &s2_a);
94        let m2 = cbc_mac(&input, &icv, &s1, &s2_b);
95        assert_ne!(m1, m2);
96    }
97}