init_data_rs/
third_party_validation.rs1use crate::parse;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD as base64_engine;
3use base64::Engine as _;
4use ed25519_dalek::{Signature, Verifier, VerifyingKey};
5use hex::FromHex;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::{InitData, InitDataError};
9
10const TEST_PUBLIC_KEY: &str = "40055058a4ee38156a06562e52eece92a771bcd8346a8c4615cb7376eddf72ec";
11const PROD_PUBLIC_KEY: &str = "e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d";
12
13fn validate_third_party_with_signature(
35 init_data: &str,
36 bot_id: i64,
37 expires_in: Option<u64>,
38 is_test: bool,
39) -> Result<InitData, InitDataError> {
40 if init_data.is_empty() || !init_data.contains('=') {
41 return Err(InitDataError::UnexpectedFormat(
42 "init_data is empty or malformed".to_string(),
43 ));
44 }
45
46 let pairs: Vec<(String, String)> = url::form_urlencoded::parse(init_data.as_bytes())
47 .map(|(k, v)| (k.into_owned(), v.into_owned()))
48 .collect();
49
50 let mut signature_b64 = None;
51 let mut filtered_pairs: Vec<(String, String)> = Vec::new();
52 let mut auth_date: Option<u64> = None;
53 for (k, v) in pairs {
54 if k == "signature" {
55 signature_b64 = Some(v);
56 } else if k == "auth_date" {
57 auth_date = v.parse().ok();
58 filtered_pairs.push((k, v));
59 } else if k != "hash" {
60 filtered_pairs.push((k, v));
61 }
62 }
63 let signature_b64 = signature_b64.ok_or(InitDataError::SignatureMissing)?;
64
65 if let (Some(expires_in), Some(auth_date)) = (expires_in, auth_date) {
66 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
67 if auth_date + expires_in < now {
68 return Err(InitDataError::Expired);
69 }
70 }
71
72 filtered_pairs.sort_by(|a, b| a.0.cmp(&b.0));
73
74 let message = format!(
75 "{}:WebAppData\n{}",
76 bot_id,
77 filtered_pairs
78 .iter()
79 .map(|(k, v)| format!("{k}={v}"))
80 .collect::<Vec<_>>()
81 .join("\n")
82 );
83
84 let signature_bytes = base64_engine
85 .decode(signature_b64.as_bytes())
86 .map_err(|_| InitDataError::SignatureInvalid("Failed to decode signature from base64".to_string()))?;
87
88 let signature = Signature::from_slice(&signature_bytes)
89 .map_err(|_| InitDataError::SignatureInvalid("Failed to parse signature".to_string()))?;
90
91 let public_key_hex = if is_test { TEST_PUBLIC_KEY } else { PROD_PUBLIC_KEY };
92
93 let public_key_bytes = <[u8; 32]>::from_hex(public_key_hex)
94 .map_err(|_| InitDataError::SignatureInvalid("Failed to parse public key".to_string()))?;
95
96 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes)
97 .map_err(|_| InitDataError::SignatureInvalid("Failed to parse public key".to_string()))?;
98
99 verifying_key
100 .verify(message.as_bytes(), &signature)
101 .map_err(|_| InitDataError::SignatureInvalid("Failed to verify signature".to_string()))?;
102
103 let data = parse(init_data)?;
105 Ok(data)
106}
107
108pub fn validate_third_party(init_data: &str, bot_id: i64, expires_in: Option<u64>) -> Result<InitData, InitDataError> {
134 validate_third_party_with_signature(init_data, bot_id, expires_in, false)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 const VALID_INIT_DATA: &str = "user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%20%2B%20-%20%3F%20%5C%2F%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%2C%22allows_write_to_pm%22%3Atrue%2C%22photo_url%22%3A%22https%3A%5C%2F%5C%2Ft.me%5C%2Fi%5C%2Fuserpic%5C%2F320%5C%2F4FPEE4tmP3ATHa57u6MqTDih13LTOiMoKoLDRG4PnSA.svg%22%7D&chat_instance=8134722200314281151&chat_type=private&auth_date=1733584787&hash=2174df5b000556d044f3f020384e879c8efcab55ddea2ced4eb752e93e7080d6&signature=zL-ucjNyREiHDE8aihFwpfR9aggP2xiAo3NSpfe-p7IbCisNlDKlo7Kb6G4D0Ao2mBrSgEk4maLSdv6MLIlADQ";
142 const BOT_ID: i64 = 7342037359;
143
144 #[test]
145 fn test_valid_third_party_signature() {
146 let result = validate_third_party(VALID_INIT_DATA, BOT_ID, None);
147 assert!(result.is_ok(), "Expected Ok, got {result:?}");
148 }
149
150 #[test]
151 fn test_invalid_signature() {
152 let tampered = VALID_INIT_DATA.replace(
154 "zL-ucjNyREiHDE8aihFwpfR9aggP2xiAo3NSpfe-p7IbCisNlDKlo7Kb6G4D0Ao2mBrSgEk4maLSdv6MLIlADQ",
155 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
156 );
157 let result = validate_third_party(&tampered, BOT_ID, None);
158 assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
159 }
160
161 #[test]
162 fn test_third_party_invalid_base64_signature() {
163 let bad_data = "query_id=test&auth_date=123&signature=!!!notbase64!!!&hash=abc";
164 let bot_id = 123456;
165 let result = validate_third_party_with_signature(bad_data, bot_id, None, true);
166 assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
167 }
168
169 #[test]
170 fn test_third_party_invalid_public_key() {
171 let valid_data = "query_id=test&auth_date=123&signature=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&hash=abc";
172 let bot_id = 123456;
173 let result = validate_third_party_with_signature(valid_data, bot_id, None, true); assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
177 }
178
179 #[test]
180 fn test_third_party_signature_verification_failure() {
181 let bad_sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 64]);
183 let bad_data = format!("query_id=test&auth_date=123&signature={bad_sig}&hash=abc");
184 let bot_id = 123456;
185 let result = validate_third_party_with_signature(&bad_data, bot_id, None, true);
186 assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
187 }
188
189 #[test]
190 fn test_missing_signature() {
191 let mut parts: Vec<&str> = VALID_INIT_DATA.split('&').collect();
193 parts.retain(|s| !s.starts_with("signature="));
194 let no_sig = parts.join("&");
195 let result = validate_third_party(&no_sig, BOT_ID, None);
196 assert!(matches!(result, Err(InitDataError::SignatureMissing)));
197 }
198
199 #[test]
200 fn test_expired_data() {
201 let expired_data = VALID_INIT_DATA.replace("auth_date=1733584787", "auth_date=1000000000");
203 let result = validate_third_party(&expired_data, BOT_ID, Some(86400));
204 assert!(matches!(result, Err(InitDataError::Expired)));
205 }
206
207 #[test]
208 fn test_malformed_input() {
209 let result = validate_third_party("not_a_query_string", BOT_ID, None);
210 assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
211 }
212
213 #[test]
214 fn test_wrong_bot_id() {
215 let result = validate_third_party(VALID_INIT_DATA, 1234567890, None);
217 assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
218 }
219
220 #[test]
221 fn test_wrong_environment() {
222 let result = validate_third_party_with_signature(VALID_INIT_DATA, BOT_ID, None, true);
224 assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
225 }
226}