use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
type HmacSha256 = Hmac<Sha256>;
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn sign_webhook_payload(secret: &str, payload: &str) -> String {
let Ok(mut mac) = HmacSha256::new_from_slice(secret.as_bytes()) else {
return String::new();
};
mac.update(payload.as_bytes());
let hex = hex::encode(mac.finalize().into_bytes());
format!("sha256={hex}")
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn constant_time_eq_hex(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.as_bytes().ct_eq(b.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_matches_node_crypto_vector_1() {
let sig = sign_webhook_payload("test-secret", r#"{"type":"version.created"}"#);
assert!(sig.starts_with("sha256="), "must use sha256= prefix");
assert_eq!(sig.len(), 7 + 64, "sha256= + 64 hex chars");
assert_eq!(
sig,
"sha256=b80c1f1744d458868dfc052244cc86b0fa5ddc9da037c9c8a23b7e473ff80bbe"
);
}
#[test]
fn sign_matches_node_crypto_vector_2() {
let sig = sign_webhook_payload(
"my-webhook-secret",
r#"{"type":"state.changed","slug":"xK9mP2nQ"}"#,
);
assert!(sig.starts_with("sha256="));
assert_eq!(sig.len(), 7 + 64);
assert_eq!(
sig,
"sha256=60a5fe34e07cb00dae4d6344f36ed0983504b097fc9e94fc011a70ce66e0938e"
);
}
#[test]
fn sign_matches_node_crypto_vector_3() {
let sig = sign_webhook_payload("secret", "");
assert!(sig.starts_with("sha256="));
assert_eq!(sig.len(), 7 + 64);
assert_eq!(
sig,
"sha256=f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169"
);
}
#[test]
fn different_secrets_produce_different_signatures() {
let payload = r#"{"type":"approval.submitted"}"#;
let sig_a = sign_webhook_payload("secret-a", payload);
let sig_b = sign_webhook_payload("secret-b", payload);
assert_ne!(sig_a, sig_b);
}
#[test]
fn different_payloads_produce_different_signatures() {
let secret = "shared-secret";
let sig_a = sign_webhook_payload(secret, r#"{"type":"version.created"}"#);
let sig_b = sign_webhook_payload(secret, r#"{"type":"document.archived"}"#);
assert_ne!(sig_a, sig_b);
}
#[test]
fn constant_time_eq_hex_equal() {
let digest = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
assert!(constant_time_eq_hex(digest, digest));
}
#[test]
fn constant_time_eq_hex_different() {
let a = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
let b = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
assert!(!constant_time_eq_hex(a, b));
}
#[test]
fn constant_time_eq_hex_different_lengths() {
assert!(!constant_time_eq_hex("abc", "abcd"));
assert!(!constant_time_eq_hex("", "a"));
}
#[test]
fn constant_time_eq_hex_empty_strings() {
assert!(constant_time_eq_hex("", ""));
}
}