use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{Error, new_api_error};
use crate::types::WebhookEvent;
pub const HEADER_SIGNATURE: &str = "x-emailit-signature";
pub const HEADER_TIMESTAMP: &str = "x-emailit-timestamp";
pub const DEFAULT_TOLERANCE: u64 = 300;
type HmacSha256 = Hmac<Sha256>;
pub fn verify_webhook_signature(
raw_body: &str,
signature: &str,
timestamp: &str,
secret: &str,
tolerance: Option<u64>,
) -> Result<WebhookEvent, Error> {
let tol = tolerance.unwrap_or(DEFAULT_TOLERANCE);
if tol > 0 {
let ts: i64 = timestamp
.parse()
.map_err(|_| new_api_error(400, "Invalid webhook timestamp.".into(), String::new()))?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let age = now - ts;
if age > tol as i64 {
return Err(new_api_error(
401,
"Webhook timestamp is too old. The request may be a replay attack.".into(),
String::new(),
));
}
}
let computed = compute_webhook_signature(raw_body, timestamp, secret);
if !constant_time_eq(computed.as_bytes(), signature.as_bytes()) {
return Err(new_api_error(
401,
"Webhook signature verification failed.".into(),
String::new(),
));
}
let event: WebhookEvent = serde_json::from_str(raw_body).map_err(|_| {
new_api_error(
400,
"Invalid webhook payload: unable to decode JSON.".into(),
String::new(),
)
})?;
Ok(event)
}
pub fn compute_webhook_signature(raw_body: &str, timestamp: &str, secret: &str) -> String {
let signed_payload = format!("{}.{}", timestamp, raw_body);
let mut mac =
HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take any size key");
mac.update(signed_payload.as_bytes());
hex::encode(mac.finalize().into_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
}