telegram_webapp_sdk/utils/
validate_init_data.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use 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/// Errors that can occur when validating Telegram init data.
11#[derive(Debug, Error, PartialEq, Eq)]
12pub enum ValidationError {
13    /// A required field such as `hash` or `signature` was missing.
14    #[error("missing required field: {0}")]
15    MissingField(&'static str),
16    /// Input contained invalid percent encoding or non-UTF8 data.
17    #[error("invalid encoding in init data")]
18    InvalidEncoding,
19    /// Signature value could not be parsed from its encoding (hex or base64).
20    #[error("invalid signature encoding")]
21    InvalidSignatureEncoding,
22    /// Computed signature did not match the provided one.
23    #[error("signature mismatch")]
24    SignatureMismatch,
25    /// Provided Ed25519 public key was malformed.
26    #[error("invalid public key")]
27    InvalidPublicKey
28}
29
30/// Key material used to validate Telegram init data.
31#[derive(Clone, Copy, Debug)]
32pub enum ValidationKey<'a> {
33    /// Validate using a bot token and HMAC-SHA256.
34    BotToken(&'a str),
35    /// Validate using an Ed25519 public key.
36    Ed25519PublicKey(&'a [u8; 32])
37}
38
39/// Validates the `hash` parameter of the init data using HMAC-SHA256.
40///
41/// The `init_data` string must be the exact value of
42/// `Telegram.WebApp.initData`. The function derives a secret key from the
43/// provided bot token and checks that the `hash` parameter matches the expected
44/// HMAC-SHA256.
45///
46/// # Errors
47/// Returns [`ValidationError`] if parsing fails or the hash does not match.
48///
49/// # Examples
50/// ```
51/// use hmac_sha256::{HMAC, Hash};
52/// use telegram_webapp_sdk::validate_init_data::verify_hmac_sha256;
53/// let token = "123456:ABC";
54/// let check_string = concat!("auth_date=1\n", "user=alice");
55/// let secret = Hash::hash(format!("WebAppData{token}").as_bytes());
56/// let hash = hex::encode(HMAC::mac(check_string.as_bytes(), secret));
57/// let init_data = format!("auth_date=1&user=alice&hash={hash}");
58/// assert!(verify_hmac_sha256(&init_data, token).is_ok());
59/// ```
60pub 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
74/// Validates the `signature` parameter of the init data using Ed25519.
75///
76/// The `init_data` string must include a `signature` parameter encoded in
77/// Base64. All other parameters are combined into the data check string
78/// according to Telegram's specification and verified against the provided
79/// Ed25519 public key.
80///
81/// # Errors
82/// Returns [`ValidationError`] if parsing fails or the signature does not
83/// verify.
84///
85/// # Examples
86/// ```
87/// use ed25519_dalek::{Signer, SigningKey};
88/// use telegram_webapp_sdk::validate_init_data::verify_ed25519;
89///
90/// // generate test key
91/// let sk = SigningKey::from_bytes(&[1u8; 32]);
92/// let pk = sk.verifying_key();
93/// let message = concat!("a=1\n", "b=2");
94/// let sig = sk.sign(message.as_bytes());
95/// let init_data = format!("a=1&b=2&signature={}", base64::encode(sig.to_bytes()));
96/// assert!(verify_ed25519(&init_data, pk.as_bytes()).is_ok());
97/// ```
98pub 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        // tamper with data
173        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        // modify data
199        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}