telegram_webapp_sdk/utils/
validate_init_data.rs

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