use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
type HmacSha256 = Hmac<Sha256>;
pub struct Webhook;
impl Webhook {
pub const HEADER_SIGNATURE: &'static str = "X-Hivehook-Signature";
pub const HEADER_TIMESTAMP: &'static str = "X-Hivehook-Timestamp";
pub const HEADER_MESSAGE_ID: &'static str = "X-Hivehook-Message-ID";
pub fn sign(payload: &str, secret: &str, timestamp: i64) -> String {
let message = format!("{}.{}", timestamp, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.expect("HMAC accepts a key of any size");
mac.update(message.as_bytes());
let result = mac.finalize();
format!("v1={}", hex::encode(result.into_bytes()))
}
pub fn verify(
payload: &str,
secret: &str,
signature: &str,
timestamp: i64,
tolerance_seconds: Option<i64>,
) -> bool {
if let Some(tol) = tolerance_seconds {
let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as i64,
Err(_) => return false,
};
let age = (now - timestamp).unsigned_abs();
let limit = tol.max(0) as u64;
if age > limit {
return false;
}
}
let provided = match extract_v1(signature) {
Some(v) => v,
None => return false,
};
let expected = Self::sign(payload, secret, timestamp);
constant_time_eq(expected.as_bytes(), provided.as_bytes())
}
pub fn verify_with_rotation(
payload: &str,
primary: &str,
secondary: Option<&str>,
signature: &str,
timestamp: i64,
tolerance_seconds: Option<i64>,
) -> bool {
if Self::verify(payload, primary, signature, timestamp, tolerance_seconds) {
return true;
}
if let Some(sec) = secondary {
return Self::verify(payload, sec, signature, timestamp, tolerance_seconds);
}
false
}
}
fn extract_v1(signature: &str) -> Option<String> {
for part in signature.split(',') {
let trimmed = part.trim();
if let Some(rest) = trimmed.strip_prefix("v1=") {
return Some(format!("v1={}", rest));
}
}
None
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}