rusty_common/auth/
hmac.rs1use crate::SmartString;
4use crate::{CommonError, Result};
5use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
6use hex;
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9use sha2::Sha512;
10
11type HmacSha256 = Hmac<Sha256>;
12type HmacSha512 = Hmac<Sha512>;
13
14pub fn generate_hmac_signature(secret: &str, message: &str) -> Result<SmartString> {
16 let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
17 .map_err(|e| CommonError::Auth(format!("Invalid key length: {e}").into()))?;
18 mac.update(message.as_bytes());
19 let result = mac.finalize();
20 Ok(hex::encode(result.into_bytes()).into())
21}
22
23pub fn generate_hmac_sha512_base64(secret: &str, message: &str) -> Result<SmartString> {
25 let mut mac = HmacSha512::new_from_slice(secret.as_bytes())
26 .map_err(|e| CommonError::Auth(format!("Invalid key length: {e}").into()))?;
27 mac.update(message.as_bytes());
28 let result = mac.finalize();
29 Ok(BASE64.encode(result.into_bytes()).into())
30}
31
32pub fn build_sorted_query_smartstring(params: &[(&str, &str)]) -> Result<SmartString> {
34 if params.is_empty() {
35 return Err(CommonError::InvalidParameter(
36 "Missing query parameter key or value".into(),
37 ));
38 }
39
40 let mut sorted_params = params.to_vec();
41 sorted_params.sort_by_key(|&(k, _)| k);
42
43 let query_string = sorted_params
44 .into_iter()
45 .map(|(k, v)| format!("{k}={v}"))
46 .collect::<Vec<_>>()
47 .join("&");
48
49 Ok(query_string.into())
50}
51
52#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
66 fn test_hmac_sha256_known_test_vectors() {
67 let key = "Jefe";
69 let data = "what do ya want for nothing?";
70 let expected = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843";
72
73 let result = generate_hmac_signature(key, data).unwrap();
74 assert_eq!(result.as_str(), expected, "HMAC-SHA256 test vector failed");
75 }
76
77 #[test]
79 fn test_hmac_sha256_bithumb_style() {
80 let secret = "test_secret_key_for_bithumb_api";
82 let message = "symbol=BTC_KRW&side=buy&type=limit&qty=0.001&price=84000000";
83 let _expected = "8c4cb5c3b3c8e5f6d4a9b2c7e8f1a5d9c2b6a3f7e4d8c1b5a9e2f6d3c7b4a8e5";
84
85 let result = generate_hmac_signature(secret, message).unwrap();
86
87 assert_eq!(
89 result.len(),
90 64,
91 "HMAC-SHA256 should produce 64 hex characters"
92 );
93 assert!(
94 result.chars().all(|c| c.is_ascii_hexdigit()),
95 "Result should be valid hex"
96 );
97
98 let result2 = generate_hmac_signature(secret, message).unwrap();
100 assert_eq!(result, result2, "HMAC should be deterministic");
101 }
102
103 #[test]
105 fn test_hmac_sha256_empty_inputs() {
106 let result = generate_hmac_signature("test_key", "").unwrap();
108 assert_eq!(result.len(), 64);
109 assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
110
111 let result_empty_key = generate_hmac_signature("", "test_message").unwrap();
113 assert_eq!(result_empty_key.len(), 64);
114 assert_ne!(
115 result, result_empty_key,
116 "Different keys should produce different signatures"
117 );
118 }
119
120 #[test]
122 fn test_hmac_sha256_unicode_and_special_chars() {
123 let secret = "测试密钥🔑";
124 let message = "symbol=BTC/USDT&amount=1.5&price=$50,000.00";
125
126 let result = generate_hmac_signature(secret, message).unwrap();
127 assert_eq!(result.len(), 64);
128 assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
129
130 let result2 = generate_hmac_signature(secret, message).unwrap();
132 assert_eq!(result, result2);
133 }
134
135 #[test]
137 fn test_hmac_sha512_known_vectors() {
138 let secret = "test_secret_key";
140 let message = "The quick brown fox jumps over the lazy dog";
141
142 let result = generate_hmac_sha512_base64(secret, message).unwrap();
143
144 assert!(
146 result.len() >= 85 && result.len() <= 90,
147 "Base64 SHA512 should be ~88 characters"
148 );
149
150 assert!(
152 BASE64.decode(result.as_str()).is_ok(),
153 "Result should be valid base64"
154 );
155
156 let result2 = generate_hmac_sha512_base64(secret, message).unwrap();
158 assert_eq!(result, result2);
159 }
160
161 #[test]
163 fn test_hmac_sha512_bithumb_scenarios() {
164 let secret = "bithumb_secret_key_example";
166 let message = "currency=BTC&payment_currency=KRW&units=0.001&price=84000000&type=bid";
167
168 let result = generate_hmac_sha512_base64(secret, message).unwrap();
169
170 let decoded = BASE64.decode(result.as_str()).unwrap();
172 assert_eq!(
173 decoded.len(),
174 64,
175 "SHA512 output should be exactly 64 bytes"
176 );
177 }
178
179 #[test]
181 fn test_build_sorted_query_string() {
182 let params = [("zebra", "last"), ("alpha", "first"), ("beta", "second")];
184 let result = build_sorted_query_smartstring(¶ms).unwrap();
185 assert_eq!(result.as_str(), "alpha=first&beta=second&zebra=last");
186
187 let single = [("key", "value")];
189 let result_single = build_sorted_query_smartstring(&single).unwrap();
190 assert_eq!(result_single.as_str(), "key=value");
191
192 let special = [("symbol", "BTC/USDT"), ("amount", "1.5")];
194 let result_special = build_sorted_query_smartstring(&special).unwrap();
195 assert_eq!(result_special.as_str(), "amount=1.5&symbol=BTC/USDT");
196 }
197
198 #[test]
200 fn test_error_handling() {
201 let empty_result = build_sorted_query_smartstring(&[]);
203 assert!(empty_result.is_err());
204
205 let error = empty_result.unwrap_err();
207 assert!(error.to_string().contains("query parameter"));
208 }
209
210 #[test]
212 fn test_hmac_consistency() {
213 let secret = "consistency_test_key";
214 let message = "test_message_for_consistency";
215
216 let signatures: Vec<SmartString> = (0..10)
218 .map(|_| generate_hmac_signature(secret, message).unwrap())
219 .collect();
220
221 let first = &signatures[0];
223 for signature in &signatures[1..] {
224 assert_eq!(first, signature, "HMAC signatures must be consistent");
225 }
226 }
227
228 #[test]
230 fn test_hmac_sensitivity() {
231 let secret = "sensitivity_test_key";
232 let base_message = "base_message";
233
234 let base_signature = generate_hmac_signature(secret, base_message).unwrap();
235
236 let changed_message = "base_messag"; let changed_signature = generate_hmac_signature(secret, changed_message).unwrap();
239
240 assert_ne!(
241 base_signature, changed_signature,
242 "Small input changes should produce different signatures"
243 );
244
245 let changed_secret = "sensitivity_test_ke"; let changed_key_signature = generate_hmac_signature(changed_secret, base_message).unwrap();
248
249 assert_ne!(
250 base_signature, changed_key_signature,
251 "Small key changes should produce different signatures"
252 );
253 }
254
255 #[test]
257 fn test_hmac_performance() {
258 let secret = "performance_test_key";
259 let message = "performance_test_message_with_reasonable_length_for_typical_api_usage";
260
261 let start = std::time::Instant::now();
262 for _ in 0..1000 {
263 let _ = generate_hmac_signature(secret, message).unwrap();
264 }
265 let duration = start.elapsed();
266
267 assert!(
269 duration.as_millis() < 100,
270 "HMAC performance regression detected: {duration:?}"
271 );
272 }
273
274 #[test]
276 fn test_hmac_manual_verification() {
277 let secret = "key";
279 let message = "message";
280
281 let result = generate_hmac_signature(secret, message).unwrap();
282
283 let expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a";
285
286 assert_eq!(result.as_str(), expected, "HMAC manual verification failed");
287 assert_eq!(result.len(), 64, "Result should be 64 hex characters");
288 assert!(
289 result.chars().all(|c| c.is_ascii_hexdigit()),
290 "Result should be valid hex"
291 );
292 }
293
294 #[test]
296 fn test_legacy_compatibility() {
297 let secret = "test_secret";
298 let message = "test_message";
299 let hmac_sign = generate_hmac_signature(secret, message);
300 let hmac_sha512 = generate_hmac_sha512_base64(secret, message);
301 let sorted_query = build_sorted_query_smartstring(&[("key", "value")]);
302
303 assert!(hmac_sign.is_ok());
304 assert!(hmac_sha512.is_ok());
305 assert!(sorted_query.is_ok());
306 }
307}