libdelve/
challenge.rs

1//! Challenge generation and validation
2
3use crate::Result;
4use base64::engine::general_purpose::STANDARD as BASE64;
5use base64::Engine;
6use chrono::{DateTime, Duration, Utc};
7use rand::RngCore;
8
9/// Minimum challenge size in bytes (spec requires at least 32 bytes)
10pub const MIN_CHALLENGE_BYTES: usize = 32;
11
12/// Generate a cryptographically secure challenge
13///
14/// The challenge includes a timestamp and random bytes to prevent replay attacks.
15///
16/// Format: `base64(timestamp || random_bytes(32))`
17///
18/// # Arguments
19///
20/// * `expires_in` - Duration until the challenge expires (recommended: 15-60 minutes)
21///
22/// # Returns
23///
24/// A tuple of (challenge_string, expiration_time)
25pub fn generate_challenge(expires_in: Duration) -> Result<(String, DateTime<Utc>)> {
26    let now = Utc::now();
27    let expires_at = now + expires_in;
28
29    // Create challenge: timestamp (RFC3339) + random bytes
30    let timestamp = now.to_rfc3339();
31    let mut random_bytes = vec![0u8; MIN_CHALLENGE_BYTES];
32    rand::thread_rng().fill_bytes(&mut random_bytes);
33
34    // Combine timestamp and random data
35    let mut challenge_data = timestamp.as_bytes().to_vec();
36    challenge_data.push(b'|');
37    challenge_data.extend_from_slice(&random_bytes);
38
39    // Base64 encode
40    let challenge = BASE64.encode(&challenge_data);
41
42    Ok((challenge, expires_at))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn test_challenges_are_unique() {
51        let (challenge1, _) = generate_challenge(Duration::minutes(30)).unwrap();
52        let (challenge2, _) = generate_challenge(Duration::minutes(30)).unwrap();
53
54        // Each challenge should be unique due to random component
55        assert_ne!(challenge1, challenge2);
56    }
57}