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 .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 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 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 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}