Skip to main content

indodax_cli/
auth.rs

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