init_data_rs/
sign.rs

1use hmac::{Hmac, KeyInit, Mac};
2use sha2::Sha256;
3use std::collections::BTreeMap;
4use url::form_urlencoded;
5
6use crate::error::InitDataError;
7
8/// Sign creates hash for init data using bot token.
9///
10/// # Errors
11///
12/// See `init_data_rs::parse` for possible errors
13pub fn sign(init_data: &str, token: &str) -> Result<String, InitDataError> {
14    if init_data.is_empty() {
15        return Err(InitDataError::UnexpectedFormat("init_data is empty".to_string()));
16    }
17
18    if token.is_empty() {
19        return Err(InitDataError::UnexpectedFormat("token is empty".to_string()));
20    }
21
22    let pairs = form_urlencoded::parse(init_data.as_bytes());
23    let mut params: BTreeMap<String, String> = BTreeMap::new();
24
25    for (key, value) in pairs {
26        if key != "hash" {
27            params.insert(key.to_string(), value.into_owned());
28        }
29    }
30
31    let data_check_string = params
32        .iter()
33        .map(|(k, v)| format!("{k}={v}"))
34        .collect::<Vec<_>>()
35        .join("\n");
36
37    // More : https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
38
39    let mut hmac: Hmac<Sha256> = hmac::Hmac::new_from_slice("WebAppData".as_bytes())
40        .map_err(|error| InitDataError::Internal(error.to_string()))?;
41
42    hmac.update(token.as_bytes());
43
44    let secret_key = hmac.finalize();
45
46    let mut hmac: Hmac<Sha256> = hmac::Hmac::new_from_slice(secret_key.as_bytes())
47        .map_err(|error| InitDataError::Internal(error.to_string()))?;
48
49    hmac.update(data_check_string.as_bytes());
50
51    Ok(hex::encode(hmac.finalize().as_bytes()))
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    const BOT_TOKEN: &str = "12345:YOUR_BOT_TOKEN";
59
60    #[test]
61    fn test_sign_empty_data() {
62        let result = sign("", BOT_TOKEN);
63        assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
64    }
65
66    #[test]
67    fn test_sign_empty_token() {
68        let base_data = "query_id=test&auth_date=123";
69        let result = sign(base_data, "");
70        assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
71    }
72
73    #[test]
74    fn test_sign_valid_data() {
75        let init_data = "query_id=AAHdF6IQAAAAAN0XohDhrOrc\
76            &user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%22%2C\
77            %22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C\
78            %22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%7D\
79            &auth_date=1662771648";
80
81        let result = sign(init_data, BOT_TOKEN).unwrap();
82        assert!(!result.is_empty());
83        assert_eq!(result.len(), 64);
84        assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
85    }
86
87    #[test]
88    fn test_sign_with_existing_hash() {
89        let init_data = "query_id=AAHdF6IQAAAAAN0XohDhrOrc\
90            &user=%7B%22id%22%3A279058397%7D\
91            &auth_date=1662771648\
92            &hash=existing_hash";
93
94        let result = sign(init_data, BOT_TOKEN).unwrap();
95        assert!(!result.is_empty());
96        assert_eq!(result.len(), 64);
97        assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
98    }
99
100    #[test]
101    fn test_sign_consistency() {
102        let init_data = "auth_date=1662771648&query_id=test123";
103
104        let hash1 = sign(init_data, BOT_TOKEN).unwrap();
105        let hash2 = sign(init_data, BOT_TOKEN).unwrap();
106
107        assert_eq!(hash1, hash2);
108    }
109
110    #[test]
111    fn test_sign_different_tokens() {
112        let init_data = "auth_date=1662771648&query_id=test123";
113
114        let hash1 = sign(init_data, "token1").unwrap();
115        let hash2 = sign(init_data, "token2").unwrap();
116
117        assert_ne!(hash1, hash2);
118    }
119
120    #[test]
121    fn test_sign_parameter_order() {
122        let init_data1 = "auth_date=1662771648&query_id=test123";
123        let init_data2 = "query_id=test123&auth_date=1662771648";
124
125        let hash1 = sign(init_data1, BOT_TOKEN).unwrap();
126        let hash2 = sign(init_data2, BOT_TOKEN).unwrap();
127
128        assert_eq!(hash1, hash2);
129    }
130}