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