rocket_webhook/webhooks/built_in/
slack.rs

1use 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
11/// # Slack webhook
12/// Looks for hex signature in `X-Slack-Signature` header, with a 'v0=' prefix,
13/// as well as the timestamp from `X-Slack-Request-Timestamp`.
14///
15/// Signature is a digest of `v0:<timestamp>:<body>`
16///
17/// [Slack docs](https://docs.slack.dev/authentication/verifying-requests-from-slack/#validating-a-request)
18pub struct SlackWebhook {
19    secret_key: Zeroizing<Vec<u8>>,
20}
21
22impl SlackWebhook {
23    /// Instantiate with the secret key
24    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
31impl Webhook for SlackWebhook {
32    async fn validate_body(
33        &self,
34        req: &Request<'_>,
35        body: impl AsyncRead + Unpin + Send + Sync,
36        time_bounds: (u32, u32),
37    ) -> Outcome<'_, Vec<u8>, WebhookError> {
38        self.validate_with_hmac(req, body, time_bounds).await
39    }
40}
41
42impl WebhookHmac for SlackWebhook {
43    type MAC = Hmac<Sha256>;
44
45    fn secret_key(&self) -> &[u8] {
46        &self.secret_key
47    }
48
49    fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError> {
50        let sig_header = try_outcome!(self.get_header(req, "X-Slack-Signature", Some("v0=")));
51        match hex::decode(sig_header) {
52            Ok(bytes) => Outcome::Success(vec![bytes]),
53            Err(_) => Outcome::Error((
54                Status::BadRequest,
55                WebhookError::InvalidHeader(format!(
56                    "X-Slack-Signature header was not valid hex: '{sig_header}'"
57                )),
58            )),
59        }
60    }
61
62    fn body_prefix(
63        &self,
64        req: &Request<'_>,
65        time_bounds: (u32, u32),
66    ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
67        let timestamp = try_outcome!(self.get_header(req, "X-Slack-Request-Timestamp", None));
68        try_outcome!(self.validate_timestamp(timestamp, time_bounds));
69
70        let prefix = [b"v0:", timestamp.as_bytes(), b":"].concat();
71        Outcome::Success(Some(prefix))
72    }
73}