Skip to main content

fips_core/noise/
replay.rs

1use super::REPLAY_WINDOW_SIZE;
2use std::fmt;
3
4/// Sliding window for replay protection.
5///
6/// Tracks which packet counters have been received within a window of
7/// REPLAY_WINDOW_SIZE. Packets with counters below the window or already
8/// seen within the window are rejected.
9///
10/// Based on WireGuard's anti-replay mechanism (RFC 6479 style).
11#[derive(Clone)]
12pub struct ReplayWindow {
13    /// Highest counter value seen.
14    highest: u64,
15    /// Ring bitmap tracking which counters in the current window have been seen.
16    /// Bit `counter % REPLAY_WINDOW_SIZE` belongs to that absolute counter while
17    /// it is within `[highest + 1 - REPLAY_WINDOW_SIZE, highest]`.
18    bitmap: [u64; REPLAY_WINDOW_SIZE / 64],
19}
20
21impl ReplayWindow {
22    /// Create a new replay window.
23    pub fn new() -> Self {
24        Self {
25            highest: 0,
26            bitmap: [0; REPLAY_WINDOW_SIZE / 64],
27        }
28    }
29
30    /// Check if a counter is valid (not replayed, not too old).
31    ///
32    /// Returns true if the counter is acceptable, false if it should be rejected.
33    /// Does NOT update the window - call `accept` after successful decryption.
34    pub fn check(&self, counter: u64) -> bool {
35        if counter > self.highest {
36            // New highest - always acceptable
37            return true;
38        }
39
40        // Counter is <= highest, check if it's within the window
41        let diff = self.highest - counter;
42        if diff as usize >= REPLAY_WINDOW_SIZE {
43            // Too old (outside window)
44            return false;
45        }
46
47        let (word_idx, mask) = Self::bit_position(counter);
48        (self.bitmap[word_idx] & mask) == 0
49    }
50
51    /// Accept a counter into the window.
52    ///
53    /// Call this only after successful decryption to prevent
54    /// DoS attacks that exhaust the window.
55    pub fn accept(&mut self, counter: u64) {
56        if counter > self.highest {
57            // Advance the logical window. The common in-order packet path only
58            // clears one recycled bit instead of shifting the whole bitmap.
59            let shift = counter - self.highest;
60            if shift as usize >= REPLAY_WINDOW_SIZE {
61                self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
62            } else {
63                self.clear_counter_range(self.highest + 1, counter);
64            }
65            self.highest = counter;
66        }
67
68        let (word_idx, mask) = Self::bit_position(counter);
69        self.bitmap[word_idx] |= mask;
70    }
71
72    fn clear_counter_range(&mut self, start: u64, end: u64) {
73        debug_assert!(start <= end);
74        for counter in start..=end {
75            let (word_idx, mask) = Self::bit_position(counter);
76            self.bitmap[word_idx] &= !mask;
77        }
78    }
79
80    #[inline]
81    fn bit_position(counter: u64) -> (usize, u64) {
82        let bit = (counter as usize) % REPLAY_WINDOW_SIZE;
83        (bit / 64, 1u64 << (bit % 64))
84    }
85
86    /// Get the highest counter seen.
87    pub fn highest(&self) -> u64 {
88        self.highest
89    }
90
91    /// Reset the window (use when rekeying).
92    pub fn reset(&mut self) {
93        self.highest = 0;
94        self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
95    }
96}
97
98impl Default for ReplayWindow {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl fmt::Debug for ReplayWindow {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.debug_struct("ReplayWindow")
107            .field("highest", &self.highest)
108            .field("window_size", &REPLAY_WINDOW_SIZE)
109            .finish()
110    }
111}