Skip to main content

vylth_flow/resources/
webhooks.rs

1use hmac::{Hmac, Mac};
2use sha2::Sha256;
3
4use crate::error::FlowError;
5use crate::types::WebhookEvent;
6
7type HmacSha256 = Hmac<Sha256>;
8
9/// Webhook signature verifier.
10pub struct WebhookVerifier {
11    secret: String,
12}
13
14impl WebhookVerifier {
15    pub fn new(secret: &str) -> Self {
16        Self {
17            secret: secret.to_string(),
18        }
19    }
20
21    /// Verify a webhook payload and return the parsed event.
22    pub fn verify(&self, payload: &[u8], signature: &str) -> Result<WebhookEvent, FlowError> {
23        if self.secret.is_empty() {
24            return Err(FlowError::InvalidSignature(
25                "webhook secret not configured".into(),
26            ));
27        }
28
29        let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
30            .map_err(|e| FlowError::Other(e.to_string()))?;
31        mac.update(payload);
32        let expected = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
33
34        if !constant_time_eq(expected.as_bytes(), signature.as_bytes()) {
35            return Err(FlowError::InvalidSignature(
36                "signature mismatch".into(),
37            ));
38        }
39
40        let event: WebhookEvent = serde_json::from_slice(payload)
41            .map_err(|e| FlowError::Other(format!("parse webhook event: {}", e)))?;
42        Ok(event)
43    }
44
45    /// Check if a webhook signature is valid without parsing.
46    pub fn is_valid(&self, payload: &[u8], signature: &str) -> bool {
47        if self.secret.is_empty() {
48            return false;
49        }
50        let Ok(mut mac) = HmacSha256::new_from_slice(self.secret.as_bytes()) else {
51            return false;
52        };
53        mac.update(payload);
54        let expected = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
55        constant_time_eq(expected.as_bytes(), signature.as_bytes())
56    }
57}
58
59fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
60    if a.len() != b.len() {
61        return false;
62    }
63    a.iter()
64        .zip(b.iter())
65        .fold(0u8, |acc, (x, y)| acc | (x ^ y))
66        == 0
67}