Skip to main content

dcp/security/
replay.rs

1//! Replay protection for DCP protocol.
2//!
3//! Provides nonce tracking and timestamp expiration checking
4//! to prevent replay attacks.
5
6use std::collections::HashMap;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use crate::SecurityError;
10
11/// Default expiration window (5 minutes)
12const DEFAULT_EXPIRATION_SECS: u64 = 300;
13
14/// Default maximum nonces to track
15const DEFAULT_MAX_NONCES: usize = 10000;
16
17/// Nonce store for replay protection
18pub struct NonceStore {
19    /// Seen nonces with their timestamps
20    nonces: HashMap<u64, u64>,
21    /// Maximum number of nonces to track
22    max_nonces: usize,
23    /// Expiration window in seconds
24    expiration_secs: u64,
25}
26
27impl NonceStore {
28    /// Create a new nonce store with default settings
29    pub fn new() -> Self {
30        Self {
31            nonces: HashMap::new(),
32            max_nonces: DEFAULT_MAX_NONCES,
33            expiration_secs: DEFAULT_EXPIRATION_SECS,
34        }
35    }
36
37    /// Create a nonce store with custom settings
38    pub fn with_config(max_nonces: usize, expiration_secs: u64) -> Self {
39        Self {
40            nonces: HashMap::with_capacity(max_nonces),
41            max_nonces,
42            expiration_secs,
43        }
44    }
45
46    /// Get the current timestamp in seconds since UNIX epoch
47    pub fn current_timestamp() -> u64 {
48        SystemTime::now()
49            .duration_since(UNIX_EPOCH)
50            .unwrap_or(Duration::ZERO)
51            .as_secs()
52    }
53
54    /// Check if a timestamp is expired
55    pub fn is_expired(&self, timestamp: u64) -> bool {
56        let now = Self::current_timestamp();
57        if timestamp > now {
58            // Future timestamp - allow some clock skew (60 seconds)
59            return timestamp > now + 60;
60        }
61        now - timestamp > self.expiration_secs
62    }
63
64    /// Check and record a nonce
65    /// Returns Ok(()) if the nonce is valid and not seen before
66    /// Returns Err(ReplayAttack) if the nonce was already used
67    /// Returns Err(ExpiredTimestamp) if the timestamp is too old
68    pub fn check_nonce(&mut self, nonce: u64, timestamp: u64) -> Result<(), SecurityError> {
69        // Check timestamp first
70        if self.is_expired(timestamp) {
71            return Err(SecurityError::ExpiredTimestamp);
72        }
73
74        // Check if nonce was already used
75        if self.nonces.contains_key(&nonce) {
76            return Err(SecurityError::ReplayAttack);
77        }
78
79        // Clean up old nonces if we're at capacity
80        if self.nonces.len() >= self.max_nonces {
81            self.cleanup_expired();
82            if self.nonces.len() >= self.max_nonces {
83                return Err(SecurityError::CapacityExceeded);
84            }
85        }
86
87        // Record the nonce
88        self.nonces.insert(nonce, timestamp);
89        Ok(())
90    }
91
92    /// Remove expired nonces from the store
93    pub fn cleanup_expired(&mut self) {
94        let now = Self::current_timestamp();
95        self.nonces
96            .retain(|_, &mut ts| now.saturating_sub(ts) <= self.expiration_secs);
97    }
98
99    /// Get the number of tracked nonces
100    pub fn len(&self) -> usize {
101        self.nonces.len()
102    }
103
104    /// Check if the store is empty
105    pub fn is_empty(&self) -> bool {
106        self.nonces.is_empty()
107    }
108
109    /// Clear all tracked nonces
110    pub fn clear(&mut self) {
111        self.nonces.clear();
112    }
113}
114
115impl Default for NonceStore {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_nonce_store_basic() {
127        let mut store = NonceStore::new();
128        let now = NonceStore::current_timestamp();
129
130        // First use should succeed
131        assert!(store.check_nonce(12345, now).is_ok());
132        assert_eq!(store.len(), 1);
133
134        // Reuse should fail
135        assert_eq!(
136            store.check_nonce(12345, now),
137            Err(SecurityError::ReplayAttack)
138        );
139
140        // Different nonce should succeed
141        assert!(store.check_nonce(67890, now).is_ok());
142        assert_eq!(store.len(), 2);
143    }
144
145    #[test]
146    fn test_expired_timestamp() {
147        let mut store = NonceStore::with_config(100, 60); // 60 second expiration
148        let now = NonceStore::current_timestamp();
149
150        // Current timestamp should work
151        assert!(store.check_nonce(1, now).is_ok());
152
153        // Old timestamp should fail
154        let old = now.saturating_sub(120); // 2 minutes ago
155        assert_eq!(
156            store.check_nonce(2, old),
157            Err(SecurityError::ExpiredTimestamp)
158        );
159    }
160
161    #[test]
162    fn test_cleanup_expired() {
163        let mut store = NonceStore::with_config(100, 1); // 1 second expiration
164        let now = NonceStore::current_timestamp();
165
166        // Add some nonces
167        store.nonces.insert(1, now);
168        store.nonces.insert(2, now.saturating_sub(10)); // Already expired
169        store.nonces.insert(3, now.saturating_sub(10)); // Already expired
170
171        store.cleanup_expired();
172
173        // Only the recent one should remain
174        assert_eq!(store.len(), 1);
175        assert!(store.nonces.contains_key(&1));
176    }
177
178    #[test]
179    fn test_future_timestamp() {
180        let mut store = NonceStore::new();
181        let now = NonceStore::current_timestamp();
182
183        // Slightly in the future (within skew) should work
184        assert!(store.check_nonce(1, now + 30).is_ok());
185
186        // Far in the future should fail
187        assert_eq!(
188            store.check_nonce(2, now + 120),
189            Err(SecurityError::ExpiredTimestamp)
190        );
191    }
192
193    #[test]
194    fn test_clear() {
195        let mut store = NonceStore::new();
196        let now = NonceStore::current_timestamp();
197
198        store.check_nonce(1, now).unwrap();
199        store.check_nonce(2, now).unwrap();
200        assert_eq!(store.len(), 2);
201
202        store.clear();
203        assert!(store.is_empty());
204
205        // Can reuse nonces after clear
206        assert!(store.check_nonce(1, now).is_ok());
207    }
208}