umsh_mac/
cache.rs

1use heapless::Deque;
2
3use crate::{RECENT_MIC_CAPACITY, REPLAY_BACKTRACK_SLOTS, REPLAY_STALE_MS};
4
5/// Duplicate-suppression key derived from an accepted packet.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum DupCacheKey {
8    /// Authenticated routable packet keyed by its MIC bytes.
9    Mic {
10        bytes: [u8; 16],
11        len: u8,
12        route_retry: bool,
13    },
14    /// MIC-less routable packet keyed by a stable local hash over non-dynamic
15    /// fields.
16    Hash32(u32),
17}
18
19/// Fixed-capacity cache of recently observed duplicate keys.
20#[derive(Clone, Debug)]
21pub struct DuplicateCache<const N: usize = 64> {
22    entries: Deque<(DupCacheKey, u64), N>,
23}
24
25impl<const N: usize> Default for DuplicateCache<N> {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl<const N: usize> DuplicateCache<N> {
32    /// Create an empty duplicate cache.
33    pub fn new() -> Self {
34        Self {
35            entries: Deque::new(),
36        }
37    }
38
39    /// Return whether `key` is already present.
40    pub fn contains(&self, key: &DupCacheKey) -> bool {
41        self.entries.iter().any(|(entry, _)| entry == key)
42    }
43
44    /// Insert `key`, evicting the oldest entry if necessary.
45    pub fn insert(&mut self, key: DupCacheKey, now_ms: u64) {
46        if self.contains(&key) {
47            return;
48        }
49        if self.entries.is_full() {
50            let _ = self.entries.pop_front();
51        }
52        let _ = self.entries.push_back((key, now_ms));
53    }
54
55    /// Return the number of tracked entries.
56    pub fn len(&self) -> usize {
57        self.entries.len()
58    }
59
60    /// Return whether the cache is empty.
61    pub fn is_empty(&self) -> bool {
62        self.entries.is_empty()
63    }
64}
65
66/// Recently accepted MIC tracked for backward-window replay handling.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct RecentMic {
69    /// Accepted frame counter.
70    pub counter: u32,
71    /// Normalized MIC bytes.
72    pub mic: [u8; 16],
73    /// Number of valid bytes in [`mic`](Self::mic).
74    pub mic_len: u8,
75    /// Acceptance timestamp in milliseconds.
76    pub accepted_ms: u64,
77}
78
79/// Replay-detection window for secure traffic from one sender.
80#[derive(Clone, Debug)]
81pub struct ReplayWindow {
82    /// Highest accepted frame counter.
83    pub last_accepted: u32,
84    /// Timestamp of the highest accepted frame.
85    pub last_accepted_time_ms: u64,
86    /// Occupancy bitmap for the backward counter window.
87    pub backward_bitmap: u8,
88    /// Accepted MICs retained for duplicate late-arrival checks.
89    pub recent_mics: Deque<RecentMic, RECENT_MIC_CAPACITY>,
90}
91
92/// Result of checking a packet against a replay window.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum ReplayVerdict {
95    /// The packet is acceptable.
96    Accept,
97    /// The exact counter/MIC pair was already accepted.
98    Replay,
99    /// The counter is too far behind the tracked window.
100    OutOfWindow,
101    /// The replay state is too stale to safely accept backward-window traffic.
102    Stale,
103}
104
105impl Default for ReplayWindow {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl ReplayWindow {
112    /// Create a fresh replay window.
113    pub fn new() -> Self {
114        Self {
115            last_accepted: 0,
116            last_accepted_time_ms: 0,
117            backward_bitmap: 0,
118            recent_mics: Deque::new(),
119        }
120    }
121
122    /// Evaluate whether `counter` and `mic` are acceptable at `now_ms`.
123    pub fn check(&self, counter: u32, mic: &[u8], now_ms: u64) -> ReplayVerdict {
124        if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
125            return ReplayVerdict::Accept;
126        }
127
128        if counter > self.last_accepted {
129            return ReplayVerdict::Accept;
130        }
131
132        if now_ms.saturating_sub(self.last_accepted_time_ms) > REPLAY_STALE_MS {
133            return ReplayVerdict::Stale;
134        }
135
136        let delta = self.last_accepted - counter;
137        if delta > REPLAY_BACKTRACK_SLOTS {
138            return ReplayVerdict::OutOfWindow;
139        }
140
141        let slot_occupied = if delta == 0 {
142            true
143        } else {
144            self.backward_bitmap & (1u8 << (delta - 1)) != 0
145        };
146
147        if !slot_occupied {
148            return ReplayVerdict::Accept;
149        }
150
151        let _ = self.has_matching_recent_mic(counter, mic, now_ms);
152        ReplayVerdict::Replay
153    }
154
155    /// Record an accepted `counter` and `mic` at `now_ms`.
156    pub fn accept(&mut self, counter: u32, mic: &[u8], now_ms: u64) {
157        self.prune_recent_mics(now_ms);
158
159        if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
160            self.last_accepted = counter;
161            self.last_accepted_time_ms = now_ms;
162        } else if counter > self.last_accepted {
163            let shift = (counter - self.last_accepted) as usize;
164            self.backward_bitmap = if shift > REPLAY_BACKTRACK_SLOTS as usize {
165                0
166            } else {
167                let shifted = if shift >= u8::BITS as usize {
168                    0
169                } else {
170                    self.backward_bitmap << shift
171                };
172                shifted | (1u8 << (shift - 1))
173            };
174            self.last_accepted = counter;
175            self.last_accepted_time_ms = now_ms;
176        } else if counter < self.last_accepted {
177            let delta = self.last_accepted - counter;
178            if (1..=REPLAY_BACKTRACK_SLOTS).contains(&delta) {
179                self.backward_bitmap |= 1u8 << (delta - 1);
180            }
181        } else {
182            self.last_accepted_time_ms = now_ms;
183        }
184
185        if let Some((normalized_mic, mic_len)) = normalize_mic(mic) {
186            if self.recent_mics.is_full() {
187                let _ = self.recent_mics.pop_front();
188            }
189            let _ = self.recent_mics.push_back(RecentMic {
190                counter,
191                mic: normalized_mic,
192                mic_len,
193                accepted_ms: now_ms,
194            });
195        }
196    }
197
198    /// Reset the replay window to a known baseline.
199    pub fn reset(&mut self, baseline: u32, now_ms: u64) {
200        self.last_accepted = baseline;
201        self.last_accepted_time_ms = now_ms;
202        self.backward_bitmap = 0;
203        self.recent_mics.clear();
204    }
205
206    fn has_matching_recent_mic(&self, counter: u32, mic: &[u8], now_ms: u64) -> bool {
207        let Some((normalized_mic, mic_len)) = normalize_mic(mic) else {
208            return false;
209        };
210
211        self.recent_mics.iter().any(|entry| {
212            entry.counter == counter
213                && now_ms.saturating_sub(entry.accepted_ms) <= REPLAY_STALE_MS
214                && entry.mic_len == mic_len
215                && entry.mic[..mic_len as usize] == normalized_mic[..mic_len as usize]
216        })
217    }
218
219    fn prune_recent_mics(&mut self, now_ms: u64) {
220        while let Some(front) = self.recent_mics.front() {
221            if now_ms.saturating_sub(front.accepted_ms) <= REPLAY_STALE_MS {
222                break;
223            }
224            let _ = self.recent_mics.pop_front();
225        }
226    }
227}
228
229fn normalize_mic(mic: &[u8]) -> Option<([u8; 16], u8)> {
230    if mic.len() > 16 {
231        return None;
232    }
233    let mut out = [0u8; 16];
234    out[..mic.len()].copy_from_slice(mic);
235    Some((out, mic.len() as u8))
236}