1use heapless::Deque;
14
15pub const RECENT_MIC_CAPACITY: usize = 9;
17pub const REPLAY_BACKTRACK_SLOTS: u32 = 8;
19pub const REPLAY_STALE_MS: u64 = 5 * 60 * 1000;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct RecentMic {
25 pub counter: u32,
27 pub mic: [u8; 16],
29 pub mic_len: u8,
31 pub accepted_ms: u64,
33}
34
35#[derive(Clone, Debug)]
37pub struct ReplayWindow {
38 pub last_accepted: u32,
40 pub last_accepted_time_ms: u64,
42 pub backward_bitmap: u8,
44 pub recent_mics: Deque<RecentMic, RECENT_MIC_CAPACITY>,
46 pub last_dup_ack_counter: u32,
55 pub last_dup_ack_ms: u64,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum ReplayVerdict {
64 Accept,
66 Replay,
68 OutOfWindow,
70 Stale,
72}
73
74impl Default for ReplayWindow {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl ReplayWindow {
81 pub fn new() -> Self {
83 Self {
84 last_accepted: 0,
85 last_accepted_time_ms: 0,
86 backward_bitmap: 0,
87 recent_mics: Deque::new(),
88 last_dup_ack_counter: 0,
89 last_dup_ack_ms: 0,
90 }
91 }
92
93 pub fn check(&self, counter: u32, mic: &[u8], now_ms: u64) -> ReplayVerdict {
95 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
96 return ReplayVerdict::Accept;
97 }
98
99 if counter > self.last_accepted {
100 return ReplayVerdict::Accept;
101 }
102
103 if now_ms.saturating_sub(self.last_accepted_time_ms) > REPLAY_STALE_MS {
104 return ReplayVerdict::Stale;
105 }
106
107 let delta = self.last_accepted - counter;
108 if delta > REPLAY_BACKTRACK_SLOTS {
109 return ReplayVerdict::OutOfWindow;
110 }
111
112 let slot_occupied = if delta == 0 {
113 true
114 } else {
115 self.backward_bitmap & (1u8 << (delta - 1)) != 0
116 };
117
118 if !slot_occupied {
119 return ReplayVerdict::Accept;
120 }
121
122 let _ = self.has_matching_recent_mic(counter, mic, now_ms);
123 ReplayVerdict::Replay
124 }
125
126 pub fn note_acknowledgeable_duplicate(
152 &mut self,
153 counter: u32,
154 mic: &[u8],
155 now_ms: u64,
156 holdoff_ms: u64,
157 ) -> bool {
158 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
159 return false;
160 }
161
162 let ack_distance = self.last_accepted.wrapping_sub(counter);
163 if ack_distance > REPLAY_BACKTRACK_SLOTS {
164 return false;
165 }
166 let Some(entry) = self.find_recent_mic(counter, mic, now_ms) else {
167 return false;
168 };
169 if now_ms.saturating_sub(entry.accepted_ms) < holdoff_ms {
170 return false;
171 }
172 if self.last_dup_ack_counter == counter
173 && now_ms.saturating_sub(self.last_dup_ack_ms) < holdoff_ms
174 {
175 return false;
176 }
177 self.last_dup_ack_counter = counter;
178 self.last_dup_ack_ms = now_ms;
179 true
180 }
181
182 pub fn accept(&mut self, counter: u32, mic: &[u8], now_ms: u64) {
184 self.prune_recent_mics(now_ms);
185
186 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
187 self.last_accepted = counter;
188 self.last_accepted_time_ms = now_ms;
189 } else if counter > self.last_accepted {
190 let shift = (counter - self.last_accepted) as usize;
191 self.backward_bitmap = if shift > REPLAY_BACKTRACK_SLOTS as usize {
192 0
193 } else {
194 let shifted = if shift >= u8::BITS as usize {
195 0
196 } else {
197 self.backward_bitmap << shift
198 };
199 shifted | (1u8 << (shift - 1))
200 };
201 self.last_accepted = counter;
202 self.last_accepted_time_ms = now_ms;
203 } else if counter < self.last_accepted {
204 let delta = self.last_accepted - counter;
205 if (1..=REPLAY_BACKTRACK_SLOTS).contains(&delta) {
206 self.backward_bitmap |= 1u8 << (delta - 1);
207 }
208 } else {
209 self.last_accepted_time_ms = now_ms;
210 }
211
212 if let Some((normalized_mic, mic_len)) = normalize_mic(mic) {
213 if self.recent_mics.is_full() {
214 let _ = self.recent_mics.pop_front();
215 }
216 let _ = self.recent_mics.push_back(RecentMic {
217 counter,
218 mic: normalized_mic,
219 mic_len,
220 accepted_ms: now_ms,
221 });
222 }
223 }
224
225 pub fn reset(&mut self, baseline: u32, now_ms: u64) {
227 self.last_accepted = baseline;
228 self.last_accepted_time_ms = now_ms;
229 self.backward_bitmap = 0;
230 self.recent_mics.clear();
231 self.last_dup_ack_counter = 0;
232 self.last_dup_ack_ms = 0;
233 }
234
235 fn has_matching_recent_mic(&self, counter: u32, mic: &[u8], now_ms: u64) -> bool {
236 let Some((normalized_mic, mic_len)) = normalize_mic(mic) else {
237 return false;
238 };
239
240 self.recent_mics.iter().any(|entry| {
241 entry.counter == counter
242 && now_ms.saturating_sub(entry.accepted_ms) <= REPLAY_STALE_MS
243 && entry.mic_len == mic_len
244 && entry.mic[..mic_len as usize] == normalized_mic[..mic_len as usize]
245 })
246 }
247
248 fn find_recent_mic(&self, counter: u32, mic: &[u8], now_ms: u64) -> Option<&RecentMic> {
249 let (normalized_mic, mic_len) = normalize_mic(mic)?;
250
251 self.recent_mics.iter().find(|entry| {
252 entry.counter == counter
253 && now_ms.saturating_sub(entry.accepted_ms) <= REPLAY_STALE_MS
254 && entry.mic_len == mic_len
255 && entry.mic[..mic_len as usize] == normalized_mic[..mic_len as usize]
256 })
257 }
258
259 fn prune_recent_mics(&mut self, now_ms: u64) {
260 while let Some(front) = self.recent_mics.front() {
261 if now_ms.saturating_sub(front.accepted_ms) <= REPLAY_STALE_MS {
262 break;
263 }
264 let _ = self.recent_mics.pop_front();
265 }
266 }
267}
268
269fn normalize_mic(mic: &[u8]) -> Option<([u8; 16], u8)> {
270 if mic.len() > 16 {
271 return None;
272 }
273 let mut out = [0u8; 16];
274 out[..mic.len()].copy_from_slice(mic);
275 Some((out, mic.len() as u8))
276}