Skip to main content

brokapi_bybit_protocol/
sign.rs

1//! The signature of a request: HMAC-SHA256, written as lower case hex.
2//!
3//! A REST request signs `timestamp + api_key + recv_window + payload`, where the payload is the query string of a GET
4//! or the JSON body of a POST, exactly as sent. The WebSocket login signs `GET/realtime` plus the `expires` time.
5
6use ring::hmac;
7
8/// HMAC-SHA256 of `payload` with `secret`, as lower case hex.
9///
10/// # Examples
11///
12/// The test vector 2 of RFC 4231:
13///
14/// ```
15/// use brokapi_bybit_protocol::sign::sign;
16///
17/// assert_eq!(
18///     sign("Jefe", "what do ya want for nothing?"),
19///     "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
20/// );
21/// ```
22pub fn sign(secret: &str, payload: &str) -> String {
23    let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes());
24    let tag = hmac::sign(&key, payload.as_bytes());
25    let mut out = String::with_capacity(64);
26    for byte in tag.as_ref() {
27        out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
28        out.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0'));
29    }
30    out
31}
32
33/// The string a REST request signs.
34///
35/// # Examples
36///
37/// ```
38/// use brokapi_bybit_protocol::sign::rest_payload;
39///
40/// // The example of the documentation: a GET with a query.
41/// assert_eq!(
42///     rest_payload(1658384314791, "XXXXXXXXXX", 5000, "category=option&symbol=BTC-29JUL22-25000-C"),
43///     "1658384314791XXXXXXXXXX5000category=option&symbol=BTC-29JUL22-25000-C"
44/// );
45/// ```
46pub fn rest_payload(timestamp_ms: i64, api_key: &str, recv_window_ms: u64, body: &str) -> String {
47    format!("{timestamp_ms}{api_key}{recv_window_ms}{body}")
48}
49
50/// The `args` of the WebSocket login: the key, the `expires` time (milliseconds, in the future) and the signature of
51/// `GET/realtime{expires}`.
52///
53/// # Examples
54///
55/// ```
56/// use brokapi_bybit_protocol::sign::ws_auth_args;
57///
58/// let args = ws_auth_args("key", "secret", 1662350400000);
59/// assert_eq!(args[0], "key");
60/// assert_eq!(args[1], "1662350400000");
61/// assert_eq!(args[2].len(), 64);
62/// ```
63pub fn ws_auth_args(api_key: &str, api_secret: &str, expires_ms: i64) -> [String; 3] {
64    [
65        api_key.to_string(),
66        expires_ms.to_string(),
67        sign(api_secret, &format!("GET/realtime{expires_ms}")),
68    ]
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn hex_is_lower_case_and_64_long() {
77        let s = sign("secret", "payload");
78        assert_eq!(s.len(), 64);
79        assert!(
80            s.chars()
81                .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
82        );
83    }
84
85    #[test]
86    fn a_post_signs_its_body_text() {
87        let body = r#"{"category":"linear"}"#;
88        let a = rest_payload(1, "k", 5000, body);
89        assert_eq!(a, r#"1k5000{"category":"linear"}"#);
90        assert_ne!(sign("s", &a), sign("s", &rest_payload(2, "k", 5000, body)));
91    }
92}