1use hmac::{Hmac, KeyInit, Mac};
2use sha2::Sha256;
3use std::collections::BTreeMap;
4use url::form_urlencoded;
5
6use crate::error::InitDataError;
7
8pub 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 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}