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
25const HASH_FIELD: &str = "hash";
26const AUTH_DATE_FIELD: &str = "auth_date";
27
28const DUMMY_URL_BASE: &str = "http://dummy.com";
29const WEB_APP_DATA_KEY: &[u8] = b"WebAppData";
30
31#[derive(Debug, PartialEq, Eq, Deserialize, Clone)]
34pub struct InitData {
35 pub auth_date: u64,
38
39 pub can_send_after: Option<u64>,
41
42 pub chat: Option<Chat>,
45
46 pub chat_type: Option<String>,
49
50 pub chat_instance: Option<i64>,
53
54 pub hash: String,
56
57 pub query_id: Option<String>,
60
61 pub receiver: Option<User>,
64
65 pub start_param: Option<String>,
68
69 pub user: Option<User>,
71}
72
73#[derive(Debug, PartialEq, Eq, Deserialize, Clone)]
76pub struct User {
77 pub added_to_attachment_menu: Option<bool>,
79
80 pub allows_write_to_pm: Option<bool>,
82
83 pub is_premium: Option<bool>,
85
86 pub first_name: String,
88
89 pub id: i64,
91
92 pub is_bot: Option<bool>,
94
95 pub last_name: Option<String>,
97
98 pub language_code: Option<String>,
100
101 pub photo_url: Option<String>,
104
105 pub username: Option<String>,
107}
108
109#[derive(Debug, PartialEq, Eq, Deserialize, Clone)]
112pub struct Chat {
113 pub id: i64,
115
116 pub r#type: String,
118
119 pub title: String,
121
122 pub photo_url: Option<String>,
125
126 pub username: Option<String>,
128}
129
130#[derive(Debug)]
131pub enum ParseDataError {
132 InvalidSignature(serde_json::Error),
133 InvalidQueryString(url::ParseError),
134}
135
136pub fn parse<T: AsRef<str>>(init_data: T) -> Result<InitData, ParseDataError> {
138 let init_data_ref = init_data.as_ref();
140
141 let mut url_string = String::with_capacity(DUMMY_URL_BASE.len() + 1 + init_data_ref.len());
143 url_string.push_str(DUMMY_URL_BASE);
144 url_string.push('?');
145 url_string.push_str(init_data_ref);
146
147 let url = Url::parse(&url_string).map_err(ParseDataError::InvalidQueryString)?;
148
149 static STRING_PROPS: phf::Set<&'static str> = phf::phf_set! {
151 "start_param",
152 };
153
154 let query_pairs = url.query_pairs();
155 let estimated_size = query_pairs.count() * 20;
156
157 let query_pairs = url.query_pairs();
159 let mut json_str = String::with_capacity(2 + estimated_size); json_str.push('{');
161
162 let mut first = true;
163 for (key, value) in query_pairs {
164 if !first {
165 json_str.push(',');
166 }
167 first = false;
168
169 json_str.push('"');
170 json_str.push_str(&key);
171 json_str.push_str("\":");
172
173 if STRING_PROPS.contains(key.as_ref()) {
175 json_str.push('"');
177 json_str.push_str(&value);
178 json_str.push('"');
179 } else {
180 if serde_json::from_str::<serde_json::Value>(&value).is_ok() {
182 json_str.push_str(&value);
184 } else {
185 json_str.push('"');
187 json_str.push_str(&value);
188 json_str.push('"');
189 }
190 }
191 }
192
193 json_str.push('}');
194
195 serde_json::from_str(&json_str).map_err(ParseDataError::InvalidSignature)
197}
198
199#[derive(Clone, Debug)]
200pub enum SignError {
201 CouldNotProcessSignature,
202 CouldNotProcessAuthTime(SystemTimeError),
203 InvalidQueryString(url::ParseError),
204}
205
206pub fn sign<T: AsRef<str>>(
209 payload: HashMap<String, String>,
210 bot_token: T,
211 auth_time: SystemTime,
212) -> Result<String, SignError> {
213 let mut pairs = payload
214 .iter()
215 .filter_map(|(k, v)| {
216 if k == HASH_FIELD || k == AUTH_DATE_FIELD {
218 None
219 } else {
220 Some(format!("{}={}", k, v))
221 }
222 })
223 .collect::<Vec<String>>();
224
225 let auth_date = auth_time
226 .duration_since(UNIX_EPOCH)
227 .map_err(SignError::CouldNotProcessAuthTime)?
228 .as_secs();
229 pairs.push(format!("auth_date={}", auth_date));
231
232 pairs.sort();
234
235 let payload = pairs.join("\n");
236
237 let mut sk_hmac = HmacSha256::new_from_slice(WEB_APP_DATA_KEY)
239 .map_err(|_| SignError::CouldNotProcessSignature)?;
240 sk_hmac.update(bot_token.as_ref().as_bytes());
241 let secret_key = sk_hmac.finalize().into_bytes();
242
243 let mut imp_hmac =
245 HmacSha256::new_from_slice(&secret_key).map_err(|_| SignError::CouldNotProcessSignature)?;
246 imp_hmac.update(payload.as_bytes());
247
248 let result = imp_hmac.finalize().into_bytes();
250
251 Ok(hex::encode(result))
252}
253
254pub fn sign_query_string<T: AsRef<str>>(
255 qs: T,
256 bot_token: T,
257 auth_time: SystemTime,
258) -> Result<String, SignError> {
259 let url = Url::parse(&format!("{}?{}", DUMMY_URL_BASE, qs.as_ref()))
260 .map_err(SignError::InvalidQueryString)?;
261
262 let mut params: HashMap<String, String> = HashMap::new();
263 for (key, value) in url.query_pairs() {
264 params.insert(key.to_string(), value.to_string());
265 }
266
267 sign(params, bot_token, auth_time)
268}
269
270#[derive(Debug)]
271pub enum ValidationError {
272 InvalidQueryString(url::ParseError),
273 UnexpectedFormat,
274 SignMissing,
275 AuthDateMissing,
276 Expired,
277 SignInvalid,
278}
279
280pub fn validate<T: AsRef<str>>(
295 init_data: T,
296 bot_token: T,
297 exp_in: Duration,
298) -> Result<bool, ValidationError> {
299 let url = Url::parse(&format!("{}?{}", DUMMY_URL_BASE, init_data.as_ref()))
301 .map_err(ValidationError::InvalidQueryString)?;
302
303 let mut auth_date: Option<SystemTime> = None;
304 let mut hash: Option<String> = None;
305 let mut pairs = Vec::new();
306
307 for (key, value) in url.query_pairs() {
309 if key == HASH_FIELD {
311 hash = Some(value.to_string());
312 continue;
313 }
314 if key == AUTH_DATE_FIELD {
315 if let Ok(timestamp) = value.parse::<u64>() {
316 auth_date = Some(UNIX_EPOCH + Duration::from_secs(timestamp));
317 }
318 }
319 pairs.push(format!("{}={}", key, value));
321 }
322
323 let hash = hash.ok_or(ValidationError::SignMissing)?;
325
326 if exp_in != Duration::from_secs(0) {
328 let auth_date = auth_date.ok_or(ValidationError::AuthDateMissing)?;
330
331 if auth_date + exp_in < SystemTime::now() {
333 return Err(ValidationError::Expired);
334 }
335 }
336
337 pairs.sort();
339
340 let calculated_hash = sign_query_string(init_data, bot_token, auth_date.unwrap_or(UNIX_EPOCH))
342 .map_err(|_| ValidationError::UnexpectedFormat)?;
343
344 if calculated_hash != hash {
346 return Err(ValidationError::SignInvalid);
347 }
348
349 Ok(true)
350}
351
352#[cfg(test)]
353mod tests {
354 use std::time::Duration;
355
356 use super::*;
357
358 #[test]
359 fn test_parse_valid_data() {
360 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";
361 let result = parse(init_data);
362 assert!(result.is_ok());
363 let data = result.unwrap();
364 assert_eq!(
365 data,
366 InitData {
367 auth_date: 1662771648,
368 can_send_after: None,
369 chat: None,
370 chat_type: None,
371 chat_instance: None,
372 hash: "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2"
373 .to_string(),
374 query_id: Some("AAHdF6IQAAAAAN0XohDhrOrc".to_string()),
375 receiver: None,
376 start_param: Some("abc".to_string()),
377 user: Some(User {
378 added_to_attachment_menu: None,
379 allows_write_to_pm: None,
380 is_premium: Some(true),
381 first_name: "Vladislav".to_string(),
382 id: 279058397,
383 is_bot: None,
384 last_name: Some("Kibenko".to_string()),
385 language_code: Some("ru".to_string()),
386 photo_url: None,
387 username: Some("vdkfrost".to_string())
388 })
389 }
390 );
391 }
392
393 #[test]
394 fn test_parse_invalid_data() {
395 let init_data = "invalid data";
396 let result = parse(init_data);
397 assert!(result.is_err());
398 }
399
400 #[test]
401 fn test_sign_query_string() {
402 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";
403 let test_bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
404 let test_sign_hash =
405 "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2".to_string();
406 let auth_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1662771648);
407
408 let result = sign_query_string(qs, test_bot_token, auth_time).unwrap();
409
410 assert_eq!(result, test_sign_hash);
411 }
412
413 #[test]
414 fn test_sign_query_string_no_date() {
415 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";
416 let test_bot_token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
417 let test_sign_hash =
418 "c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2".to_string();
419 let auth_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1662771648);
420
421 let result = sign_query_string(qs, test_bot_token, auth_time).unwrap();
422
423 assert_eq!(result, test_sign_hash);
424 }
425
426 #[test]
427 fn test_validate_success() {
428 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";
429 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
430 let exp_in = Duration::from_secs(1662771648);
431
432 assert!(matches!(validate(init_data, token, exp_in), Ok(true)));
433 }
434
435 #[test]
436 fn test_validate_expired() {
437 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";
438 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
439 let exp_in = Duration::from_secs(86400);
440
441 assert!(matches!(
442 validate(init_data, token, exp_in),
443 Err(ValidationError::Expired)
444 ));
445 }
446
447 #[test]
448 fn test_validate_missing_hash() {
449 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";
450 let token = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
451 let exp_in = Duration::from_secs(86400);
452
453 assert!(matches!(
454 validate(init_data, token, exp_in),
455 Err(ValidationError::SignMissing)
456 ));
457 }
458}