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}