1use bytes::Bytes;
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4
5use crate::{error::WiError, types::Event};
6
7type HmacSha256 = Hmac<Sha256>;
8
9const SIGNATURE_HEADER: &str = "x-wi-signature";
10const SIGNATURE_V2_HEADER: &str = "x-wi-signature-v2";
11
12pub const DEFAULT_TOLERANCE_SECONDS: u64 = 300;
14
15#[derive(Debug, Clone, Copy)]
17pub struct VerifyOptions {
18 pub tolerance_seconds: u64,
20 pub now: Option<i64>,
22}
23
24impl Default for VerifyOptions {
25 fn default() -> Self {
26 Self {
27 tolerance_seconds: DEFAULT_TOLERANCE_SECONDS,
28 now: None,
29 }
30 }
31}
32
33fn header_value<'a, K: AsRef<str>, V: AsRef<str>>(
34 headers: &'a [(K, V)],
35 name: &str,
36) -> Option<&'a str> {
37 headers
38 .iter()
39 .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
40 .map(|(_, v)| v.as_ref())
41}
42
43pub fn verify_webhook<K: AsRef<str>, V: AsRef<str>>(
55 body: &[u8],
56 headers: &[(K, V)],
57 secret: &[u8],
58 options: VerifyOptions,
59) -> bool {
60 let Some(v2) = header_value(headers, SIGNATURE_V2_HEADER) else {
61 return header_value(headers, SIGNATURE_HEADER)
62 .is_some_and(|legacy| verify_signature(body, legacy, secret));
63 };
64
65 let (mut t, mut v1) = (None, None);
66 for part in v2.split(',') {
67 match part.trim().split_once('=') {
68 Some(("t", v)) => t = Some(v),
69 Some(("v1", v)) => v1 = Some(v),
70 _ => {}
71 }
72 }
73 let (Some(t_str), Some(v1)) = (t, v1) else {
74 return false;
75 };
76 let Ok(t_val) = t_str.parse::<i64>() else {
77 return false;
78 };
79 let Ok(expected) = hex::decode(v1) else {
80 return false;
81 };
82
83 let now = options.now.unwrap_or_else(|| {
84 std::time::SystemTime::now()
85 .duration_since(std::time::UNIX_EPOCH)
86 .map(|d| d.as_secs() as i64)
87 .unwrap_or(0)
88 });
89 if now.abs_diff(t_val) > options.tolerance_seconds {
90 return false;
91 }
92
93 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
94 mac.update(t_str.as_bytes());
95 mac.update(b".");
96 mac.update(body);
97 mac.verify_slice(&expected).is_ok()
98}
99
100pub fn verify_signature(body: &[u8], signature: &str, secret: &[u8]) -> bool {
109 let hex_part = match signature.strip_prefix("sha256=") {
110 Some(h) => h,
111 None => return false,
112 };
113
114 let expected = match hex::decode(hex_part) {
115 Ok(b) => b,
116 Err(_) => return false,
117 };
118
119 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
120 mac.update(body);
121 mac.verify_slice(&expected).is_ok()
122}
123
124pub fn parse_event(body: &[u8]) -> Result<Event, WiError> {
126 serde_json::from_slice(body).map_err(WiError::Serialize)
127}
128
129pub struct WebhookPayload {
131 pub event: Event,
132 pub raw: Bytes,
133}
134
135impl WebhookPayload {
136 pub fn from_parts(
140 body: Bytes,
141 signature: &str,
142 secret: &[u8],
143 ) -> Option<Result<Self, WiError>> {
144 if !verify_signature(&body, signature, secret) {
145 return None;
146 }
147 Some(parse_event(&body).map(|event| WebhookPayload { event, raw: body }))
148 }
149
150 pub fn from_headers<K: AsRef<str>, V: AsRef<str>>(
154 body: Bytes,
155 headers: &[(K, V)],
156 secret: &[u8],
157 options: VerifyOptions,
158 ) -> Option<Result<Self, WiError>> {
159 if !verify_webhook(&body, headers, secret, options) {
160 return None;
161 }
162 Some(parse_event(&body).map(|event| WebhookPayload { event, raw: body }))
163 }
164
165 pub const HEADER: &'static str = SIGNATURE_HEADER;
167}