paddle_rust_sdk/
webhooks.rs

1//! # Helpers for validating webhook requests.
2
3use std::num::ParseIntError;
4use std::str::FromStr;
5
6use chrono::{prelude::*, Duration};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9
10use crate::error::{Error, SignatureError};
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// Maximum allowed age for a signature.
15///
16/// Signatures sent by Paddle contain the timestamp when they were generated. Pass this struct to [Paddle::unmarshal](crate::Paddle::unmarshal) to set the maximum allowed age for signatures.
17///
18/// [MaximumVariance::default] - signatures cannot be older than 5 seconds.
19pub struct MaximumVariance(pub Option<Duration>);
20
21impl MaximumVariance {
22    pub fn seconds(seconds: u64) -> Self {
23        Self(Some(Duration::seconds(seconds as i64)))
24    }
25}
26
27impl Default for MaximumVariance {
28    /// Default config - signatures cannot be older than 5 seconds.
29    fn default() -> Self {
30        Self(Some(Duration::seconds(5)))
31    }
32}
33
34pub struct Signature {
35    timestamp: DateTime<Utc>,
36    signature: Vec<u8>,
37}
38
39impl Signature {
40    pub fn verify(
41        &self,
42        request_body: impl AsRef<str>,
43        key: impl AsRef<str>,
44        maximum_variance: MaximumVariance,
45    ) -> Result<(), Error> {
46        if let Some(maximum_variance) = maximum_variance.0 {
47            if Utc::now() > self.timestamp + maximum_variance {
48                return Err(Error::PaddleSignature(SignatureError::MaxVarianceExceeded(
49                    maximum_variance,
50                )));
51            }
52        }
53
54        let signed_payload = format!("{}:{}", self.timestamp.format("%s"), request_body.as_ref());
55
56        let mut mac = HmacSha256::new_from_slice(key.as_ref().as_bytes())
57            .expect("HMAC can take key of any size");
58
59        mac.update(signed_payload.as_bytes());
60        mac.verify_slice(&self.signature)?;
61
62        Ok(())
63    }
64}
65
66impl FromStr for Signature {
67    type Err = crate::Error;
68
69    fn from_str(signature: &str) -> Result<Self, Self::Err> {
70        // ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151
71
72        if signature.is_empty() {
73            return Err(Error::PaddleSignature(SignatureError::Empty));
74        }
75
76        let signature_parts = signature.split(';').collect::<Vec<_>>();
77
78        if signature_parts.len() != 2 {
79            return Err(Error::PaddleSignature(SignatureError::InvalidFormat));
80        }
81
82        let mut timestamp = None;
83        let mut signature = None;
84
85        for part in signature_parts {
86            let key_value = part.split('=').collect::<Vec<_>>();
87
88            if key_value.len() != 2 {
89                return Err(Error::PaddleSignature(SignatureError::InvalidPartFormat));
90            }
91
92            if key_value[0] == "ts" {
93                timestamp = DateTime::from_timestamp(key_value[1].parse()?, 0);
94            }
95
96            if key_value[0] == "h1" {
97                signature = Some(key_value[1].to_string());
98            }
99        }
100
101        let Some((timestamp, signature)) = timestamp.zip(signature) else {
102            return Err(Error::PaddleSignature(SignatureError::InvalidFormat));
103        };
104
105        Ok(Self {
106            timestamp,
107            signature: decode_hex(&signature)?,
108        })
109    }
110}
111
112fn decode_hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
113    (0..s.len())
114        .step_by(2)
115        .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
116        .collect()
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn parsing_correctly() {
125        let signature_str =
126            "ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151";
127
128        let sig: Signature = signature_str.parse().expect("To parse correctly");
129    }
130
131    #[test]
132    fn malformed_parameters() {
133        let signature_str =
134            "ts=1671552777h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151";
135        assert!(signature_str.parse::<Signature>().is_err());
136
137        let signature_str =
138            "ts=1671552a777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151";
139        assert!(signature_str.parse::<Signature>().is_err());
140    }
141}