vylth_flow/resources/
webhooks.rs1use hmac::{Hmac, Mac};
2use sha2::Sha256;
3
4use crate::error::FlowError;
5use crate::types::WebhookEvent;
6
7type HmacSha256 = Hmac<Sha256>;
8
9pub 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 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 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}