use std::collections::{HashMap, VecDeque};
use std::sync::Mutex;
use super::auth::session::now_epoch;
use super::didcomm_unpack::REPLAY_WINDOW_SECS;
use super::error::AppError;
pub const MAX_ENTRIES: usize = 50_000;
pub const MAX_ENTRIES_PER_SIGNER: usize = 5_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplayError {
Duplicate,
Full,
}
impl From<ReplayError> for AppError {
fn from(e: ReplayError) -> Self {
match e {
ReplayError::Duplicate => {
AppError::Validation("duplicate message id (replay-detected)".into())
}
ReplayError::Full => AppError::Internal(
"replay cache is full; refusing new signed requests until records expire".into(),
),
}
}
}
#[derive(Debug, Default)]
struct Inner {
seen: HashMap<(String, String), u64>,
order: VecDeque<(u64, (String, String))>,
per_signer: HashMap<String, usize>,
}
impl Inner {
fn expire(&mut self, now: u64) {
while let Some((at, _)) = self.order.front() {
if now.saturating_sub(*at) <= REPLAY_WINDOW_SECS {
break;
}
let (_, key) = self.order.pop_front().expect("front exists");
self.seen.remove(&key);
if let Some(n) = self.per_signer.get_mut(&key.0) {
*n -= 1;
if *n == 0 {
self.per_signer.remove(&key.0);
}
}
}
}
}
#[derive(Debug, Default)]
pub struct ReplayCache {
inner: Mutex<Inner>,
}
impl ReplayCache {
pub fn new() -> Self {
Self::default()
}
pub fn check(&self, signer: &str, id: &str) -> Result<(), ReplayError> {
self.check_at(signer, id, now_epoch())
}
fn check_at(&self, signer: &str, id: &str, now: u64) -> Result<(), ReplayError> {
let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
inner.expire(now);
let key = (signer.to_string(), id.to_string());
if inner.seen.contains_key(&key) {
return Err(ReplayError::Duplicate);
}
if inner.seen.len() >= MAX_ENTRIES
|| inner.per_signer.get(signer).copied().unwrap_or(0) >= MAX_ENTRIES_PER_SIGNER
{
return Err(ReplayError::Full);
}
inner.seen.insert(key.clone(), now);
inner.order.push_back((now, key));
*inner.per_signer.entry(signer.to_string()).or_default() += 1;
Ok(())
}
pub fn check_and_insert(&self, signer: &str, id: &str) -> Result<(), AppError> {
self.check(signer, id).map_err(AppError::from)
}
pub fn len(&self) -> usize {
self.inner
.lock()
.unwrap_or_else(|p| p.into_inner())
.seen
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_insert_succeeds_and_duplicate_is_refused() {
let cache = ReplayCache::new();
cache.check("did:example:a", "msg-1").unwrap();
assert_eq!(
cache.check("did:example:a", "msg-1"),
Err(ReplayError::Duplicate)
);
let err = cache
.check_and_insert("did:example:a", "msg-1")
.unwrap_err();
assert!(matches!(err, AppError::Validation(ref m) if m.contains("replay-detected")));
}
#[test]
fn distinct_signer_or_id_accepted() {
let cache = ReplayCache::new();
cache.check("did:example:a", "msg-1").unwrap();
cache.check("did:example:a", "msg-2").unwrap();
cache.check("did:example:b", "msg-1").unwrap();
assert_eq!(cache.len(), 3);
}
#[test]
fn a_record_expires_after_the_window() {
let cache = ReplayCache::new();
cache.check_at("did:example:a", "msg-1", 1_000).unwrap();
assert_eq!(
cache.check_at("did:example:a", "msg-1", 1_000 + REPLAY_WINDOW_SECS),
Err(ReplayError::Duplicate),
"still inside the window"
);
cache
.check_at("did:example:a", "msg-1", 1_001 + REPLAY_WINDOW_SECS)
.expect("expired, so accepted again");
assert_eq!(cache.len(), 1);
}
#[test]
fn a_full_signer_is_refused_and_nothing_is_forgotten() {
let cache = ReplayCache::new();
for i in 0..MAX_ENTRIES_PER_SIGNER {
cache.check_at("did:flood", &i.to_string(), 1_000).unwrap();
}
assert_eq!(
cache.check_at("did:flood", "one-more", 1_000),
Err(ReplayError::Full)
);
assert_eq!(
cache.check_at("did:flood", "0", 1_000),
Err(ReplayError::Duplicate),
"the oldest record is still remembered"
);
cache.check_at("did:other", "x", 1_000).unwrap();
}
#[test]
fn the_global_bound_refuses_new_records() {
let cache = ReplayCache::new();
for i in 0..MAX_ENTRIES {
let signer = format!("did:s{}", i % 100);
cache.check_at(&signer, &i.to_string(), 1_000).unwrap();
}
assert_eq!(
cache.check_at("did:fresh", "x", 1_000),
Err(ReplayError::Full)
);
}
}