1use crate::types::webhooks::WebhookEvent;
3use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, thiserror::Error)]
10pub enum WebhookError {
11 #[error("invalid webhook signature")]
13 InvalidSignature,
14 #[error("invalid input ")]
16 Invalid(String),
17 #[error("failed to deserialize webhook payload: error:{0} content:{1}")]
19 Deserialization(serde_json::Error, String),
20}
21
22type HmacSha256 = Hmac<Sha256>;
23
24const DEFAULT_TOLERANCE_SECONDS: i64 = 300;
25
26pub struct Webhooks;
27
28impl Webhooks {
29 pub fn build_event(
30 body: &str,
31 signature: &str,
32 timestamp: &str,
33 webhook_id: &str,
34 secret: &str,
35 ) -> Result<WebhookEvent, WebhookError> {
36 Self::build_event_with_tolerance(
37 body,
38 signature,
39 timestamp,
40 webhook_id,
41 secret,
42 DEFAULT_TOLERANCE_SECONDS,
43 )
44 }
45
46 fn build_event_with_tolerance(
47 body: &str,
48 signature: &str,
49 timestamp: &str,
50 webhook_id: &str,
51 secret: &str,
52 tolerance_seconds: i64,
53 ) -> Result<WebhookEvent, WebhookError> {
54 Self::verify_signature_with_tolerance(
56 body,
57 signature,
58 timestamp,
59 webhook_id,
60 secret,
61 tolerance_seconds,
62 )?;
63
64 let event: WebhookEvent = serde_json::from_str(body)
66 .map_err(|e| WebhookError::Deserialization(e, body.to_string()))?;
67
68 Ok(event)
69 }
70
71 pub fn verify_signature(
72 body: &str,
73 signature: &str,
74 timestamp: &str,
75 webhook_id: &str,
76 secret: &str,
77 ) -> Result<(), WebhookError> {
78 Self::verify_signature_with_tolerance(
79 body,
80 signature,
81 timestamp,
82 webhook_id,
83 secret,
84 DEFAULT_TOLERANCE_SECONDS,
85 )
86 }
87
88 fn verify_signature_with_tolerance(
89 body: &str,
90 signature: &str,
91 timestamp: &str,
92 webhook_id: &str,
93 secret: &str,
94 tolerance_seconds: i64,
95 ) -> Result<(), WebhookError> {
96 let timestamp_seconds = timestamp
98 .parse::<i64>()
99 .map_err(|_| WebhookError::Invalid("invalid timestamp format".to_string()))?;
100
101 let now = SystemTime::now()
102 .duration_since(UNIX_EPOCH)
103 .unwrap()
104 .as_secs() as i64;
105
106 if now - timestamp_seconds > tolerance_seconds {
107 return Err(WebhookError::Invalid(
108 "webhook timestamp is too old".to_string(),
109 ));
110 }
111
112 if timestamp_seconds > now + tolerance_seconds {
113 return Err(WebhookError::Invalid(
114 "webhook timestamp is too new".to_string(),
115 ));
116 }
117
118 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
120
121 let secret_key = secret.strip_prefix("whsec_").unwrap_or(secret);
123
124 let secret_bytes = BASE64.decode(secret_key).map_err(|_| {
126 WebhookError::Invalid("failed to decode secret from base64".to_string())
127 })?;
128
129 let mut mac = HmacSha256::new_from_slice(&secret_bytes)
131 .map_err(|_| WebhookError::Invalid("invalid secret key length".to_string()))?;
132 mac.update(signed_payload.as_bytes());
133
134 let expected_signature = BASE64.encode(mac.finalize().into_bytes());
136
137 let signature_to_verify = if signature.contains(',') {
140 signature
142 .split_whitespace()
143 .filter_map(|sig| {
144 let parts: Vec<&str> = sig.split(',').collect();
145 if parts.len() == 2 && parts[0] == "v1" {
146 Some(parts[1])
147 } else {
148 None
149 }
150 })
151 .collect::<Vec<&str>>()
152 } else {
153 vec![signature]
154 };
155
156 for sig in signature_to_verify {
158 if constant_time_eq(sig.as_bytes(), expected_signature.as_bytes()) {
159 return Ok(());
160 }
161 }
162
163 Err(WebhookError::InvalidSignature)
164 }
165}
166
167fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
168 if a.len() != b.len() {
169 return false;
170 }
171
172 let mut result = 0u8;
173 for (a_byte, b_byte) in a.iter().zip(b.iter()) {
174 result |= a_byte ^ b_byte;
175 }
176
177 result == 0
178}
179
180#[cfg(all(test, feature = "webhook"))]
181mod tests {
182 use super::*;
183
184 fn current_timestamp() -> String {
185 SystemTime::now()
186 .duration_since(UNIX_EPOCH)
187 .unwrap()
188 .as_secs()
189 .to_string()
190 }
191
192 #[test]
193 fn test_constant_time_eq() {
194 assert!(constant_time_eq(b"hello", b"hello"));
195 assert!(!constant_time_eq(b"hello", b"world"));
196 assert!(!constant_time_eq(b"hello", b"hell"));
197 assert!(!constant_time_eq(b"hello", b"helloo"));
198 }
199
200 #[test]
201 fn test_verify_signature_invalid() {
202 let body = r#"{"test":"data"}"#;
203 let signature = "invalid_signature";
204 let timestamp = current_timestamp();
205 let webhook_id = "webhook_test";
206 let secret = BASE64.encode(b"test_secret");
207
208 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
209 assert!(result.is_err());
210 }
212
213 #[test]
214 fn test_verify_signature_valid() {
215 let body = r#"{"test":"data"}"#;
216 let timestamp = current_timestamp();
217 let webhook_id = "webhook_test";
218 let secret = BASE64.encode(b"test_secret");
220
221 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
223 let secret_bytes = BASE64.decode(&secret).unwrap();
224 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
225 mac.update(signed_payload.as_bytes());
226 let signature = BASE64.encode(mac.finalize().into_bytes());
227
228 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
229 assert!(result.is_ok());
230 }
231
232 #[test]
233 fn test_verify_signature_with_prefix() {
234 let body = r#"{"test":"data"}"#;
235 let timestamp = current_timestamp();
236 let webhook_id = "webhook_test";
237 let secret = BASE64.encode(b"test_secret");
238 let prefixed_secret = format!("whsec_{}", secret);
239
240 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
242 let secret_bytes = BASE64.decode(&secret).unwrap();
243 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
244 mac.update(signed_payload.as_bytes());
245 let signature = BASE64.encode(mac.finalize().into_bytes());
246
247 let result =
249 Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &prefixed_secret);
250 assert!(result.is_ok());
251 }
252
253 #[test]
254 fn test_verify_signature_with_version() {
255 let body = r#"{"test":"data"}"#;
256 let timestamp = current_timestamp();
257 let webhook_id = "webhook_test";
258 let secret = BASE64.encode(b"test_secret");
259
260 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
262 let secret_bytes = BASE64.decode(&secret).unwrap();
263 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
264 mac.update(signed_payload.as_bytes());
265 let sig_b64 = BASE64.encode(mac.finalize().into_bytes());
266
267 let signature = format!("v1,{}", sig_b64);
269
270 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
271 assert!(result.is_ok());
272 }
273
274 #[test]
275 fn test_timestamp_too_old() {
276 let body = r#"{"test":"data"}"#;
277 let old_timestamp = "1234567890"; let webhook_id = "webhook_test";
279 let secret = BASE64.encode(b"test_secret");
280
281 let signed_payload = format!("{}.{}.{}", webhook_id, old_timestamp, body);
283 let secret_bytes = BASE64.decode(&secret).unwrap();
284 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
285 mac.update(signed_payload.as_bytes());
286 let signature = BASE64.encode(mac.finalize().into_bytes());
287
288 let result =
289 Webhooks::verify_signature(body, &signature, old_timestamp, webhook_id, &secret);
290 assert!(result.is_err());
291 match result.unwrap_err() {
292 WebhookError::Invalid(msg) => {
293 assert!(msg.contains("too old"));
294 }
295 _ => panic!("Expected InvalidSignature error"),
296 }
297 }
298
299 #[test]
300 fn test_timestamp_too_new() {
301 let body = r#"{"test":"data"}"#;
302 let future_timestamp = (SystemTime::now()
304 .duration_since(UNIX_EPOCH)
305 .unwrap()
306 .as_secs()
307 + 1000)
308 .to_string();
309 let webhook_id = "webhook_test";
310 let secret = BASE64.encode(b"test_secret");
311
312 let signed_payload = format!("{}.{}.{}", webhook_id, future_timestamp, body);
314 let secret_bytes = BASE64.decode(&secret).unwrap();
315 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
316 mac.update(signed_payload.as_bytes());
317 let signature = BASE64.encode(mac.finalize().into_bytes());
318
319 let result =
320 Webhooks::verify_signature(body, &signature, &future_timestamp, webhook_id, &secret);
321 assert!(result.is_err());
322 match result.unwrap_err() {
323 WebhookError::Invalid(msg) => {
324 assert!(msg.contains("too new"));
325 }
326 _ => panic!("Expected InvalidSignature error"),
327 }
328 }
329
330 #[test]
331 fn test_invalid_timestamp_format() {
332 let body = r#"{"test":"data"}"#;
333 let invalid_timestamp = "not_a_number";
334 let webhook_id = "webhook_test";
335 let secret = BASE64.encode(b"test_secret");
336
337 let result = Webhooks::verify_signature(
338 body,
339 "any_signature",
340 invalid_timestamp,
341 webhook_id,
342 &secret,
343 );
344 assert!(result.is_err());
345 match result.unwrap_err() {
346 WebhookError::Invalid(msg) => {
347 assert!(msg.contains("timestamp"));
348 }
349 _ => panic!("Expected InvalidSignature error"),
350 }
351 }
352
353 #[test]
354 fn test_construct_event_invalid_json() {
355 let body = r#"{"invalid json"#;
356 let timestamp = current_timestamp();
357 let webhook_id = "webhook_test";
358 let secret = BASE64.encode(b"test_secret");
359
360 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
362 let secret_bytes = BASE64.decode(&secret).unwrap();
363 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
364 mac.update(signed_payload.as_bytes());
365 let signature = BASE64.encode(mac.finalize().into_bytes());
366
367 let result = Webhooks::build_event(body, &signature, ×tamp, webhook_id, &secret);
368 assert!(result.is_err());
369 assert!(matches!(
370 result.unwrap_err(),
371 WebhookError::Deserialization(..)
372 ));
373 }
374}