Skip to main content

bybit/
crypto.rs

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