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)]
38pub struct Signer {
39 secret: Vec<u8>,
40}
41
42impl std::fmt::Debug for Signer {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("Signer")
45 .field("secret", &"[REDACTED]")
46 .finish()
47 }
48}
49
50impl Signer {
51 pub fn new(secret: &str) -> Self {
59 let decoded = BASE64_URL_SAFE_NO_PAD
60 .decode(secret)
61 .or_else(|_| URL_SAFE.decode(secret))
62 .or_else(|_| STANDARD.decode(secret))
63 .unwrap_or_else(|_| secret.as_bytes().to_vec());
64
65 Self { secret: decoded }
66 }
67
68 pub fn from_raw(secret: &str) -> Self {
70 Self {
71 secret: secret.as_bytes().to_vec(),
72 }
73 }
74
75 pub fn sign(&self, message: &str, format: Base64Format) -> Result<String, String> {
81 let mut mac = Hmac::<Sha256>::new_from_slice(&self.secret)
82 .map_err(|e| format!("Failed to create HMAC: {}", e))?;
83
84 mac.update(message.as_bytes());
85 let result = mac.finalize();
86
87 let signature = match format {
88 Base64Format::UrlSafe => {
89 let sig = STANDARD.encode(result.into_bytes());
91 sig.replace('+', "-").replace('/', "_")
92 }
93 Base64Format::Standard => STANDARD.encode(result.into_bytes()),
94 };
95
96 Ok(signature)
97 }
98
99 pub fn create_message(timestamp: u64, method: &str, path: &str, body: Option<&str>) -> String {
103 format!("{}{}{}{}", timestamp, method, path, body.unwrap_or(""))
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_current_timestamp() {
113 let ts = current_timestamp();
114 assert!(ts > 1_600_000_000);
116 }
117
118 #[test]
119 fn test_signer_new() {
120 let secret = "c2VjcmV0"; let signer = Signer::new(secret);
123 assert_eq!(signer.secret, b"secret");
124 }
125
126 #[test]
127 fn test_signer_from_raw() {
128 let signer = Signer::from_raw("secret");
129 assert_eq!(signer.secret, b"secret");
130 }
131
132 #[test]
133 fn test_sign_url_safe() {
134 let secret = "c2VjcmV0"; let signer = Signer::new(secret);
136
137 let message = Signer::create_message(1234567890, "GET", "/api/test", None);
138 let signature = signer.sign(&message, Base64Format::UrlSafe).unwrap();
139
140 assert!(!signature.contains('+'));
142 assert!(!signature.contains('/'));
143 }
144
145 #[test]
146 fn test_sign_standard() {
147 let secret = "c2VjcmV0"; let signer = Signer::new(secret);
149
150 let message = Signer::create_message(1234567890, "GET", "/api/test", None);
151 let signature = signer.sign(&message, Base64Format::Standard).unwrap();
152
153 assert!(!signature.is_empty());
155 }
156
157 #[test]
158 fn test_create_message() {
159 let msg = Signer::create_message(1234567890, "GET", "/api/test", None);
160 assert_eq!(msg, "1234567890GET/api/test");
161
162 let msg_with_body =
163 Signer::create_message(1234567890, "POST", "/api/test", Some(r#"{"key":"value"}"#));
164 assert_eq!(msg_with_body, r#"1234567890POST/api/test{"key":"value"}"#);
165 }
166
167 #[test]
168 fn test_signer_debug_redacts_secret() {
169 let signer = Signer::new("c2VjcmV0");
171 let debug_output = format!("{:?}", signer);
172 assert!(
173 debug_output.contains("[REDACTED]"),
174 "Debug output should contain [REDACTED]: {}",
175 debug_output
176 );
177 assert!(
179 !debug_output.contains("c2VjcmV0"),
180 "Debug output should not contain the base64 secret: {}",
181 debug_output
182 );
183 assert!(
184 !debug_output.contains("115, 101"),
185 "Debug output should not contain decoded secret bytes: {}",
186 debug_output
187 );
188 }
189}