Skip to main content

bybit/
crypto.rs

1use hmac::{Hmac, KeyInit, Mac};
2use sha2::Sha256;
3use std::fmt;
4
5use crate::Timestamp;
6
7#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct SensitiveString(String);
9
10impl fmt::Display for SensitiveString {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        write!(f, "REDACTED")
13    }
14}
15
16impl fmt::Debug for SensitiveString {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(f, "{:?}", "REDACTED")
19    }
20}
21
22impl std::ops::Deref for SensitiveString {
23    type Target = str;
24    fn deref(&self) -> &Self::Target {
25        &self.0
26    }
27}
28
29impl From<String> for SensitiveString {
30    fn from(s: String) -> Self {
31        SensitiveString(s)
32    }
33}
34
35impl From<&str> for SensitiveString {
36    fn from(s: &str) -> Self {
37        SensitiveString(s.to_string())
38    }
39}
40
41impl AsRef<str> for SensitiveString {
42    fn as_ref(&self) -> &str {
43        &self.0
44    }
45}
46
47impl SensitiveString {
48    pub fn expose(&self) -> &str {
49        &self.0
50    }
51}
52
53/// Generates HMAC using the given key and message.
54///
55/// # Arguments
56/// * `key` - Secret key for HMAC.
57/// * `message` - Message to authenticate.
58///
59/// # Returns
60/// A String containing the hex of HMAC digest.
61///
62/// # Example
63/// ```
64/// use bybit::hmac_sha256;
65/// let signature = hmac_sha256(b"my-secret-key", b"important message");
66/// println!("HMAC: {signature}");
67/// ```
68pub fn hmac_sha256(key: impl AsRef<[u8]>, message: impl AsRef<[u8]>) -> String {
69    let mut mac = Hmac::<Sha256>::new_from_slice(key.as_ref()).unwrap();
70    mac.update(message.as_ref());
71    let mac = mac.finalize().into_bytes();
72    hex::encode(mac)
73}
74
75type Timer = fn() -> Timestamp;
76
77#[derive(Debug)]
78pub struct Signer {
79    api_key: SensitiveString,
80    api_secret: SensitiveString,
81    /// Milliseconds.
82    recv_window: Timestamp,
83    timer: Timer,
84}
85
86impl Signer {
87    /// Create Signer instance.
88    pub fn new(
89        api_key: SensitiveString,
90        api_secret: SensitiveString,
91        recv_window: Timestamp,
92        timer: Option<Timer>,
93    ) -> Self {
94        Self {
95            api_key,
96            api_secret,
97            recv_window,
98            timer: timer.unwrap_or(timestamp),
99        }
100    }
101
102    /// Sign `s` with the current time of this signer's timer.
103    ///
104    /// return: (signature, timestamp)
105    pub fn sign(&self, s: &str) -> (String, String) {
106        let timestamp = (self.timer)();
107        (self.sign_at(s, timestamp), timestamp.to_string())
108    }
109
110    /// Sign `s` as if sent at `timestamp` (milliseconds), e.g. the local time
111    /// corrected by the offset to the server clock.
112    pub fn sign_at(&self, s: &str, timestamp: Timestamp) -> String {
113        let api_key = self.api_key.expose();
114        let api_secret = self.api_secret.expose();
115        let message = format!("{timestamp}{}{}{s}", api_key, self.recv_window);
116
117        hmac_sha256(api_secret, message)
118    }
119}
120
121/// Return milliseconds.
122pub fn timestamp() -> Timestamp {
123    std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as Timestamp
124}
125
126pub fn create_stream_signature(expires: Timestamp, api_secret: SensitiveString) -> String {
127    let api_secret = api_secret.expose();
128    let message = format!("GET/realtime{expires}");
129    hmac_sha256(api_secret, message)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn sign_get_request() {
138        let api_key = SensitiveString("API_KEY".to_string());
139        let api_secret = SensitiveString("API_SECRET".to_string());
140        let recv_window = 5000;
141        let signer = Signer::new(api_key, api_secret, recv_window, Some(|| 1658384314791));
142        let query = "category=option&symbol=BTC-29JUL22-25000-C";
143        let expected = "8b050bb7b9d53a91c42e16a7aa94a485d7aad9c7d08023a6249a184331a52ae7";
144
145        let (signature, timestamp) = signer.sign(query);
146
147        assert_eq!(signature, expected);
148        assert_eq!(timestamp.len(), 13);
149    }
150
151    #[test]
152    fn sign_post_request() {
153        let api_key = SensitiveString("API_KEY".to_string());
154        let api_secret = SensitiveString("API_SECRET".to_string());
155        let recv_window = 5000;
156        let signer = Signer::new(api_key, api_secret, recv_window, Some(|| 1658385579423));
157        let json = r#"{"category":"option"}"#;
158        let expected = "1b9b318f05208c9113f2612b2a6d76ca29427e6d8148937c03d6505f8c00804c";
159
160        let (signature, timestamp) = signer.sign(json);
161
162        assert_eq!(signature, expected);
163        assert_eq!(timestamp.len(), 13);
164    }
165
166    #[test]
167    fn sign_at_matches_sign_with_the_same_time() {
168        let signer = Signer::new(
169            "API_KEY".into(),
170            "API_SECRET".into(),
171            5000,
172            Some(|| 1658384314791),
173        );
174        let query = "category=option&symbol=BTC-29JUL22-25000-C";
175
176        let (signature, _) = signer.sign(query);
177
178        assert_eq!(signer.sign_at(query, 1658384314791), signature);
179    }
180}