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},
};
#[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;
#[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);
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Data {
pub user: User,
pub subscription: Subscription,
pub timestamp: u64,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub username: Option<String>,
pub avatar: Option<String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Subscription {
pub id: String,
pub expires: Option<i64>,
}
pub struct Client {
pub app_id: String,
pub client_key: String,
}
impl Client {
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
}
pub fn authenticate_user(&self) -> Result<Data, AuthError> {
let hwid = goldberg_stmts! {{
dbo!();
get_id().or(Err(AuthError::FailedToGetHWID))?
}};
match self.validate_user(hwid.as_str()) {
Ok(data) => return Ok(data),
Err(err) => match err {
ValidateError::UserNotFound => {}
_ => return Err(AuthError::ValidateError(err)),
},
};
goldberg_stmts! {{
dbo!();
if let Err(_) = open::that(format!("https://tsar.cc/auth/{}/{}", self.app_id, hwid)) {
return Err(AuthError::FailedToOpenBrowser);
}
}};
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),
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);
}
}};
}
}
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!();
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),
}
}
let data = response
.json::<Value>()
.or(Err(ValidateError::FailedToParseBody))?;
let base64_data = data
.get("data")
.and_then(|v| v.as_str())
.ok_or(ValidateError::FailedToGetData)?;
let base64_signature = data
.get("signature")
.and_then(|v| v.as_str())
.ok_or(ValidateError::FailedToGetSignature)?;
let data_bytes = BASE64_STANDARD
.decode(base64_data)
.or(Err(ValidateError::FailedToDecodeData))?;
let json_string =
String::from_utf8(data_bytes.clone()).or(Err(ValidateError::FailedToParseData))?;
let json: Data =
serde_json::from_str(&json_string).or(Err(ValidateError::FailedToParseData))?;
let timestamp = json.timestamp;
dbo!();
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);
}
let signature_bytes = BASE64_STANDARD
.decode(base64_signature)
.or(Err(ValidateError::FailedToDecodeSignature))?;
let mut signature = Signature::from_bytes(signature_bytes[..].try_into().unwrap())
.or(Err(ValidateError::FailedToBuildSignature))?;
signature = signature.normalize_s().unwrap_or(signature);
dbo!();
let result = pub_key.verify(&data_bytes, &signature);
if result.is_ok() {
return Ok(json);
}
Err(ValidateError::InvalidSignature)
}};
result
}
}