1use base64::{
6 engine::general_purpose::{STANDARD, URL_SAFE},
7 prelude::BASE64_URL_SAFE_NO_PAD,
8 Engine,
9};
10use hmac::{Hmac, Mac};
11use sha2::Sha256;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14pub fn current_timestamp() -> u64 {
19 SystemTime::now()
20 .duration_since(UNIX_EPOCH)
21 .unwrap_or_default()
22 .as_secs()
23}
24
25#[derive(Clone, Copy, Debug)]
27pub enum Base64Format {
28 UrlSafe,
30 Standard,
32}
33
34#[derive(Clone, Debug)]
38pub struct Signer {
39 secret: Vec<u8>,
40}
41
42impl Signer {
43 pub fn new(secret: &str) -> Result<Self, String> {
51 let decoded = BASE64_URL_SAFE_NO_PAD
52 .decode(secret)
53 .or_else(|_| URL_SAFE.decode(secret))
54 .or_else(|_| STANDARD.decode(secret))
55 .unwrap_or_else(|_| secret.as_bytes().to_vec());
56
57 Ok(Self { secret: decoded })
58 }
59
60 pub fn from_raw(secret: &str) -> Self {
62 Self {
63 secret: secret.as_bytes().to_vec(),
64 }
65 }
66
67 pub fn sign(&self, message: &str, format: Base64Format) -> Result<String, String> {
73 let mut mac = Hmac::<Sha256>::new_from_slice(&self.secret)
74 .map_err(|e| format!("Failed to create HMAC: {}", e))?;
75
76 mac.update(message.as_bytes());
77 let result = mac.finalize();
78
79 let signature = match format {
80 Base64Format::UrlSafe => {
81 let sig = STANDARD.encode(result.into_bytes());
83 sig.replace('+', "-").replace('/', "_")
84 }
85 Base64Format::Standard => STANDARD.encode(result.into_bytes()),
86 };
87
88 Ok(signature)
89 }
90
91 pub fn create_message(timestamp: u64, method: &str, path: &str, body: Option<&str>) -> String {
95 format!("{}{}{}{}", timestamp, method, path, body.unwrap_or(""))
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_current_timestamp() {
105 let ts = current_timestamp();
106 assert!(ts > 1_600_000_000);
108 }
109
110 #[test]
111 fn test_signer_new() {
112 let secret = "c2VjcmV0"; let signer = Signer::new(secret).unwrap();
115 assert_eq!(signer.secret, b"secret");
116 }
117
118 #[test]
119 fn test_signer_from_raw() {
120 let signer = Signer::from_raw("secret");
121 assert_eq!(signer.secret, b"secret");
122 }
123
124 #[test]
125 fn test_sign_url_safe() {
126 let secret = "c2VjcmV0"; let signer = Signer::new(secret).unwrap();
128
129 let message = Signer::create_message(1234567890, "GET", "/api/test", None);
130 let signature = signer.sign(&message, Base64Format::UrlSafe).unwrap();
131
132 assert!(!signature.contains('+'));
134 assert!(!signature.contains('/'));
135 }
136
137 #[test]
138 fn test_sign_standard() {
139 let secret = "c2VjcmV0"; let signer = Signer::new(secret).unwrap();
141
142 let message = Signer::create_message(1234567890, "GET", "/api/test", None);
143 let signature = signer.sign(&message, Base64Format::Standard).unwrap();
144
145 assert!(!signature.is_empty());
147 }
148
149 #[test]
150 fn test_create_message() {
151 let msg = Signer::create_message(1234567890, "GET", "/api/test", None);
152 assert_eq!(msg, "1234567890GET/api/test");
153
154 let msg_with_body =
155 Signer::create_message(1234567890, "POST", "/api/test", Some(r#"{"key":"value"}"#));
156 assert_eq!(msg_with_body, r#"1234567890POST/api/test{"key":"value"}"#);
157 }
158}