rusher_core/
signature.rs

1use std::fmt;
2
3use hmac::{Hmac, Mac};
4use http::Request;
5use sha2::Sha256;
6use url::Url;
7
8use crate::{ChannelName, SocketId};
9
10#[derive(Debug, Clone)]
11pub struct Signature {
12    mac: Hmac<Sha256>,
13}
14
15impl Signature {
16    pub fn into_bytes(self) -> Vec<u8> {
17        self.mac.finalize().into_bytes().to_vec()
18    }
19
20    pub fn verify(self, signature: impl AsRef<[u8]>) -> bool {
21        self.mac.verify_slice(signature.as_ref()).is_ok()
22    }
23}
24
25impl fmt::LowerHex for Signature {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        for byte in self.clone().into_bytes() {
28            write!(f, "{:02x}", byte)?;
29        }
30        Ok(())
31    }
32}
33
34impl fmt::UpperHex for Signature {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        for byte in self.clone().into_bytes() {
37            write!(f, "{:02X}", byte)?;
38        }
39        Ok(())
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum SignatureError {
45    InvalidData,
46    InvalidSecret,
47}
48
49pub fn sign_request<T>(
50    secret: impl AsRef<[u8]>,
51    req: &Request<T>,
52) -> Result<Signature, SignatureError> {
53    let method = req.method().to_string().to_uppercase();
54    let path = req.uri().path();
55    let query_params = {
56        let url = Url::parse(&req.uri().to_string())
57            // The url might be a relative one, so we try adding a dummy schema and host,
58            // which is fine, since we only care about the query params at this point
59            .or_else(|_| Url::parse(&format!("http://example{}", req.uri())))
60            .map_err(|_| SignatureError::InvalidData)?;
61
62        let mut params = url
63            .query_pairs()
64            .map(|(k, v)| (k.to_lowercase(), v))
65            .filter(|(k, _)| k != "auth_signature")
66            .collect::<Vec<_>>();
67
68        params.sort_by_key(|(k, _)| k.clone());
69
70        params
71            .into_iter()
72            .map(|(k, v)| format!("{k}={v}"))
73            .collect::<Vec<_>>()
74            .join("&")
75    };
76
77    sign_data(secret, format!("{method}\n{path}\n{query_params}"))
78}
79
80pub fn sign_private_channel(
81    secret: impl AsRef<[u8]>,
82    socket_id: &SocketId,
83    channel: &ChannelName,
84) -> Result<Signature, SignatureError> {
85    sign_data(secret, format!("{socket_id}:{channel}"))
86}
87
88pub fn sign_user_data(
89    secret: impl AsRef<[u8]>,
90    socket_id: &SocketId,
91    user_data: &str,
92) -> Result<Signature, SignatureError> {
93    sign_data(secret, format!("{socket_id}::user::{user_data}"))
94}
95
96fn sign_data(
97    secret: impl AsRef<[u8]>,
98    data: impl AsRef<[u8]>,
99) -> Result<Signature, SignatureError> {
100    hmac::Hmac::<Sha256>::new_from_slice(secret.as_ref())
101        .map(|mac| mac.chain_update(data))
102        .map(|mac| Signature { mac })
103        .map_err(|_| SignatureError::InvalidSecret)
104}
105
106#[cfg(test)]
107mod tests {
108    use std::str::FromStr;
109
110    use http::method;
111
112    use super::*;
113
114    #[test]
115    fn test_sign_request() {
116        // Example took from the official docs:
117        // https://pusher.com/docs/channels/library_auth_reference/rest-api/#worked-authentication-example
118
119        let secret = "7ad3773142a6692b25b8";
120        let auth_key = "278d425bdf160c739803";
121        let auth_timestamp = "1353088179";
122        let auth_version = "1.0";
123        let body_md5 = "ec365a775a4cd0599faeb73354201b6f";
124        let auth_signature = "da454824c97ba181a32ccc17a72625ba02771f50b50e1e7430e47a1f3f457e6c";
125
126        let req = Request::builder()
127            .method(method::Method::POST)
128            .uri(format!("/apps/3/events?body_md5={body_md5}&auth_key={auth_key}&auth_version={auth_version}&auth_timestamp={auth_timestamp}"))
129            .body(r##"{"name":"foo","channels":["project-3"],"data":"{\"some\":\"data\"}"}'"##)
130            .unwrap();
131
132        let signature = sign_request(secret, &req).unwrap();
133
134        assert_eq!(auth_signature, format!("{:x}", signature));
135        assert!(signature.verify(hex::decode(auth_signature).unwrap()));
136    }
137
138    #[test]
139    fn test_sign_user() {
140        // Example took from the official docs:
141        // https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication
142
143        let secret = "7ad3773142a6692b25b8";
144        let data = r#"1234.1234::user::{"id":"12345"}"#;
145        let auth_signature = "4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba";
146
147        let signature = sign_data(secret, data).unwrap();
148
149        assert_eq!(auth_signature, format!("{:x}", signature));
150        assert!(signature.verify(hex::decode(auth_signature).unwrap()));
151    }
152
153    #[test]
154    fn test_sign_channel() {
155        // Example took from the official docs:
156        // https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#private-channel
157
158        let secret = "7ad3773142a6692b25b8";
159        let socket_id = SocketId(String::from("1234.1234"));
160        let channel = ChannelName::from_str("private-foobar").unwrap();
161        let auth_signature = "58df8b0c36d6982b82c3ecf6b4662e34fe8c25bba48f5369f135bf843651c3a4";
162
163        let signature = sign_private_channel(secret, &socket_id, &channel).unwrap();
164
165        assert_eq!(auth_signature, format!("{:x}", signature));
166        assert!(signature.verify(hex::decode(auth_signature).unwrap()));
167    }
168}