1use crate::Result;
4use base64::engine::general_purpose::STANDARD as BASE64;
5use base64::Engine;
6use chrono::{DateTime, Duration, Utc};
7use rand::RngCore;
8
9pub const MIN_CHALLENGE_BYTES: usize = 32;
11
12pub fn generate_challenge(expires_in: Duration) -> Result<(String, DateTime<Utc>)> {
26 let now = Utc::now();
27 let expires_at = now + expires_in;
28
29 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 let mut challenge_data = timestamp.as_bytes().to_vec();
36 challenge_data.push(b'|');
37 challenge_data.extend_from_slice(&random_bytes);
38
39 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 assert_ne!(challenge1, challenge2);
56 }
57}