fips_core/noise/replay.rs
1use super::REPLAY_WINDOW_SIZE;
2use std::fmt;
3
4/// Reason a counter is rejected by the replay window.
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum ReplayRejection {
7 /// Counter is inside the replay window but its bit was already observed.
8 Duplicate,
9 /// Counter is behind the retained replay window.
10 TooOld,
11}
12
13/// Sliding window for replay protection.
14///
15/// Tracks which packet counters have been received within a window of
16/// REPLAY_WINDOW_SIZE. Packets with counters below the window or already
17/// seen within the window are rejected.
18///
19/// Based on WireGuard's anti-replay mechanism (RFC 6479 style).
20#[derive(Clone)]
21pub struct ReplayWindow {
22 /// Highest counter value seen.
23 highest: u64,
24 /// Bitmap tracking which counters in the window have been seen.
25 /// Bit i corresponds to counter (highest - i).
26 bitmap: [u64; REPLAY_WINDOW_SIZE / 64],
27}
28
29impl ReplayWindow {
30 /// Create a new replay window.
31 pub fn new() -> Self {
32 Self {
33 highest: 0,
34 bitmap: [0; REPLAY_WINDOW_SIZE / 64],
35 }
36 }
37
38 /// Check if a counter is valid (not replayed, not too old).
39 ///
40 /// Returns true if the counter is acceptable, false if it should be rejected.
41 /// Does NOT update the window - call `accept` after successful decryption.
42 pub fn check(&self, counter: u64) -> bool {
43 self.rejection_reason(counter).is_none()
44 }
45
46 /// Classify why a counter would be rejected, without updating the window.
47 pub fn rejection_reason(&self, counter: u64) -> Option<ReplayRejection> {
48 if counter > self.highest {
49 // New highest - always acceptable
50 return None;
51 }
52
53 // Counter is <= highest, check if it's within the window
54 let diff = self.highest - counter;
55 if diff as usize >= REPLAY_WINDOW_SIZE {
56 // Too old (outside window)
57 return Some(ReplayRejection::TooOld);
58 }
59
60 // Check bitmap - bit is set if counter was already seen
61 let word_idx = (diff as usize) / 64;
62 let bit_idx = (diff as usize) % 64;
63 if (self.bitmap[word_idx] & (1u64 << bit_idx)) == 0 {
64 None
65 } else {
66 Some(ReplayRejection::Duplicate)
67 }
68 }
69
70 /// Accept a counter into the window.
71 ///
72 /// Call this only after successful decryption to prevent
73 /// DoS attacks that exhaust the window.
74 pub fn accept(&mut self, counter: u64) {
75 if counter > self.highest {
76 // Shift the window
77 let shift = counter - self.highest;
78 if shift as usize >= REPLAY_WINDOW_SIZE {
79 // Complete reset
80 self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
81 } else {
82 // Shift bitmap
83 self.shift_bitmap(shift as usize);
84 }
85 self.highest = counter;
86 // Mark counter 0 (which is now the highest) as seen
87 self.bitmap[0] |= 1;
88 } else {
89 // Mark the counter as seen
90 let diff = self.highest - counter;
91 let word_idx = (diff as usize) / 64;
92 let bit_idx = (diff as usize) % 64;
93 self.bitmap[word_idx] |= 1u64 << bit_idx;
94 }
95 }
96
97 /// Shift the bitmap by the given number of positions.
98 ///
99 /// This moves old counters to higher bit positions to make room for the
100 /// new highest counter at position 0.
101 fn shift_bitmap(&mut self, shift: usize) {
102 if shift >= REPLAY_WINDOW_SIZE {
103 self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
104 return;
105 }
106
107 let word_shift = shift / 64;
108 let bit_shift = shift % 64;
109
110 // Shift entire words first (from high to low to avoid overwriting)
111 if word_shift > 0 {
112 for i in (word_shift..self.bitmap.len()).rev() {
113 self.bitmap[i] = self.bitmap[i - word_shift];
114 }
115 for i in 0..word_shift {
116 self.bitmap[i] = 0;
117 }
118 }
119
120 // Shift bits within words (from low to high so carry propagates correctly)
121 if bit_shift > 0 {
122 let mut carry = 0u64;
123 for i in 0..self.bitmap.len() {
124 let new_carry = self.bitmap[i] >> (64 - bit_shift);
125 self.bitmap[i] = (self.bitmap[i] << bit_shift) | carry;
126 carry = new_carry;
127 }
128 }
129 }
130
131 /// Get the highest counter seen.
132 pub fn highest(&self) -> u64 {
133 self.highest
134 }
135
136 /// Reset the window (use when rekeying).
137 pub fn reset(&mut self) {
138 self.highest = 0;
139 self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
140 }
141}
142
143impl Default for ReplayWindow {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl fmt::Debug for ReplayWindow {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.debug_struct("ReplayWindow")
152 .field("highest", &self.highest)
153 .field("window_size", &REPLAY_WINDOW_SIZE)
154 .finish()
155 }
156}