1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// TSAR
// (c) 2024 TSAR, under MIT license

//! Official wrapper for the TSAR client API.

use base64::prelude::*;
use errors::{AuthError, ValidateError};
use goldberg::goldberg_stmts;
use hardware_id::get_id;
use p256::{
    ecdsa::{signature::Verifier, Signature, VerifyingKey},
    pkcs8::DecodePublicKey,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{
    thread,
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};

// Debugoff
#[cfg(all(target_os = "linux", not(debug_assertions)))]
use debugoff;

macro_rules! dbo {
    () => {
        #[cfg(all(target_os = "linux", not(debug_assertions)))]
        debugoff::multi_ptraceme_or_die();
    };
}

mod errors;

// Tester [ cargo test -- --nocapture ]
#[cfg(test)]
mod tests {
    use crate::Client;

    const PUBLIC_KEY: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELlyGTmNEv3AarudyshJUUA9ig1pOfSl5qWX8g/hkPiieeKlWvv9o4IZmWI4cCrcR0fteVEcUhBvu5GAr/ITBqA==";
    const APP_ID: &str = "58816206-b24c-41d4-a594-8500746a78ee";

    #[test]
    fn authenticate_user() {
        let api = Client::new(APP_ID, PUBLIC_KEY);

        match api.authenticate_user() {
            Ok(data) => println!("\x1b[32m[TEST SUCCESS] Data\x1b[0m: {:?}", data),
            Err(err) => println!("\x1b[31m[TEST ERROR] {:?}\x1b[0m: {}", err, err),
        }

        assert!(true);
    }
}

/// Data returned by the server when running `authenticate_user()` or `validate_user()`.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Data {
    pub user: User,
    pub subscription: Subscription,
    pub timestamp: u64,
}

/// User object which gets returned as part of `authenticate_user()` or `validate_user()`.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct User {
    pub id: String,
    pub username: Option<String>,
    pub avatar: Option<String>,
}

/// Subscription object which gets returned as part of `authenticate_user()` or `validate_user()`.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Subscription {
    pub id: String,
    /// Timestamp of when the subscription expires
    pub expires: Option<i64>,
}

/// The TSAR Client struct. Used to interact with the API after it's initialized.
pub struct Client {
    /// The ID of your TSAR app. Should be in UUID format: 00000000-0000-0000-0000-000000000000
    pub app_id: String,
    /// The public decryption key for your TSAR app. Should be in base64 format.
    pub client_key: String,
}

impl Client {
    /// Creates a new TSAR client using an `app_id` and `client_key` variables.
    pub fn new(app_id: &str, client_key: &str) -> Self {
        let result: Self = goldberg_stmts! {{

                Self {
                    app_id: app_id.to_string(),
                    client_key: client_key.to_string(),
                }

        }};

        result
    }

    /// Starts an authentication flow which attempts to authenticate the user.
    /// If the user's HWID is not already authorized, the function opens the user's default browser to authenticate them.
    pub fn authenticate_user(&self) -> Result<Data, AuthError> {
        let hwid = goldberg_stmts! {{
            dbo!();

            get_id().or(Err(AuthError::FailedToGetHWID))?
        }};

        // Attempt to validate user
        match self.validate_user(hwid.as_str()) {
            Ok(data) => return Ok(data),

            // Only continue execution if the user is not found, if any other part of the validate_user function fails then return an error
            Err(err) => match err {
                ValidateError::UserNotFound => {}
                _ => return Err(AuthError::ValidateError(err)),
            },
        };

        goldberg_stmts! {{
            dbo!();

            // Open default browser
            if let Err(_) = open::that(format!("https://tsar.cc/auth/{}/{}", self.app_id, hwid)) {
                return Err(AuthError::FailedToOpenBrowser);
            }
        }};

        // Start validation loop
        let start_time = goldberg_stmts! {{
            Instant::now()
        }};

        loop {
            thread::sleep(Duration::from_millis(5000));

            dbo!();

            match self.validate_user(hwid.as_str()) {
                Ok(data) => return Ok(data),

                // Only continue execution if the user is not found, if any other part of the validate_user function fails then return an error
                Err(err) => match err {
                    ValidateError::UserNotFound => {}
                    _ => return Err(AuthError::ValidateError(err)),
                },
            };

            goldberg_stmts! {{
                if start_time.elapsed() >= Duration::from_secs(600) {
                    return Err(AuthError::Timeout);
                }
            }};
        }
    }

    /// Check if a HWID is authorized to use the application.
    pub fn validate_user(&self, hwid: &str) -> Result<Data, ValidateError> {
        let pub_key_bytes = goldberg_stmts! {{
            dbo!();

            BASE64_STANDARD
                .decode(self.client_key.as_str())
                .or(Err(ValidateError::FailedToDecodePubKey))?
        }};

        dbo!();

        // Build key from public key pem
        let pub_key: VerifyingKey =
            VerifyingKey::from_public_key_der(pub_key_bytes[..].try_into().unwrap())
                .or(Err(ValidateError::FailedToBuildKey))?;

        #[allow(non_camel_case_types)]
        let result: Result<Data, ValidateError> = goldberg_stmts! {{
            dbo!();

            let url = format!(
                "https://tsar.cc/api/client/v1/subscriptions/validate?app={}&hwid={}",
                self.app_id, hwid
            );

            let response = reqwest::blocking::get(&url).or(Err(ValidateError::RequestFailed))?;

            if !response.status().is_success() {
                match response.status() {
                    StatusCode::NOT_FOUND => return Err(ValidateError::UserNotFound),
                    _ => return Err(ValidateError::ServerError),
                }
            }

            // Parse body into JSON
            let data = response
                .json::<Value>()
                .or(Err(ValidateError::FailedToParseBody))?;

            // Get the base64-encoded data from the response
            let base64_data = data
                .get("data")
                .and_then(|v| v.as_str())
                .ok_or(ValidateError::FailedToGetData)?;

            // Get the base64-encoded signature from the response
            let base64_signature = data
                .get("signature")
                .and_then(|v| v.as_str())
                .ok_or(ValidateError::FailedToGetSignature)?;

            // Decode the base64-encoded data (turns into buffer)
            let data_bytes = BASE64_STANDARD
                .decode(base64_data)
                .or(Err(ValidateError::FailedToDecodeData))?;

            // Get json string
            let json_string =
                String::from_utf8(data_bytes.clone()).or(Err(ValidateError::FailedToParseData))?;

            // Turn string to json
            let json: Data =
                serde_json::from_str(&json_string).or(Err(ValidateError::FailedToParseData))?;

            // Get the timestamp value
            let timestamp = json.timestamp;

            dbo!();

            // Verify that the timestamp is less than least 30 seconds old
            let timestamp_system_time = UNIX_EPOCH + Duration::from_secs(timestamp / 1000);
            let thirty_seconds_ago = SystemTime::now() - Duration::from_secs(30);

            if timestamp_system_time < thirty_seconds_ago {
                return Err(ValidateError::OldResponse);
            }

            // Decode the base64-encoded signature (turns into buffer)
            let signature_bytes = BASE64_STANDARD
                .decode(base64_signature)
                .or(Err(ValidateError::FailedToDecodeSignature))?;



            // Build signature from buffer
            let mut signature = Signature::from_bytes(signature_bytes[..].try_into().unwrap())
                .or(Err(ValidateError::FailedToBuildSignature))?;

            // NodeJS sucks so we need to normalize the sig
            signature = signature.normalize_s().unwrap_or(signature);

            dbo!();

            // Verify the signature
            let result = pub_key.verify(&data_bytes, &signature);

            if result.is_ok() {
                return Ok(json);
            }

            Err(ValidateError::InvalidSignature)
        }};

        result
    }
}