rocket_webhook/webhooks/interface/
hmac.rs1use hmac::{Mac, digest::KeyInit};
4use rocket::{
5 Request, data::Outcome, futures::StreamExt, http::Status, outcome::try_outcome,
6 tokio::io::AsyncRead,
7};
8use subtle::ConstantTimeEq;
9use tokio_util::io::ReaderStream;
10
11use crate::{
12 WebhookError,
13 webhooks::{Webhook, utils::body_size},
14};
15
16pub trait WebhookHmac: Webhook {
18 type MAC: Mac + KeyInit + Send;
20
21 fn secret_key(&self) -> &[u8];
23
24 fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError>;
27
28 #[allow(unused_variables)]
31 fn body_prefix(
32 &self,
33 req: &Request<'_>,
34 time_bounds: (u32, u32),
35 ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
36 Outcome::Success(None)
37 }
38
39 #[allow(unused_variables)]
42 fn body_suffix(
43 &self,
44 req: &Request<'_>,
45 time_bounds: (u32, u32),
46 ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
47 Outcome::Success(None)
48 }
49
50 fn validate_with_hmac(
53 &self,
54 req: &Request<'_>,
55 body: impl AsyncRead + Unpin + Send + Sync,
56 time_bounds: (u32, u32),
57 ) -> impl Future<Output = Outcome<'_, Vec<u8>, WebhookError>> + Send + Sync
58 where
59 Self: Sync,
60 Self::MAC: Sync,
61 {
62 async move {
63 let expected_signatures = try_outcome!(self.expected_signatures(req));
65
66 let key = self.secret_key();
68 let mut mac = <<Self as WebhookHmac>::MAC as hmac::Mac>::new_from_slice(key)
69 .expect("HMAC should take any key length");
70
71 if let Some(prefix) = try_outcome!(self.body_prefix(req, time_bounds)) {
73 mac.update(&prefix);
74 }
75
76 let mut body_stream = ReaderStream::new(body);
78 let mut raw_body = Vec::with_capacity(body_size(req.headers()).unwrap_or(512));
79 while let Some(chunk_result) = body_stream.next().await {
80 match chunk_result {
81 Ok(chunk_bytes) => {
82 mac.update(&chunk_bytes);
83 raw_body.extend_from_slice(&chunk_bytes);
84 }
85 Err(e) => {
86 return Outcome::Error((Status::BadRequest, WebhookError::Read(e)));
87 }
88 }
89 }
90
91 if let Some(suffix) = try_outcome!(self.body_suffix(req, time_bounds)) {
93 mac.update(&suffix);
94 }
95
96 let body_sig = mac.finalize().into_bytes();
98 for signature in expected_signatures {
99 if body_sig.ct_eq(&signature).into() {
100 return Outcome::Success(raw_body);
101 }
102 }
103 return Outcome::Error((
104 Status::Unauthorized,
105 WebhookError::Signature("HMAC didn't match any provided signature".into()),
106 ));
107 }
108 }
109}