use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::SecurityError;
const DEFAULT_EXPIRATION_SECS: u64 = 300;
const DEFAULT_MAX_NONCES: usize = 10000;
pub struct NonceStore {
nonces: HashMap<u64, u64>,
max_nonces: usize,
expiration_secs: u64,
}
impl NonceStore {
pub fn new() -> Self {
Self {
nonces: HashMap::new(),
max_nonces: DEFAULT_MAX_NONCES,
expiration_secs: DEFAULT_EXPIRATION_SECS,
}
}
pub fn with_config(max_nonces: usize, expiration_secs: u64) -> Self {
Self {
nonces: HashMap::with_capacity(max_nonces),
max_nonces,
expiration_secs,
}
}
pub fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
}
pub fn is_expired(&self, timestamp: u64) -> bool {
let now = Self::current_timestamp();
if timestamp > now {
return timestamp > now + 60;
}
now - timestamp > self.expiration_secs
}
pub fn check_nonce(&mut self, nonce: u64, timestamp: u64) -> Result<(), SecurityError> {
if self.is_expired(timestamp) {
return Err(SecurityError::ExpiredTimestamp);
}
if self.nonces.contains_key(&nonce) {
return Err(SecurityError::ReplayAttack);
}
if self.nonces.len() >= self.max_nonces {
self.cleanup_expired();
if self.nonces.len() >= self.max_nonces {
return Err(SecurityError::CapacityExceeded);
}
}
self.nonces.insert(nonce, timestamp);
Ok(())
}
pub fn cleanup_expired(&mut self) {
let now = Self::current_timestamp();
self.nonces
.retain(|_, &mut ts| now.saturating_sub(ts) <= self.expiration_secs);
}
pub fn len(&self) -> usize {
self.nonces.len()
}
pub fn is_empty(&self) -> bool {
self.nonces.is_empty()
}
pub fn clear(&mut self) {
self.nonces.clear();
}
}
impl Default for NonceStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nonce_store_basic() {
let mut store = NonceStore::new();
let now = NonceStore::current_timestamp();
assert!(store.check_nonce(12345, now).is_ok());
assert_eq!(store.len(), 1);
assert_eq!(
store.check_nonce(12345, now),
Err(SecurityError::ReplayAttack)
);
assert!(store.check_nonce(67890, now).is_ok());
assert_eq!(store.len(), 2);
}
#[test]
fn test_expired_timestamp() {
let mut store = NonceStore::with_config(100, 60); let now = NonceStore::current_timestamp();
assert!(store.check_nonce(1, now).is_ok());
let old = now.saturating_sub(120); assert_eq!(
store.check_nonce(2, old),
Err(SecurityError::ExpiredTimestamp)
);
}
#[test]
fn test_cleanup_expired() {
let mut store = NonceStore::with_config(100, 1); let now = NonceStore::current_timestamp();
store.nonces.insert(1, now);
store.nonces.insert(2, now.saturating_sub(10)); store.nonces.insert(3, now.saturating_sub(10));
store.cleanup_expired();
assert_eq!(store.len(), 1);
assert!(store.nonces.contains_key(&1));
}
#[test]
fn test_future_timestamp() {
let mut store = NonceStore::new();
let now = NonceStore::current_timestamp();
assert!(store.check_nonce(1, now + 30).is_ok());
assert_eq!(
store.check_nonce(2, now + 120),
Err(SecurityError::ExpiredTimestamp)
);
}
#[test]
fn test_clear() {
let mut store = NonceStore::new();
let now = NonceStore::current_timestamp();
store.check_nonce(1, now).unwrap();
store.check_nonce(2, now).unwrap();
assert_eq!(store.len(), 2);
store.clear();
assert!(store.is_empty());
assert!(store.check_nonce(1, now).is_ok());
}
}