1#![forbid(unsafe_code)]
2#![forbid(clippy::exit)]
3#![deny(clippy::pattern_type_mismatch)]
4#![warn(
5 clippy::future_not_send,
6 clippy::exhaustive_enums,
7 clippy::exhaustive_structs,
8 clippy::must_use_unit,
9 clippy::missing_inline_in_public_items,
10 clippy::must_use_candidate
11)]
12
13use hmac::{Hmac, Mac};
14use serde::Deserialize;
15use sha2::Sha256;
16use url::Url;
17
18use std::{
19 collections::HashMap,
20 time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH},
21};
22
23type HmacSha256 = Hmac<Sha256>;
24
25#[derive(Debug, PartialEq, Deserialize)]
28pub struct InitData {
29 pub auth_date: u64,
32
33 pub can_send_after: Option<u64>,
35
36 pub chat: Option<Chat>,
39
40 pub chat_type: Option<String>,
43
44 pub chat_instance: Option<u64>,
47
48 pub hash: String,
50
51 pub query_id: Option<String>,
54
55 pub receiver: Option<User>,
58
59 pub start_param: Option<String>,
62
63 pub user: Option<User>,
65}
66
67#[derive(Debug, PartialEq, Deserialize)]
70pub struct User {
71 pub added_to_attachment_menu: Option<bool>,
73
74 pub allows_write_to_pm: Option<bool>,
76
77 pub is_premium: Option<bool>,
79
80 pub first_name: String,
82
83 pub id: i64,
85
86 pub is_bot: Option<bool>,
88
89 pub last_name: Option<String>,
91
92 pub language_code: Option<String>,
94
95 pub photo_url: Option<String>,
98
99 pub username: Option<String>,
101}
102
103#[derive(Debug, PartialEq, Deserialize)]
106pub struct Chat {
107 pub id: i64,
109
110 pub r#type: String,
112
113 pub title: String,
115
116 pub photo_url: Option<String>,
119
120 pub username: Option<String>,
122}
123
124#[derive(Debug)]
125pub enum ParseDataError {
126 InvalidSignature(serde_json::Error),
127 InvalidQueryString(url::ParseError),
128}
129
130pub fn parse<T: AsRef<str>>(init_data: T) -> Result<InitData, ParseDataError> {
132 let url = Url::parse(&format!("http://dummy.com?{}", init_data.as_ref()))
134 .map_err(ParseDataError::InvalidQueryString)?;
135
136 static STRING_PROPS: phf::Set<&'static str> = phf::phf_set! {
138 "start_param",
139 };
140
141 let mut pairs = Vec::new();
143 for (key, value) in url.query_pairs() {
144 let val = value.to_string();
145
146 let formatted_pair = if STRING_PROPS.contains(key.as_ref()) {
148 format!("\"{}\":\"{}\"", key, val)
150 } else {
151 if serde_json::from_str::<serde_json::Value>(&val).is_ok() {
153 format!("\"{}\":{}", key, val)
155 } else {
156 format!("\"{}\":\"{}\"", key, val)
158 }
159 };
160
161 pairs.push(formatted_pair);
162 }
163
164 let json_str = format!("{{{}}}", pairs.join(","));
166
167 serde_json::from_str(&json_str).map_err(ParseDataError::InvalidSignature)
169}
170
171#[derive(Debug)]
172pub enum SignError {
173 CouldNotProcessSignature,
174 CouldNotProcessAuthTime(SystemTimeError),
175 InvalidQueryString(url::ParseError),
176}
177
178pub fn sign<T: AsRef<str>>(
181 payload: HashMap<String, String>,
182 bot_token: T,
183 auth_time: SystemTime,
184) -> Result<String, SignError> {
185 let mut pairs = payload
186 .iter()
187 .filter_map(|(k, v)| {
188 if k == "hash" || k == "auth_date" {
190 None
191 } else {
192 Some(format!("{}={}", k, v))
193 }
194 })
195 .collect::<Vec<String>>();
196
197 let auth_date = auth_time
198 .duration_since(UNIX_EPOCH)
199 .map_err(SignError::CouldNotProcessAuthTime)?
200 .as_secs();
201 pairs.push(format!("auth_date={}", auth_date));
203
204 pairs.sort();
206
207 let payload = pairs.join("\n");
208
209 let mut sk_hmac = HmacSha256::new_from_slice("WebAppData".as_bytes())
211 .map_err(|_| SignError::CouldNotProcessSignature)?;
212 sk_hmac.update(bot_token.as_ref().as_bytes());
213 let secret_key = sk_hmac.finalize().into_bytes();
214
215 let mut imp_hmac =
217 HmacSha256::new_from_slice(&secret_key).map_err(|_| SignError::CouldNotProcessSignature)?;
218 imp_hmac.update(payload.as_bytes());
219
220 let result = imp_hmac.finalize().into_bytes();
222
223 Ok(hex::encode(result))
224}
225
226pub fn sign_query_string<T: AsRef<str>>(
227 qs: T,
228 bot_token: T,
229 auth_time: SystemTime,
230) -> Result<String, SignError> {
231 let url = Url::parse(&format!("http://dummy.com?{}", qs.as_ref()))
232 .map_err(SignError::InvalidQueryString)?;
233
234 let mut params: HashMap<String, String> = HashMap::new();
235 for (key, value) in url.query_pairs() {
236 params.insert(key.to_string(), value.to_string());
237 }
238
239 sign(params, bot_token, auth_time)
240}
241
242#[derive(Debug)]
243pub enum ValidationError {
244 InvalidQueryString(url::ParseError),
245 UnexpectedFormat,
246 SignMissing,
247 AuthDateMissing,
248 Expired,
249 SignInvalid,
250}
251
252pub fn validate<T: AsRef<str>>(
267 init_data: T,
268 bot_token: T,
269 exp_in: Duration,
270) -> Result<bool, ValidationError> {
271 let url = Url::parse(&format!("http://dummy.com?{}", init_data.as_ref()))
273 .map_err(ValidationError::InvalidQueryString)?;
274
275 let mut auth_date: Option<SystemTime> = None;
276 let mut hash: Option<String> = None;
277 let mut pairs = Vec::new();
278
279 for (key, value) in url.query_pairs() {
281 if key == "hash" {
283 hash = Some(value.to_string());
284 continue;
285 }
286 if key == "auth_date" {
287 if let Ok(timestamp) = value.parse::<u64>() {
288 auth_date = Some(UNIX_EPOCH + Duration::from_secs(timestamp));
289 }
290 }
291 pairs.push(format!("{}={}", key, value));
293 }
294
295 let hash = hash.ok_or(ValidationError::SignMissing)?;
297
298 if exp_in != Duration::from_secs(0) {
300 let auth_date = auth_date.ok_or(ValidationError::AuthDateMissing)?;
302
303 if auth_date + exp_in < SystemTime::now() {
305 return Err(ValidationError::Expired);
306 }
307 }
308
309 pairs.sort();
311
312 let calculated_hash = sign_query_string(init_data, bot_token, auth_date.unwrap_or(UNIX_EPOCH))
314 .map_err(|_| ValidationError::UnexpectedFormat)?;
315
316 if calculated_hash != hash {
318 return Err(ValidationError::SignInvalid);
319 }
320
321 Ok(true)
322}
323
324#[cfg(test)]
325mod tests {
326 use std::time::Duration;
327
328 use super::*;
329
330 #[test]
331 fn test_parse_valid_data() {
332 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&start_param=abc";
333 let result = parse(init_data);
334 assert!(result.is_ok());
335 let data = result.unwrap();
336 assert_eq!(
337 data,
338 InitData {
339 auth_date: 1662771648,
340 can_send_after: None,
341 chat: None,
342 chat_type: None,
343 chat_instance: None,
344 hash: "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2"
345 .to_string(),
346 query_id: Some("AAHdF6IQAAAAAN0XohDhrOrc".to_string()),
347 receiver: None,
348 start_param: Some("abc".to_string()),
349 user: Some(User {
350 added_to_attachment_menu: None,
351 allows_write_to_pm: None,
352 is_premium: Some(true),
353 first_name: "Vladislav".to_string(),
354 id: 279058397,
355 is_bot: None,
356 last_name: Some("Kibenko".to_string()),
357 language_code: Some("ru".to_string()),
358 photo_url: None,
359 username: Some("vdkfrost".to_string())
360 })
361 }
362 );
363 }
364
365 #[test]
366 fn test_parse_invalid_data() {
367 let init_data = "invalid data";
368 let result = parse(init_data);
369 assert!(result.is_err());
370 }
371
372 #[test]
373 fn test_sign_query_string() {
374 let qs = "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";
375 let test_bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
376 let test_sign_hash =
377 "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2".to_string();
378 let auth_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1662771648);
379
380 let result = sign_query_string(qs, test_bot_token, auth_time).unwrap();
381
382 assert_eq!(result, test_sign_hash);
383 }
384
385 #[test]
386 fn test_sign_query_string_no_date() {
387 let qs = "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";
388 let test_bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
389 let test_sign_hash =
390 "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2".to_string();
391 let auth_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1662771648);
392
393 let result = sign_query_string(qs, test_bot_token, auth_time).unwrap();
394
395 assert_eq!(result, test_sign_hash);
396 }
397
398 #[test]
399 fn test_validate_success() {
400 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";
401 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
402 let exp_in = Duration::from_secs(1662771648);
403
404 assert!(matches!(validate(init_data, token, exp_in), Ok(true)));
405 }
406
407 #[test]
408 fn test_validate_expired() {
409 let init_data =
410 "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";
411 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
412 let exp_in = Duration::from_secs(86400);
413
414 assert!(matches!(
415 validate(init_data, token, exp_in),
416 Err(ValidationError::Expired)
417 ));
418 }
419
420 #[test]
421 fn test_validate_missing_hash() {
422 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";
423 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
424 let exp_in = Duration::from_secs(86400);
425
426 assert!(matches!(
427 validate(init_data, token, exp_in),
428 Err(ValidationError::SignMissing)
429 ));
430 }
431}