1use std::time::{SystemTime, UNIX_EPOCH};
8
9use crate::error::InitDataError;
10use crate::model::InitData;
11use crate::{parse, sign};
12
13const DEFAULT_EXPIRATION: u64 = 86400;
15
16fn extract_hash(init_data: &str) -> Result<(String, String), InitDataError> {
25 let (base_data, hash) = if let Some(pos) = init_data.find("&hash=") {
26 let (base, hash_part) = init_data.split_at(pos);
27 let hash = &hash_part[6..]; (base.to_string(), hash.to_string())
29 } else {
30 return Err(InitDataError::HashMissing);
31 };
32
33 if !hash.chars().all(|c| c.is_ascii_hexdigit()) || hash.len() != 64 {
34 return Err(InitDataError::HashInvalid);
35 }
36
37 Ok((base_data, hash))
38}
39
40pub fn validate(init_data: &str, token: &str, expires_in: Option<u64>) -> Result<InitData, InitDataError> {
74 if init_data.is_empty() || !init_data.contains('=') {
75 return Err(InitDataError::UnexpectedFormat(
76 "init_data is empty or malformed".to_string(),
77 ));
78 }
79
80 let (base_data, hash) = extract_hash(init_data)?;
81
82 let expected_hash = sign(&base_data, token)?;
83
84 if hash != expected_hash {
85 return Err(InitDataError::HashInvalid);
86 }
87
88 let data = parse(init_data)?;
89
90 let expires_in = expires_in.unwrap_or(DEFAULT_EXPIRATION);
91 if expires_in > 0 {
92 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
93
94 if data.auth_date + expires_in < now {
95 return Err(InitDataError::Expired);
96 }
97 }
98
99 Ok(data)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 const BOT_TOKEN: &str = "5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8";
107 const INVALID_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
108 const VALID_INIT_DATA: &str = "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";
110
111 #[test]
112 fn test_validate_empty_data() {
113 let result = validate("", BOT_TOKEN, None);
114 assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
115 }
116
117 #[test]
118 fn test_validate_invalid_format() {
119 let result = validate("invalid_format", BOT_TOKEN, None);
120 assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
121 }
122
123 #[test]
124 fn test_validate_missing_hash() {
125 let data = "query_id=test&auth_date=123";
126 let token = "valid:token";
127 let result = validate(data, token, None);
128 assert!(matches!(result, Err(InitDataError::HashMissing)));
129 }
130
131 #[test]
132 fn test_validate_invalid_hash() {
133 let result = validate("query_id=test123&hash=invalid", BOT_TOKEN, None);
134 assert!(matches!(result, Err(InitDataError::HashInvalid)));
135 }
136
137 #[test]
138 fn test_validate_expired() {
139 let base_data = VALID_INIT_DATA
140 .replace("auth_date=1662771648", "auth_date=1000000000")
141 .replace(
142 "hash=c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2",
143 "",
144 );
145
146 let hash = sign(&base_data, BOT_TOKEN).unwrap();
147 let init_data = format!("{base_data}&hash={hash}");
148 let result = validate(&init_data, BOT_TOKEN, Some(86400));
149 assert!(matches!(result, Err(InitDataError::Expired)));
150 }
151
152 #[test]
153 fn test_validate_no_expiration() {
154 let base_data = VALID_INIT_DATA
155 .replace("auth_date=1662771648", "auth_date=1000000000")
156 .replace(
157 "hash=c501b71e775f74ce10e377dea85a7ea24ecd640b223ea86dfe453e0eaed2e2b2",
158 "",
159 );
160
161 let hash = sign(&base_data, BOT_TOKEN).unwrap();
162 let init_data = format!("{base_data}&hash={hash}");
163 let result = validate(&init_data, BOT_TOKEN, Some(0));
164 println!("result: {result:?}");
165
166 assert!(result.is_ok());
167 }
168
169 #[test]
170 fn test_validate_valid_data() {
171 let result = validate(VALID_INIT_DATA, BOT_TOKEN, Some(0)); assert!(result.is_ok());
174
175 let data = result.unwrap();
176 assert_eq!(data.auth_date, 1662771648);
177 assert!(data.user.is_some());
178
179 if let Some(user) = data.user {
180 assert_eq!(user.id, 279058397);
181 assert_eq!(user.first_name, "Vladislav");
182 assert_eq!(user.last_name, Some("Kibenko".to_string()));
183 assert_eq!(user.username, Some("vdkfrost".to_string()));
184 assert_eq!(user.language_code, Some("ru".to_string()));
185 assert_eq!(user.is_premium, Some(true));
186 }
187 }
188
189 #[test]
190 fn test_validate_malformed_hash() {
191 let result = validate("query_id=test123&hash=", BOT_TOKEN, None);
192 assert!(matches!(result, Err(InitDataError::HashInvalid)));
193 }
194
195 #[test]
196 fn test_validate_hash_format_length() {
197 let result = validate("query_id=test123&hash=abc123", BOT_TOKEN, None);
198 assert!(matches!(result, Err(InitDataError::HashInvalid)));
199
200 let result = validate(&format!("query_id=test123&hash={INVALID_HASH}0"), BOT_TOKEN, None);
202 assert!(matches!(result, Err(InitDataError::HashInvalid)));
203 }
204
205 #[test]
206 fn test_validate_hash_format_invalid_chars() {
207 let result = validate(
209 "query_id=test123&hash=gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
210 BOT_TOKEN,
211 None,
212 );
213 assert!(matches!(result, Err(InitDataError::HashInvalid)));
214 }
215
216 #[test]
217 fn test_validate_hash_extraction_failure() {
218 let result = validate("query_id=test123&hash=&other=value", BOT_TOKEN, None);
220 assert!(matches!(result, Err(InitDataError::HashInvalid)));
221
222 let result = validate("query_id=test123&hash=&auth_date=123", BOT_TOKEN, None);
224 assert!(matches!(result, Err(InitDataError::HashInvalid)));
225 }
226
227 #[test]
228 fn test_validate_impossible_hash_extraction() {
229 let result = validate("query_id=test123&hash=abc\n&hash=def", BOT_TOKEN, None);
232 assert!(matches!(result, Err(InitDataError::HashInvalid)));
233 }
234
235 #[test]
236 fn test_validate_hash_extraction_corner_case() {
237 let result = validate("query_id=test123&hash=", BOT_TOKEN, None);
239 assert!(matches!(result, Err(InitDataError::HashInvalid)));
240
241 let result = validate("query_id=test123%26hash=abc", BOT_TOKEN, None);
243 assert!(matches!(result, Err(InitDataError::HashMissing)));
244
245 let result = validate("query_id=test123%26hash%3Dabc", BOT_TOKEN, None);
247 assert!(matches!(result, Err(InitDataError::HashMissing)));
248 }
249
250 #[test]
251 fn test_extract_hash() {
252 let init_data = "query_id=test123&hash=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
254 let result = extract_hash(init_data);
255 assert!(result.is_ok());
256 let (base, hash) = result.unwrap();
257 assert_eq!(base, "query_id=test123");
258 assert_eq!(hash, "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
259
260 let result = extract_hash("query_id=test123");
262 assert!(matches!(result, Err(InitDataError::HashMissing)));
263
264 let result = extract_hash("query_id=test123&hash=invalid");
266 assert!(matches!(result, Err(InitDataError::HashInvalid)));
267 }
268
269 #[test]
270 fn test_validate_incorrect_hash() {
271 let base_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";
272 let invalid_hash = "0000000000000000000000000000000000000000000000000000000000000000";
274 let init_data = format!("{base_data}&hash={invalid_hash}");
275 let result = validate(&init_data, BOT_TOKEN, None);
276 assert!(matches!(result, Err(InitDataError::HashInvalid)));
277 }
278}