rusty_common/auth/
hmac.rs

1//! HMAC utility functions for authentication
2
3use 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
14/// Generate HMAC-SHA256 signature and return as hex SmartString
15pub 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
23/// Generate HMAC-SHA512 signature and return as base64 SmartString
24pub 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
32/// Build a sorted query SmartString from key-value pairs
33pub 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/// Comprehensive test suite for HMAC functions with known test vectors
53///
54/// Test vectors are derived from RFC 4231 and exchange-specific examples
55/// to ensure security correctness and compliance.
56///
57/// ```sh
58/// cargo test hmac
59/// ```
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    /// Test HMAC-SHA256 with known test vectors for security validation
65    #[test]
66    fn test_hmac_sha256_known_test_vectors() {
67        // Test with simple string key and data
68        let key = "Jefe";
69        let data = "what do ya want for nothing?";
70        // Expected result can be verified with: echo -n "what do ya want for nothing?" | openssl dgst -sha256 -hmac "Jefe"
71        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 HMAC-SHA256 with Bithumb-style parameters (exchange-specific validation)
78    #[test]
79    fn test_hmac_sha256_bithumb_style() {
80        // Realistic Bithumb API parameters
81        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        // Verify format: 64 character hex string
88        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        // Verify consistency - same input should produce same output
99        let result2 = generate_hmac_signature(secret, message).unwrap();
100        assert_eq!(result, result2, "HMAC should be deterministic");
101    }
102
103    /// Test HMAC-SHA256 with empty inputs (edge case validation)
104    #[test]
105    fn test_hmac_sha256_empty_inputs() {
106        // Empty message with key
107        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        // Empty key (should still work but be different)
112        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 HMAC-SHA256 with Unicode and special characters
121    #[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        // Verify consistency with Unicode
131        let result2 = generate_hmac_signature(secret, message).unwrap();
132        assert_eq!(result, result2);
133    }
134
135    /// Test HMAC-SHA512 with known test vectors
136    #[test]
137    fn test_hmac_sha512_known_vectors() {
138        // Test case derived from RFC examples
139        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        // Verify base64 format and length (SHA512 = 64 bytes = ~88 base64 chars with padding)
145        assert!(
146            result.len() >= 85 && result.len() <= 90,
147            "Base64 SHA512 should be ~88 characters"
148        );
149
150        // Verify it's valid base64
151        assert!(
152            BASE64.decode(result.as_str()).is_ok(),
153            "Result should be valid base64"
154        );
155
156        // Verify consistency
157        let result2 = generate_hmac_sha512_base64(secret, message).unwrap();
158        assert_eq!(result, result2);
159    }
160
161    /// Test HMAC-SHA512 with Bithumb-specific scenarios
162    #[test]
163    fn test_hmac_sha512_bithumb_scenarios() {
164        // Simulate Bithumb order parameters
165        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        // Verify the decoded length is exactly 64 bytes (SHA512 output)
171        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 sorted query string building (used in authentication)
180    #[test]
181    fn test_build_sorted_query_string() {
182        // Test basic sorting
183        let params = [("zebra", "last"), ("alpha", "first"), ("beta", "second")];
184        let result = build_sorted_query_smartstring(&params).unwrap();
185        assert_eq!(result.as_str(), "alpha=first&beta=second&zebra=last");
186
187        // Test single parameter
188        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        // Test with special characters that need encoding context
193        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 error handling for invalid inputs
199    #[test]
200    fn test_error_handling() {
201        // Empty parameters should error
202        let empty_result = build_sorted_query_smartstring(&[]);
203        assert!(empty_result.is_err());
204
205        // Verify error message is meaningful
206        let error = empty_result.unwrap_err();
207        assert!(error.to_string().contains("query parameter"));
208    }
209
210    /// Test HMAC consistency across multiple calls (security requirement)
211    #[test]
212    fn test_hmac_consistency() {
213        let secret = "consistency_test_key";
214        let message = "test_message_for_consistency";
215
216        // Generate multiple signatures
217        let signatures: Vec<SmartString> = (0..10)
218            .map(|_| generate_hmac_signature(secret, message).unwrap())
219            .collect();
220
221        // All should be identical
222        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 HMAC sensitivity to input changes (security requirement)
229    #[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        // Small change in message should produce completely different signature
237        let changed_message = "base_messag"; // Removed one character
238        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        // Small change in key should produce completely different signature
246        let changed_secret = "sensitivity_test_ke"; // Removed one character
247        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    /// Performance benchmark test (optional, for monitoring)
256    #[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        // Should complete 1000 HMAC operations in reasonable time (< 100ms)
268        assert!(
269            duration.as_millis() < 100,
270            "HMAC performance regression detected: {duration:?}"
271        );
272    }
273
274    /// Test vector validation with manually computed HMAC (additional security check)
275    #[test]
276    fn test_hmac_manual_verification() {
277        // Simple test case that can be verified manually or with external tools
278        let secret = "key";
279        let message = "message";
280
281        let result = generate_hmac_signature(secret, message).unwrap();
282
283        // Expected value verified with: echo -n "message" | openssl dgst -sha256 -hmac "key"
284        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    /// Legacy compatibility test (keep existing behavior)
295    #[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}