use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::FlowError;
use crate::types::WebhookEvent;
type HmacSha256 = Hmac<Sha256>;
pub struct WebhookVerifier {
secret: String,
}
impl WebhookVerifier {
pub fn new(secret: &str) -> Self {
Self {
secret: secret.to_string(),
}
}
pub fn verify(&self, payload: &[u8], signature: &str) -> Result<WebhookEvent, FlowError> {
if self.secret.is_empty() {
return Err(FlowError::InvalidSignature(
"webhook secret not configured".into(),
));
}
let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
.map_err(|e| FlowError::Other(e.to_string()))?;
mac.update(payload);
let expected = hex::encode(mac.finalize().into_bytes());
if !constant_time_eq(expected.as_bytes(), signature.as_bytes()) {
return Err(FlowError::InvalidSignature(
"signature mismatch".into(),
));
}
let event: WebhookEvent = serde_json::from_slice(payload)
.map_err(|e| FlowError::Other(format!("parse webhook event: {}", e)))?;
Ok(event)
}
pub fn is_valid(&self, payload: &[u8], signature: &str) -> bool {
if self.secret.is_empty() {
return false;
}
let Ok(mut mac) = HmacSha256::new_from_slice(self.secret.as_bytes()) else {
return false;
};
mac.update(payload);
let expected = hex::encode(mac.finalize().into_bytes());
constant_time_eq(expected.as_bytes(), signature.as_bytes())
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}