telemetry_kit/sync/
auth.rs

1//! HMAC-SHA256 authentication for sync requests
2
3use hmac::{Hmac, Mac};
4use sha2::Sha256;
5
6type HmacSha256 = Hmac<Sha256>;
7
8/// HMAC authentication helper
9pub struct HmacAuth {
10    secret: String,
11}
12
13impl HmacAuth {
14    /// Create a new HMAC authenticator with the given secret
15    pub fn new(secret: impl Into<String>) -> Self {
16        Self {
17            secret: secret.into(),
18        }
19    }
20
21    /// Calculate HMAC signature for a message
22    ///
23    /// Message format: `{timestamp}:{nonce}:{body}`
24    ///
25    /// # Arguments
26    /// * `timestamp` - Unix timestamp in seconds
27    /// * `nonce` - Unique nonce (UUID v4)
28    /// * `body` - JSON request body
29    ///
30    /// # Returns
31    /// Hex-encoded HMAC-SHA256 signature
32    pub fn sign(&self, timestamp: &str, nonce: &str, body: &str) -> String {
33        let message = format!("{}:{}:{}", timestamp, nonce, body);
34
35        let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
36            .expect("HMAC can take key of any size");
37
38        mac.update(message.as_bytes());
39
40        let result = mac.finalize();
41        hex::encode(result.into_bytes())
42    }
43
44    /// Verify an HMAC signature
45    ///
46    /// # Arguments
47    /// * `timestamp` - Unix timestamp in seconds
48    /// * `nonce` - Unique nonce
49    /// * `body` - JSON request body
50    /// * `signature` - Signature to verify
51    ///
52    /// # Returns
53    /// `true` if signature is valid, `false` otherwise
54    pub fn verify(&self, timestamp: &str, nonce: &str, body: &str, signature: &str) -> bool {
55        let expected = self.sign(timestamp, nonce, body);
56        constant_time_eq(signature.as_bytes(), expected.as_bytes())
57    }
58}
59
60/// Constant-time comparison to prevent timing attacks
61fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
62    if a.len() != b.len() {
63        return false;
64    }
65
66    let mut diff = 0u8;
67    for (a_byte, b_byte) in a.iter().zip(b.iter()) {
68        diff |= a_byte ^ b_byte;
69    }
70
71    diff == 0
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_hmac_signing() {
80        let auth = HmacAuth::new("test_secret_key");
81        let timestamp = "1732003200";
82        let nonce = "550e8400-e29b-41d4-a716-446655440000";
83        let body = r#"{"events":[]}"#;
84
85        let signature = auth.sign(timestamp, nonce, body);
86
87        // Signature should be a 64-character hex string
88        assert_eq!(signature.len(), 64);
89        assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
90    }
91
92    #[test]
93    fn test_hmac_verification() {
94        let auth = HmacAuth::new("test_secret_key");
95        let timestamp = "1732003200";
96        let nonce = "550e8400-e29b-41d4-a716-446655440000";
97        let body = r#"{"events":[]}"#;
98
99        let signature = auth.sign(timestamp, nonce, body);
100
101        // Verify the signature
102        assert!(auth.verify(timestamp, nonce, body, &signature));
103
104        // Tampered body should fail verification
105        let tampered_body = r#"{"events":[{"tampered":true}]}"#;
106        assert!(!auth.verify(timestamp, nonce, tampered_body, &signature));
107    }
108
109    #[test]
110    fn test_signature_determinism() {
111        let auth = HmacAuth::new("test_secret_key");
112        let timestamp = "1732003200";
113        let nonce = "550e8400-e29b-41d4-a716-446655440000";
114        let body = r#"{"events":[]}"#;
115
116        let sig1 = auth.sign(timestamp, nonce, body);
117        let sig2 = auth.sign(timestamp, nonce, body);
118
119        // Same inputs should produce same signature
120        assert_eq!(sig1, sig2);
121    }
122
123    #[test]
124    fn test_constant_time_eq() {
125        assert!(constant_time_eq(b"hello", b"hello"));
126        assert!(!constant_time_eq(b"hello", b"world"));
127        assert!(!constant_time_eq(b"hello", b"hello!"));
128    }
129}