Skip to main content

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}