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