use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use std::time::{Duration, Instant};
use tracing::{debug, instrument, warn};
#[derive(Debug, Clone)]
struct CacheEntry {
added_at: Instant,
timestamp: u64,
#[allow(dead_code)]
nonce: [u8; 16],
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
peer_id: String,
nonce: [u8; 16],
}
#[derive(Debug)]
pub struct ReplayCache {
entries: HashMap<CacheKey, CacheEntry>,
insertion_order: VecDeque<CacheKey>,
ttl: Duration,
max_entries: usize,
}
impl ReplayCache {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
insertion_order: VecDeque::new(),
ttl: Duration::from_secs(300),
max_entries: 10_000,
}
}
pub fn with_settings(ttl: Duration, max_entries: usize) -> Self {
Self {
entries: HashMap::new(),
insertion_order: VecDeque::new(),
ttl,
max_entries,
}
}
#[instrument(skip(self, peer_id, nonce))]
pub fn is_replay(&mut self, peer_id: &str, nonce: &[u8; 16], timestamp: u64) -> bool {
let key = CacheKey {
peer_id: peer_id.to_string(),
nonce: *nonce,
};
self.cleanup_expired();
if let Some(entry) = self.entries.get(&key) {
if entry.timestamp == timestamp {
warn!(
peer_id,
?nonce,
timestamp,
"Replay attack detected - identical nonce and timestamp"
);
return true;
}
debug!(
peer_id,
?nonce,
"Nonce seen before with different timestamp - allowing"
);
}
let entry = CacheEntry {
added_at: Instant::now(),
timestamp,
nonce: *nonce,
};
if self.entries.len() >= self.max_entries {
let to_remove = self.entries.len() - self.max_entries + 1;
self.remove_oldest_entries(to_remove);
}
self.entries.insert(key.clone(), entry);
self.insertion_order.push_back(key);
debug!(peer_id, ?nonce, timestamp, "New nonce/timestamp cached");
false
}
fn cleanup_expired(&mut self) {
let now = Instant::now();
let initial_count = self.entries.len();
self.entries
.retain(|_, entry| now.duration_since(entry.added_at) < self.ttl);
while let Some(key) = self.insertion_order.front() {
if !self.entries.contains_key(key) {
self.insertion_order.pop_front();
} else {
break;
}
}
let removed = initial_count - self.entries.len();
if removed > 0 {
debug!("Cleaned up {} expired replay cache entries", removed);
}
}
#[inline]
fn remove_oldest_entries(&mut self, count: usize) {
if count == 0 {
return;
}
for _ in 0..count {
if let Some(key) = self.insertion_order.pop_front() {
self.entries.remove(&key);
}
}
debug!(
"Removed {} oldest replay cache entries due to size limit",
count
);
}
pub fn stats(&self) -> CacheStats {
CacheStats {
entries: self.entries.len(),
max_entries: self.max_entries,
ttl_seconds: self.ttl.as_secs(),
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.insertion_order.clear();
debug!("Replay cache cleared");
}
}
impl Default for ReplayCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entries: usize,
pub max_entries: usize,
pub ttl_seconds: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_replay_detection() {
let mut cache = ReplayCache::with_settings(Duration::from_secs(60), 100);
let peer_id = "test_peer";
let nonce = [1u8; 16];
let timestamp = 1234567890;
assert!(!cache.is_replay(peer_id, &nonce, timestamp));
assert!(cache.is_replay(peer_id, &nonce, timestamp));
}
#[test]
fn test_different_nonce_allowed() {
let mut cache = ReplayCache::with_settings(Duration::from_secs(60), 100);
let peer_id = "test_peer";
let nonce1 = [1u8; 16];
let nonce2 = [2u8; 16];
let timestamp = 1234567890;
assert!(!cache.is_replay(peer_id, &nonce1, timestamp));
assert!(!cache.is_replay(peer_id, &nonce2, timestamp));
}
#[test]
fn test_same_nonce_different_timestamp_allowed() {
let mut cache = ReplayCache::with_settings(Duration::from_secs(60), 100);
let peer_id = "test_peer";
let nonce = [1u8; 16];
let timestamp1 = 1234567890;
let timestamp2 = 1234567891;
assert!(!cache.is_replay(peer_id, &nonce, timestamp1));
assert!(!cache.is_replay(peer_id, &nonce, timestamp2));
}
#[test]
fn test_expiration() {
let mut cache = ReplayCache::with_settings(Duration::from_millis(10), 100);
let peer_id = "test_peer";
let nonce = [1u8; 16];
let timestamp = 1234567890;
assert!(!cache.is_replay(peer_id, &nonce, timestamp));
thread::sleep(Duration::from_millis(20));
assert!(!cache.is_replay(peer_id, &nonce, timestamp));
}
#[test]
fn test_max_entries_limit() {
let mut cache = ReplayCache::with_settings(Duration::from_secs(60), 5);
for i in 0..10 {
let nonce = [i as u8; 16];
assert!(!cache.is_replay("peer", &nonce, 1000 + i as u64));
}
assert!(cache.entries.len() <= 5);
}
}