Skip to main content

indodax_cli/
auth.rs

1use hmac::{Hmac, Mac};
2use sha2::Sha512;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::errors::IndodaxError;
7
8#[derive(Debug)]
9pub struct Signer {
10    api_key: String,
11    secret_key: String,
12    last_nonce: AtomicU64,
13}
14
15impl Signer {
16    pub fn new(api_key: &str, secret_key: &str) -> Self {
17        Self {
18            api_key: api_key.to_string(),
19            secret_key: secret_key.to_string(),
20            last_nonce: AtomicU64::new(0),
21        }
22    }
23
24    pub fn api_key(&self) -> &str {
25        &self.api_key
26    }
27
28    pub fn next_nonce_str(&self) -> String {
29        self.next_nonce().to_string()
30    }
31
32    fn next_nonce(&self) -> u64 {
33        let now = SystemTime::now()
34            .duration_since(UNIX_EPOCH)
35            .unwrap_or_default()
36            .as_millis() as u64;
37        loop {
38            let prev = self.last_nonce.load(Ordering::Acquire);
39            let next = if now > prev { now } else { prev + 1 };
40            if self
41                .last_nonce
42                .compare_exchange(prev, next, Ordering::Release, Ordering::Relaxed)
43                .is_ok()
44            {
45                return next;
46            }
47        }
48    }
49
50    pub fn now_millis() -> u64 {
51        SystemTime::now()
52            .duration_since(UNIX_EPOCH)
53            .unwrap_or_default()
54            .as_millis() as u64
55    }
56
57    pub fn sign_v1(&self, payload: &str) -> Result<(String, String), IndodaxError> {
58        let signature = self.hmac_sha512(payload, &self.secret_key)?;
59        let encoded_sign = hex::encode(signature);
60
61        Ok((payload.to_string(), encoded_sign))
62    }
63
64    pub fn sign_v2(&self, query_string: &str, _timestamp: u64) -> Result<String, IndodaxError> {
65        let signature = self.hmac_sha512(query_string, &self.secret_key)?;
66        Ok(hex::encode(signature))
67    }
68
69    fn hmac_sha512(&self, data: &str, key: &str) -> Result<Vec<u8>, IndodaxError> {
70        let mut mac = Hmac::<Sha512>::new_from_slice(key.as_bytes())
71            .map_err(|e| IndodaxError::Other(format!("HMAC initialization failed: {}", e)))?;
72        mac.update(data.as_bytes());
73        Ok(mac.finalize().into_bytes().to_vec())
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_signer_new() {
83        let signer = Signer::new("api_key", "secret_key");
84        assert_eq!(signer.api_key(), "api_key");
85    }
86
87    #[test]
88    fn test_signer_api_key() {
89        let signer = Signer::new("my_api_key", "my_secret");
90        assert_eq!(signer.api_key(), "my_api_key");
91    }
92
93    #[test]
94    fn test_signer_next_nonce_str() {
95        let signer = Signer::new("key", "secret");
96        let nonce = signer.next_nonce_str();
97        assert!(!nonce.is_empty());
98        // Nonce should be a number
99        assert!(nonce.parse::<u64>().is_ok());
100    }
101
102    #[test]
103    fn test_signer_next_nonce_is_increasing() {
104        let signer = Signer::new("key", "secret");
105        let nonce1 = signer.next_nonce();
106        let nonce2 = signer.next_nonce();
107        // Nonces should be increasing (or at least not decreasing)
108        assert!(nonce2 >= nonce1);
109    }
110
111    #[test]
112    fn test_signer_now_millis() {
113        let millis = Signer::now_millis();
114        assert!(millis > 0);
115        // Should be around current time in millis
116        assert!(millis > 1_000_000_000_000); // After year 2001
117    }
118
119    #[test]
120    fn test_signer_sign_v1() {
121        let signer = Signer::new("key", "secret");
122        let (payload, signature) = signer.sign_v1("method=test&nonce=123").unwrap();
123        assert_eq!(payload, "method=test&nonce=123");
124        assert!(!signature.is_empty());
125        // Signature should be hex encoded
126        assert!(hex::decode(&signature).is_ok());
127    }
128
129    #[test]
130    fn test_signer_sign_v1_different_secrets() {
131        let signer1 = Signer::new("key", "secret1");
132        let signer2 = Signer::new("key", "secret2");
133        let (_payload, sig1) = signer1.sign_v1("test").unwrap();
134        let (_payload2, sig2) = signer2.sign_v1("test").unwrap();
135        assert_ne!(sig1, sig2);
136    }
137
138    #[test]
139    fn test_signer_sign_v2() {
140        let signer = Signer::new("key", "secret");
141        let signature = signer.sign_v2("param1=value1", 1234567890).unwrap();
142        assert!(!signature.is_empty());
143        // Signature should be hex encoded
144        assert!(hex::decode(&signature).is_ok());
145    }
146
147    #[test]
148    fn test_signer_sign_v2_with_timestamp() {
149        let signer = Signer::new("key", "secret");
150        let query_string = "symbol=BTCIDR";
151        let timestamp = 1234567890000u64;
152        let signature = signer.sign_v2(query_string, timestamp).unwrap();
153        
154        // Verify the payload that was signed
155        let _expected_payload = format!("{}&timestamp={}&recvWindow=10000", query_string, timestamp);
156        let _decoded = hex::decode(&signature).unwrap();
157        // We can't easily verify the HMAC without knowing the secret, but we can verify it's valid hex
158        assert!(!signature.is_empty());
159    }
160
161    #[test]
162    fn test_hmac_sha512_output_length() {
163        let signer = Signer::new("key", "secret");
164        let result = signer.hmac_sha512("test data", "secret").unwrap();
165        // SHA512 produces 64 bytes
166        assert_eq!(result.len(), 64);
167    }
168
169    #[test]
170    fn test_signer_multiple_signatures_different() {
171        let signer = Signer::new("key", "secret");
172        let (_, sig1) = signer.sign_v1("payload1").unwrap();
173        let (_, sig2) = signer.sign_v1("payload2").unwrap();
174        assert_ne!(sig1, sig2);
175    }
176}