rocket_webhook/webhooks/built_in/
standard.rs1use base64::{Engine, prelude::BASE64_STANDARD};
2use hmac::Hmac;
3use rocket::{Request, data::Outcome, http::Status, outcome::try_outcome, tokio::io::AsyncRead};
4use sha2::Sha256;
5use zeroize::Zeroizing;
6
7use crate::{
8 WebhookError,
9 webhooks::{Webhook, interface::hmac::WebhookHmac},
10};
11
12const ID_HEADER: &str = "id";
13const TIMESTAMP_HEADER: &str = "timestamp";
14const SIG_HEADER: &str = "signature";
15
16pub struct StandardWebhook {
28 secret_key: Zeroizing<Vec<u8>>,
29 id_header: String,
30 time_header: String,
31 sig_header: String,
32}
33
34impl StandardWebhook {
35 pub fn with_secret(secret_key: impl AsRef<str>) -> Result<Self, base64::DecodeError> {
38 let stripped_key = secret_key
39 .as_ref()
40 .strip_prefix("whsec_")
41 .unwrap_or(secret_key.as_ref());
42 let secret_key = Zeroizing::new(BASE64_STANDARD.decode(stripped_key)?);
43 Ok(Self {
44 secret_key,
45 id_header: format!("webhook-{ID_HEADER}"),
46 sig_header: format!("webhook-{SIG_HEADER}"),
47 time_header: format!("webhook-{TIMESTAMP_HEADER}"),
48 })
49 }
50
51 pub fn with_secret_and_prefix(
54 secret_key: impl AsRef<str>,
55 header_prefix: impl AsRef<str>,
56 ) -> Result<Self, base64::DecodeError> {
57 let stripped_key = secret_key
58 .as_ref()
59 .strip_prefix("whsec_")
60 .unwrap_or(secret_key.as_ref());
61 let secret_key = Zeroizing::new(BASE64_STANDARD.decode(stripped_key)?);
62 Ok(Self {
63 secret_key,
64 id_header: format!("{}{ID_HEADER}", header_prefix.as_ref()),
65 sig_header: format!("{}{SIG_HEADER}", header_prefix.as_ref()),
66 time_header: format!("{}{TIMESTAMP_HEADER}", header_prefix.as_ref()),
67 })
68 }
69}
70
71impl Webhook for StandardWebhook {
72 async fn validate_body(
73 &self,
74 req: &Request<'_>,
75 body: impl AsyncRead + Unpin + Send + Sync,
76 time_bounds: (u32, u32),
77 ) -> Outcome<'_, Vec<u8>, WebhookError> {
78 self.validate_with_hmac(req, body, time_bounds).await
79 }
80}
81
82impl WebhookHmac for StandardWebhook {
83 type MAC = Hmac<Sha256>;
84
85 fn secret_key(&self) -> &[u8] {
86 &self.secret_key
87 }
88
89 fn body_prefix(
90 &self,
91 req: &Request<'_>,
92 time_bounds: (u32, u32),
93 ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
94 let id = try_outcome!(self.get_header(req, &self.id_header, None));
95 let timestamp = try_outcome!(self.get_header(req, &self.time_header, None));
96 try_outcome!(self.validate_timestamp(timestamp, time_bounds));
97
98 let prefix = [id.as_bytes(), b".", timestamp.as_bytes(), b"."].concat();
99 Outcome::Success(Some(prefix))
100 }
101
102 fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError> {
104 let header = try_outcome!(self.get_header(req, &self.sig_header, None));
105 let mut signatures = Vec::new();
106 for base64_sig in header.split(' ').filter_map(|s| s.strip_prefix("v1,")) {
107 match BASE64_STANDARD.decode(base64_sig) {
108 Ok(bytes) => signatures.push(bytes),
109 Err(_) => {
110 return Outcome::Error((
111 Status::BadRequest,
112 WebhookError::InvalidHeader(format!(
113 "Signature in '{}' header was not valid base64: got '{base64_sig}'",
114 self.sig_header
115 )),
116 ));
117 }
118 }
119 }
120
121 Outcome::Success(signatures)
122 }
123}