Skip to main content

ferrox_guards/
session_replay_guard.rs

1//! # Session Replay & Fingerprint Guard (`session_replay_guard.rs`)
2//!
3//! Binds PASETO/JWT session tokens to client TLS/HTTP User-Agent fingerprints (OWASP Session Security).
4//! Prevents stolen token replay attacks when tokens are reused across mismatched browsers or IPs.
5
6use serde::{Deserialize, Serialize};
7use sha2::{Sha256, Digest};
8
9/// Status returned by session replay verification
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum SessionStatus {
12    Valid,
13    HijackDetected { expected_fingerprint: String, actual_fingerprint: String },
14}
15
16/// Composite Client Fingerprint
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ClientFingerprint {
19    pub hash: String,
20    pub user_agent: String,
21}
22
23impl ClientFingerprint {
24    pub fn new(user_agent: &str, accept_language: &str, accept_encoding: &str) -> Self {
25        let mut hasher = Sha256::new();
26        hasher.update(user_agent.as_bytes());
27        hasher.update(b"|");
28        hasher.update(accept_language.as_bytes());
29        hasher.update(b"|");
30        hasher.update(accept_encoding.as_bytes());
31        let hash = format!("{:x}", hasher.finalize());
32
33        Self {
34            hash,
35            user_agent: user_agent.to_string(),
36        }
37    }
38}
39
40/// Session Replay Detector Engine
41pub struct SessionReplayDetector;
42
43impl SessionReplayDetector {
44    pub fn evaluate_session(
45        bound_fingerprint_hash: &str,
46        current_fingerprint: &ClientFingerprint,
47    ) -> SessionStatus {
48        if bound_fingerprint_hash == current_fingerprint.hash {
49            SessionStatus::Valid
50        } else {
51            SessionStatus::HijackDetected {
52                expected_fingerprint: bound_fingerprint_hash.to_string(),
53                actual_fingerprint: current_fingerprint.hash.clone(),
54            }
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_session_replay_detector() {
65        let fp1 = ClientFingerprint::new("Mozilla/5.0 (Windows NT 10.0)", "it-IT,it;q=0.9", "gzip, deflate");
66        let fp2 = ClientFingerprint::new("Python-urllib/3.9", "en-US", "gzip");
67
68        // Matching fingerprint -> Valid
69        let res_valid = SessionReplayDetector::evaluate_session(&fp1.hash, &fp1);
70        assert_eq!(res_valid, SessionStatus::Valid);
71
72        // Mismatched fingerprint -> HijackDetected
73        let res_hijack = SessionReplayDetector::evaluate_session(&fp1.hash, &fp2);
74        match res_hijack {
75            SessionStatus::HijackDetected { .. } => {}
76            _ => panic!("Expected HijackDetected"),
77        }
78    }
79}