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}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum ReplayVerdict {
51 Accept,
53 Replay,
55 OutOfWindow,
57 Stale,
59}
60
61impl Default for ReplayWindow {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl ReplayWindow {
68 pub fn new() -> Self {
70 Self {
71 last_accepted: 0,
72 last_accepted_time_ms: 0,
73 backward_bitmap: 0,
74 recent_mics: Deque::new(),
75 }
76 }
77
78 pub fn check(&self, counter: u32, mic: &[u8], now_ms: u64) -> ReplayVerdict {
80 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
81 return ReplayVerdict::Accept;
82 }
83
84 if counter > self.last_accepted {
85 return ReplayVerdict::Accept;
86 }
87
88 if now_ms.saturating_sub(self.last_accepted_time_ms) > REPLAY_STALE_MS {
89 return ReplayVerdict::Stale;
90 }
91
92 let delta = self.last_accepted - counter;
93 if delta > REPLAY_BACKTRACK_SLOTS {
94 return ReplayVerdict::OutOfWindow;
95 }
96
97 let slot_occupied = if delta == 0 {
98 true
99 } else {
100 self.backward_bitmap & (1u8 << (delta - 1)) != 0
101 };
102
103 if !slot_occupied {
104 return ReplayVerdict::Accept;
105 }
106
107 let _ = self.has_matching_recent_mic(counter, mic, now_ms);
108 ReplayVerdict::Replay
109 }
110
111 pub fn is_acknowledgeable_duplicate(&self, counter: u32, mic: &[u8], now_ms: u64) -> bool {
118 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
119 return false;
120 }
121
122 let ack_distance = self.last_accepted.wrapping_sub(counter);
123 ack_distance <= REPLAY_BACKTRACK_SLOTS && self.has_matching_recent_mic(counter, mic, now_ms)
124 }
125
126 pub fn accept(&mut self, counter: u32, mic: &[u8], now_ms: u64) {
128 self.prune_recent_mics(now_ms);
129
130 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
131 self.last_accepted = counter;
132 self.last_accepted_time_ms = now_ms;
133 } else if counter > self.last_accepted {
134 let shift = (counter - self.last_accepted) as usize;
135 self.backward_bitmap = if shift > REPLAY_BACKTRACK_SLOTS as usize {
136 0
137 } else {
138 let shifted = if shift >= u8::BITS as usize {
139 0
140 } else {
141 self.backward_bitmap << shift
142 };
143 shifted | (1u8 << (shift - 1))
144 };
145 self.last_accepted = counter;
146 self.last_accepted_time_ms = now_ms;
147 } else if counter < self.last_accepted {
148 let delta = self.last_accepted - counter;
149 if (1..=REPLAY_BACKTRACK_SLOTS).contains(&delta) {
150 self.backward_bitmap |= 1u8 << (delta - 1);
151 }
152 } else {
153 self.last_accepted_time_ms = now_ms;
154 }
155
156 if let Some((normalized_mic, mic_len)) = normalize_mic(mic) {
157 if self.recent_mics.is_full() {
158 let _ = self.recent_mics.pop_front();
159 }
160 let _ = self.recent_mics.push_back(RecentMic {
161 counter,
162 mic: normalized_mic,
163 mic_len,
164 accepted_ms: now_ms,
165 });
166 }
167 }
168
169 pub fn reset(&mut self, baseline: u32, now_ms: u64) {
171 self.last_accepted = baseline;
172 self.last_accepted_time_ms = now_ms;
173 self.backward_bitmap = 0;
174 self.recent_mics.clear();
175 }
176
177 fn has_matching_recent_mic(&self, counter: u32, mic: &[u8], now_ms: u64) -> bool {
178 let Some((normalized_mic, mic_len)) = normalize_mic(mic) else {
179 return false;
180 };
181
182 self.recent_mics.iter().any(|entry| {
183 entry.counter == counter
184 && now_ms.saturating_sub(entry.accepted_ms) <= REPLAY_STALE_MS
185 && entry.mic_len == mic_len
186 && entry.mic[..mic_len as usize] == normalized_mic[..mic_len as usize]
187 })
188 }
189
190 fn prune_recent_mics(&mut self, now_ms: u64) {
191 while let Some(front) = self.recent_mics.front() {
192 if now_ms.saturating_sub(front.accepted_ms) <= REPLAY_STALE_MS {
193 break;
194 }
195 let _ = self.recent_mics.pop_front();
196 }
197 }
198}
199
200fn normalize_mic(mic: &[u8]) -> Option<([u8; 16], u8)> {
201 if mic.len() > 16 {
202 return None;
203 }
204 let mut out = [0u8; 16];
205 out[..mic.len()].copy_from_slice(mic);
206 Some((out, mic.len() as u8))
207}