telegram_webapp_sdk/utils/
validate_init_data.rs1use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use hmac_sha256::{HMAC, Hash};
4use masterror::Error;
5use percent_encoding::percent_decode_str;
6
7#[derive(Debug, Error, PartialEq, Eq)]
9pub enum ValidationError {
10 #[error("missing required field: {0}")]
12 MissingField(&'static str),
13 #[error("invalid encoding in init data")]
15 InvalidEncoding,
16 #[error("invalid signature encoding")]
18 InvalidSignatureEncoding,
19 #[error("signature mismatch")]
21 SignatureMismatch,
22 #[error("invalid public key")]
24 InvalidPublicKey
25}
26
27#[derive(Clone, Copy, Debug)]
29pub enum ValidationKey<'a> {
30 BotToken(&'a str),
32 Ed25519PublicKey(&'a [u8; 32])
34}
35
36pub fn verify_hmac_sha256(init_data: &str, bot_token: &str) -> Result<(), ValidationError> {
58 let (check_string, hash) = extract_check_string(init_data, "hash")?;
59
60 let secret_key = Hash::hash(format!("WebAppData{bot_token}").as_bytes());
61 let expected = HMAC::mac(check_string.as_bytes(), secret_key);
62 let expected_hex = hex::encode(expected);
63
64 if expected_hex == hash {
65 Ok(())
66 } else {
67 Err(ValidationError::SignatureMismatch)
68 }
69}
70
71pub fn verify_ed25519(init_data: &str, public_key: &[u8; 32]) -> Result<(), ValidationError> {
96 let (check_string, signature_b64) = extract_check_string(init_data, "signature")?;
97
98 let sig_bytes = BASE64_STANDARD
99 .decode(signature_b64)
100 .map_err(|_| ValidationError::InvalidSignatureEncoding)?;
101 let signature = Signature::from_slice(&sig_bytes)
102 .map_err(|_| ValidationError::InvalidSignatureEncoding)?;
103 let verifying_key =
104 VerifyingKey::from_bytes(public_key).map_err(|_| ValidationError::InvalidPublicKey)?;
105
106 verifying_key
107 .verify(check_string.as_bytes(), &signature)
108 .map_err(|_| ValidationError::SignatureMismatch)
109}
110
111fn extract_check_string(
112 init_data: &str,
113 signature_field: &'static str
114) -> Result<(String, String), ValidationError> {
115 let mut data: Vec<(String, String)> = Vec::new();
116 let mut signature: Option<String> = None;
117
118 for pair in init_data.split('&') {
119 let mut parts = pair.splitn(2, '=');
120 let key = parts.next().ok_or(ValidationError::InvalidEncoding)?;
121 let value = parts.next().ok_or(ValidationError::InvalidEncoding)?;
122 let decoded = percent_decode_str(value)
123 .decode_utf8()
124 .map_err(|_| ValidationError::InvalidEncoding)?
125 .to_string();
126 if key == signature_field {
127 signature = Some(decoded);
128 } else {
129 data.push((key.to_string(), decoded));
130 }
131 }
132
133 let signature = signature.ok_or(ValidationError::MissingField(signature_field))?;
134
135 data.sort_by(|a, b| a.0.cmp(&b.0));
136 let check_string = data
137 .iter()
138 .map(|(k, v)| format!("{k}={v}"))
139 .collect::<Vec<_>>()
140 .join("\n");
141
142 Ok((check_string, signature))
143}
144
145#[cfg(test)]
146mod tests {
147 use ed25519_dalek::{Signer, SigningKey};
148
149 use super::*;
150
151 #[test]
152 fn hmac_validates() {
153 let bot_token = "123456:ABC";
154 let secret_key = Hash::hash(format!("WebAppData{bot_token}").as_bytes());
155 let check_string = "a=1\nb=2";
156 let expected = HMAC::mac(check_string.as_bytes(), secret_key);
157 let hash = hex::encode(expected);
158 let query = format!("a=1&b=2&hash={hash}");
159 assert!(verify_hmac_sha256(&query, bot_token).is_ok());
160 }
161
162 #[test]
163 fn hmac_rejects_modified_data() {
164 let bot_token = "123456:ABC";
165 let secret_key = Hash::hash(format!("WebAppData{bot_token}").as_bytes());
166 let check_string = "a=1\nb=2";
167 let expected = HMAC::mac(check_string.as_bytes(), secret_key);
168 let hash = hex::encode(expected);
169 assert_eq!(
171 verify_hmac_sha256(&format!("a=1&b=3&hash={hash}"), bot_token),
172 Err(ValidationError::SignatureMismatch)
173 );
174 }
175
176 #[test]
177 fn ed25519_validates() {
178 let sk = SigningKey::from_bytes(&[42u8; 32]);
179 let pk = sk.verifying_key();
180 let message = "a=1\nb=2";
181 let sig = sk.sign(message.as_bytes());
182 let init_data = format!(
183 "a=1&b=2&signature={}",
184 BASE64_STANDARD.encode(sig.to_bytes())
185 );
186 assert!(verify_ed25519(&init_data, pk.as_bytes()).is_ok());
187 }
188
189 #[test]
190 fn ed25519_rejects_bad_signature() {
191 let sk = SigningKey::from_bytes(&[42u8; 32]);
192 let pk = sk.verifying_key();
193 let message = "a=1\nb=2";
194 let sig = sk.sign(message.as_bytes());
195 let tampered = format!(
197 "a=1&b=3&signature={}",
198 BASE64_STANDARD.encode(sig.to_bytes())
199 );
200 assert_eq!(
201 verify_ed25519(&tampered, pk.as_bytes()),
202 Err(ValidationError::SignatureMismatch)
203 );
204 }
205}