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