tsar_sdk/structs/
client.rs

1use super::user::User;
2use crate::errors::TsarError;
3use base64::prelude::*;
4use hardware_id::get_id;
5use p256::{
6    ecdsa::{signature::Verifier, Signature, VerifyingKey},
7    pkcs8::DecodePublicKey,
8};
9use reqwest::StatusCode;
10use rsntp::SntpClient;
11use serde::de::DeserializeOwned;
12use serde::Deserialize;
13use serde_json::Value;
14use sha2::Digest;
15use sha2::Sha256;
16use std::env::current_exe;
17use std::fs::File;
18use std::io::Read;
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21/// TSAR Client. Used to interact with the TSAR API.
22#[derive(Debug)]
23pub struct Client {
24    /// The ID of your TSAR app. Should be in UUID format: 00000000-0000-0000-0000-000000000000
25    pub app_id: String,
26    /// The client decryption key for your TSAR app. Should be in base64 format. Always starts with "MFk..."
27    pub client_key: String,
28    /// The hostname of your app's dashboard.
29    pub dashboard_hostname: String,
30}
31
32/// TSAR Client options. Pass this into the `new()` function of the TSAR Client.
33#[derive(Debug)]
34pub struct ClientParams {
35    /// The ID of your TSAR app. Should be in UUID format: 00000000-0000-0000-0000-000000000000
36    pub app_id: String,
37    /// The client decryption key for your TSAR app. Should be in base64 format. Always starts with "MFk..."
38    pub client_key: String,
39}
40
41/// TSAR Client options. Pass this into the `new()` function of the TSAR Client.
42#[derive(Debug)]
43pub struct AuthParams {
44    /// Whether authenticate() should automatically open the user's browser when auth fails.
45    /// Disable this when using authenticate() more than once or in loops so that you dont spam the user's browser with tabs.
46    pub open_browser: bool,
47}
48
49impl Default for AuthParams {
50    fn default() -> Self {
51        Self { open_browser: true }
52    }
53}
54
55/// Data returned by the TSAR API when initializing.
56#[derive(Deserialize)]
57struct InitializeReturnData {
58    dashboard_hostname: String,
59}
60
61impl Client {
62    /// Utility for getting a user's HWID across all platforms.
63    pub fn get_hwid() -> Result<String, TsarError> {
64        get_id().or(Err(TsarError::FailedToGetHWID))
65    }
66
67    /// Get the hash of the current binary
68    pub fn get_hash() -> Result<String, TsarError> {
69        let current_exe = current_exe().or(Err(TsarError::FailedToGetHash))?;
70
71        let mut file = File::open(&current_exe).or(Err(TsarError::FailedToGetHash))?;
72        let mut hasher = Sha256::new();
73        let mut buffer = vec![0; 1024];
74
75        loop {
76            let count = file.read(&mut buffer).or(Err(TsarError::FailedToGetHash))?;
77            if count == 0 {
78                break;
79            }
80            hasher.update(&buffer[..count]);
81        }
82
83        let hash_result = hasher.finalize();
84        Ok(format!("{:x}", hash_result))
85    }
86
87    /// Creates a new TSAR client.
88    pub fn create(options: ClientParams) -> Result<Self, TsarError> {
89        // Verify that all options passed are in the right format
90        if options.app_id.len() != 36 {
91            return Err(TsarError::InvalidAppId);
92        }
93
94        if options.client_key.len() != 124 {
95            return Err(TsarError::InvalidClientKey);
96        }
97
98        // Make the init request
99        let params = vec![("app_id", options.app_id.as_str())];
100
101        let init_result = Client::encrypted_api_call::<InitializeReturnData>(
102            "initialize",
103            options.client_key.as_str(),
104            params,
105        )?;
106
107        Ok(Self {
108            app_id: options.app_id.to_string(),
109            client_key: options.client_key.to_string(),
110            dashboard_hostname: init_result.dashboard_hostname,
111        })
112    }
113
114    /// Attempts to authenticate the user.
115    /// If the user's HWID is not authorized, the function opens the user's default browser to prompt a login.
116    pub fn authenticate(&self, options: AuthParams) -> Result<User, TsarError> {
117        let params = vec![("app_id", self.app_id.as_str())];
118
119        let auth_result =
120            Client::encrypted_api_call::<User>("authenticate", &self.client_key, params);
121
122        let hwid = Self::get_hwid()?;
123
124        match auth_result {
125            Ok(user) => return Ok(user),
126            Err(TsarError::Unauthorized) => {
127                if options.open_browser {
128                    let _ =
129                        open::that(format!("https://{}/auth/{}", self.dashboard_hostname, hwid));
130                }
131                return Err(TsarError::Unauthorized);
132            }
133            Err(TsarError::HashUnauthorized) => {
134                if options.open_browser {
135                    let _ = open::that(format!(
136                        "https://{}/assets?outdated=true",
137                        self.dashboard_hostname
138                    ));
139                }
140                return Err(TsarError::HashUnauthorized);
141            }
142            Err(err) => return Err(err),
143        }
144    }
145
146    /// Query an endpoint from the TSAR API.
147    pub fn encrypted_api_call<T: DeserializeOwned>(
148        path: &str,
149        public_key: &str,
150        // The request's query parameters
151        params: Vec<(&str, &str)>,
152    ) -> Result<T, TsarError> {
153        let hwid = Client::get_hwid()?;
154        let hash = Client::get_hash()?;
155
156        // Convert client_key der to buffer
157        let pub_key_bytes = BASE64_STANDARD
158            .decode(public_key)
159            .or(Err(TsarError::InvalidClientKey))
160            .unwrap();
161
162        // Build public key from buffer
163        let pub_key: VerifyingKey =
164            VerifyingKey::from_public_key_der(pub_key_bytes[..].try_into().unwrap())
165                .or(Err(TsarError::InvalidClientKey))?;
166
167        // Append a / to path if it does not start with one
168        let path = if path.starts_with('/') {
169            path.to_string()
170        } else {
171            format!("/{}", path)
172        };
173
174        // Add HWID to the params
175        let mut full_params = params.to_vec();
176        full_params.push(("hwid", &hwid));
177        full_params.push(("hash", &hash));
178
179        // Send the request
180        let url = reqwest::Url::parse_with_params(
181            &format!("https://tsar.cc/api/client{}", path),
182            &full_params,
183        )
184        .or(Err(TsarError::RequestFailed))?;
185
186        let response = reqwest::blocking::get(url).or(Err(TsarError::RequestFailed))?;
187
188        if !response.status().is_success() {
189            match response.status() {
190                StatusCode::BAD_REQUEST => return Err(TsarError::BadRequest),
191                StatusCode::NOT_FOUND => return Err(TsarError::AppNotFound),
192                StatusCode::UNAUTHORIZED => return Err(TsarError::Unauthorized),
193                StatusCode::TOO_MANY_REQUESTS => return Err(TsarError::RateLimited),
194                StatusCode::SERVICE_UNAVAILABLE => return Err(TsarError::AppPaused),
195                StatusCode::FORBIDDEN => return Err(TsarError::HashUnauthorized),
196                _ => return Err(TsarError::ServerError),
197            }
198        }
199
200        // Parse body into JSON
201        let data = response
202            .json::<Value>()
203            .or(Err(TsarError::FailedToDecode))?;
204
205        // Get the base64-encoded data from the response
206        let base64_data = data
207            .get("data")
208            .and_then(|v| v.as_str())
209            .ok_or(TsarError::FailedToDecode)?;
210
211        // Get the base64-encoded signature from the response
212        let base64_signature = data
213            .get("signature")
214            .and_then(|v| v.as_str())
215            .ok_or(TsarError::FailedToDecode)?;
216
217        // Decode the base64-encoded data (turns into buffer)
218        let data_bytes = BASE64_STANDARD
219            .decode(base64_data)
220            .or(Err(TsarError::FailedToDecode))?;
221
222        // Get json string
223        let json_string =
224            String::from_utf8(data_bytes.clone()).or(Err(TsarError::FailedToDecode))?;
225
226        // Turn string to json
227        let json: Value = serde_json::from_str(&json_string).or(Err(TsarError::FailedToDecode))?;
228
229        // Verify that HWIDs match
230        if let Some(hwid_value) = json.get("hwid") {
231            if let Some(hwid_str) = hwid_value.as_str() {
232                if hwid != hwid_str {
233                    return Err(TsarError::StateMismatch);
234                }
235            } else {
236                return Err(TsarError::FailedToDecode);
237            }
238        } else {
239            return Err(TsarError::FailedToDecode);
240        }
241
242        // Get the timestamp value
243        let timestamp = match json.get("timestamp").and_then(|ts| ts.as_u64()) {
244            Some(ts_secs) => {
245                let duration_secs = Duration::from_secs(ts_secs);
246                UNIX_EPOCH
247                    .checked_add(duration_secs)
248                    .ok_or(TsarError::FailedToDecode)?
249            }
250            None => return Err(TsarError::FailedToDecode),
251        };
252
253        // Get NTP time
254        let client = SntpClient::new();
255        let ntp_time = client
256            .synchronize("time.cloudflare.com")
257            .unwrap()
258            .datetime()
259            .into_system_time()
260            .unwrap();
261
262        // Get system time
263        let system_time = SystemTime::now();
264
265        let duration = if ntp_time > system_time {
266            ntp_time.duration_since(system_time).unwrap()
267        } else {
268            system_time.duration_since(ntp_time).unwrap()
269        };
270
271        // Check that time is synced within a 5 minute leeway
272        if duration.as_millis() > 300000 || timestamp < (system_time - Duration::from_secs(300)) {
273            return Err(TsarError::TamperedResponse);
274        }
275
276        // Decode the base64-encoded signature (turns into buffer)
277        let signature_bytes = BASE64_STANDARD
278            .decode(base64_signature)
279            .or(Err(TsarError::FailedToDecode))?;
280
281        // Build signature from buffer
282        let mut signature = Signature::from_bytes(signature_bytes[..].try_into().unwrap())
283            .or(Err(TsarError::FailedToDecode))?;
284
285        signature = signature.normalize_s().unwrap_or(signature);
286
287        // Verify the signature
288        let result = pub_key.verify(&data_bytes, &signature);
289
290        if result.is_ok() {
291            if std::any::type_name::<T>() == "()" {
292                return Ok(serde_json::from_value(Value::Null).unwrap());
293            }
294
295            // Extract the actual data object
296            let actual_data = json.get("data").ok_or(TsarError::FailedToDecode)?;
297            return Ok(serde_json::from_value(actual_data.clone()).unwrap());
298        }
299
300        Err(TsarError::FailedToDecode)
301    }
302}