use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::{Result, ScreenshotFreeAPIError};
type HmacSha256 = Hmac<Sha256>;
pub fn verify_webhook_signature(body: &[u8], signature: &str, secret: &str) -> Result<()> {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|_| ScreenshotFreeAPIError::InvalidSignature)?;
mac.update(body);
let expected = hex::encode(mac.finalize().into_bytes());
if !constant_time_eq(expected.as_bytes(), signature.as_bytes()) {
return Err(ScreenshotFreeAPIError::InvalidSignature);
}
Ok(())
}
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
}
#[cfg(test)]
mod tests {
use super::*;
fn make_signature(body: &[u8], secret: &str) -> String {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(body);
hex::encode(mac.finalize().into_bytes())
}
#[test]
fn valid_signature_passes() {
let body = b"{\"jobId\":\"abc\",\"status\":\"completed\"}";
let secret = "my_webhook_secret";
let sig = make_signature(body, secret);
assert!(verify_webhook_signature(body, &sig, secret).is_ok());
}
#[test]
fn wrong_secret_fails() {
let body = b"{\"jobId\":\"abc\"}";
let sig = make_signature(body, "correct_secret");
assert!(verify_webhook_signature(body, &sig, "wrong_secret").is_err());
}
#[test]
fn tampered_body_fails() {
let original = b"{\"jobId\":\"abc\"}";
let tampered = b"{\"jobId\":\"xyz\"}";
let sig = make_signature(original, "secret");
assert!(verify_webhook_signature(tampered, &sig, "secret").is_err());
}
#[test]
fn length_mismatch_fails() {
let body = b"hello";
assert!(verify_webhook_signature(body, "short", "secret").is_err());
}
#[test]
fn empty_body_valid() {
let body = b"";
let sig = make_signature(body, "secret");
assert!(verify_webhook_signature(body, &sig, "secret").is_ok());
}
}