init_data_rust/
validate.rs

1use std::{
2    collections::HashMap,
3    time::{Duration, SystemTime, UNIX_EPOCH},
4};
5
6use url::Url;
7
8use crate::{errors::ValidationError, sign};
9
10/// Validates the passed init data.
11pub fn validate(
12    init_data: &str,
13    bot_token: &str,
14    exp_in: Duration,
15) -> Result<bool, ValidationError> {
16    // Parse passed init data as query string
17    let url = Url::parse(&format!("http://dummy.com?{}", init_data))?;
18
19    let mut auth_date: Option<SystemTime> = None;
20    let mut hash: Option<String> = None;
21    let mut params: HashMap<String, String> = HashMap::new();
22
23    // Iterate over all key-value pairs of parsed parameters
24    for (key, value) in url.query_pairs() {
25        let key_str = key.into_owned();
26        let value_str = value.into_owned();
27
28        // Store found sign
29        if key_str == "hash" {
30            hash = Some(value_str.clone());
31            continue;
32        }
33        if key_str == "auth_date" {
34            if let Ok(timestamp) = value_str.parse::<u64>() {
35                auth_date = Some(UNIX_EPOCH + Duration::from_secs(timestamp));
36            }
37        }
38        // Insert into params
39        params.insert(key_str, value_str);
40    }
41
42    // Sign is always required
43    let hash = hash.ok_or(ValidationError::SignMissing)?;
44
45    // In case expiration time is passed, we do additional parameters check
46    if exp_in != Duration::from_secs(0) {
47        // In case auth date is none, it means we cannot check if parameters are expired
48        let auth_date = auth_date.ok_or(ValidationError::AuthDateMissing)?;
49
50        // Check if init data is expired
51        if auth_date + exp_in < SystemTime::now() {
52            return Err(ValidationError::Expired);
53        }
54    }
55
56    // Calculate signature
57    let calculated_hash = sign(&params, bot_token, auth_date.unwrap_or(UNIX_EPOCH))
58        .map_err(|_| ValidationError::UnexpectedFormat)?;
59
60    // In case our sign is not equal to found one, we should throw an error
61    if calculated_hash != hash {
62        return Err(ValidationError::SignInvalid);
63    }
64
65    Ok(true)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::time::Duration;
72
73    #[test]
74    fn test_validate_success() {
75        let init_data = "query_id=AAHdF6IQAAAAAN0XohDhrOrc&user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%22%2C%22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C%22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%7D&auth_date=1662771648&hash=c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2";
76        let bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
77        let exp_in = Duration::from_secs(1662771648);
78
79        assert!(validate(init_data, bot_token, exp_in).is_ok());
80    }
81
82    #[test]
83    fn test_validate_expired() {
84        let init_data = "query_id=AAHdF6IQAAAAAN0XohDhrOrc&user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%22%2C%22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C%22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%7D&auth_date=1662771648&hash=c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2";
85        let bot_token = "your_bot_token";
86        let exp_in = Duration::from_secs(1);
87
88        assert!(matches!(
89            validate(init_data, bot_token, exp_in),
90            Err(ValidationError::Expired)
91        ));
92    }
93
94    #[test]
95    fn test_validate_missing_hash() {
96        let init_data = "query_id=AAHdF6IQAAAAAN0XohDhrOrc&user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%22%2C%22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C%22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%7D&auth_date=1662771648";
97        let bot_token = "your_bot_token";
98        let exp_in = Duration::from_secs(0);
99
100        assert!(matches!(
101            validate(init_data, bot_token, exp_in),
102            Err(ValidationError::SignMissing)
103        ));
104    }
105
106    #[test]
107    fn test_validate_invalid_signature() {
108        let init_data = "query_id=invalid&user=%7B%22id%22%3A12345%2C%22first_name%22%3A%22Test%22%7D&auth_date=1662771648&hash=invalidhash";
109        let bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
110        let exp_in = Duration::from_secs(0);
111
112        assert!(matches!(
113            validate(init_data, bot_token, exp_in),
114            Err(ValidationError::SignInvalid)
115        ));
116    }
117}