use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use thiserror::Error;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum WebhookError {
#[error("stadar: invalid webhook signature")]
Invalid,
}
pub fn verify(body: &[u8], signature: &str, secret: &str) -> Result<(), WebhookError> {
if secret.is_empty() || signature.trim().is_empty() {
return Err(WebhookError::Invalid);
}
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|_| WebhookError::Invalid)?;
mac.update(body);
let expected = mac.finalize().into_bytes();
for candidate in parse_signature_header(signature) {
if candidate.len() == expected.len()
&& bool::from(candidate.ct_eq(&expected))
{
return Ok(());
}
}
Err(WebhookError::Invalid)
}
fn parse_signature_header(header: &str) -> Vec<Vec<u8>> {
header
.trim()
.split(',')
.filter_map(|raw| {
let part = raw.trim();
let hex = match part.find('=') {
Some(eq) => {
if &part[..eq] != "v1" {
return None;
}
&part[eq + 1..]
}
None => part,
};
decode_hex(hex)
})
.collect()
}
fn decode_hex(hex: &str) -> Option<Vec<u8>> {
if hex.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(hex.len() / 2);
let bytes = hex.as_bytes();
let mut i = 0;
while i < bytes.len() {
let h = nibble(bytes[i])?;
let l = nibble(bytes[i + 1])?;
out.push((h << 4) | l);
i += 2;
}
Some(out)
}
fn nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}