rocket_webhook/webhooks/built_in/
stripe.rs1use hmac::Hmac;
2use rocket::{Request, data::Outcome, http::Status, outcome::try_outcome, tokio::io::AsyncRead};
3use sha2::Sha256;
4use zeroize::Zeroizing;
5
6use crate::{
7 WebhookError,
8 webhooks::{Webhook, interface::hmac::WebhookHmac},
9};
10
11pub struct StripeWebhook {
19 secret_key: Zeroizing<Vec<u8>>,
20}
21
22impl StripeWebhook {
23 pub fn with_secret(secret_key: impl Into<Vec<u8>>) -> Self {
25 Self {
26 secret_key: Zeroizing::new(secret_key.into()),
27 }
28 }
29}
30
31const SIG_HEADER: &str = "Stripe-Signature";
32
33impl Webhook for StripeWebhook {
34 async fn validate_body(
35 &self,
36 req: &Request<'_>,
37 body: impl AsyncRead + Unpin + Send + Sync,
38 time_bounds: (u32, u32),
39 ) -> Outcome<'_, Vec<u8>, WebhookError> {
40 self.validate_with_hmac(req, body, time_bounds).await
41 }
42}
43
44impl WebhookHmac for StripeWebhook {
45 type MAC = Hmac<Sha256>;
46
47 fn secret_key(&self) -> &[u8] {
48 &self.secret_key
49 }
50
51 fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError> {
52 let header = try_outcome!(self.get_header(req, SIG_HEADER, None));
53 let mut signatures = Vec::new();
54 for hex_sig in header.split(',').filter_map(|s| s.strip_prefix("v1=")) {
55 match hex::decode(hex_sig) {
56 Ok(bytes) => signatures.push(bytes),
57 Err(_) => {
58 return Outcome::Error((
59 Status::BadRequest,
60 WebhookError::InvalidHeader(format!(
61 "Signature in {SIG_HEADER} header was not valid hex: '{hex_sig}'"
62 )),
63 ));
64 }
65 };
66 }
67
68 Outcome::Success(signatures)
69 }
70
71 fn body_prefix(
72 &self,
73 req: &Request<'_>,
74 time_bounds: (u32, u32),
75 ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
76 let sig_header = try_outcome!(self.get_header(req, SIG_HEADER, None));
77 let Some(timestamp) = sig_header
78 .split(',')
79 .find_map(|part| part.strip_prefix("t="))
80 else {
81 return Outcome::Error((
82 Status::BadRequest,
83 WebhookError::InvalidHeader(format!(
84 "Did not find timestamp in header: '{sig_header}'"
85 )),
86 ));
87 };
88 try_outcome!(self.validate_timestamp(timestamp, time_bounds));
89
90 let prefix = [timestamp.as_bytes(), b"."].concat();
91 Outcome::Success(Some(prefix))
92 }
93}