use base64::prelude::*;
use errors::{AuthError, ValidateError};
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},
};
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(Debug, Serialize, Deserialize)]
pub struct Data {
user: User,
subscription: Subscription,
timestamp: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
id: String,
username: Option<String>,
avatar: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Subscription {
id: String,
expires: Option<i64>,
}
pub struct Client {
app_id: String,
client_key: String,
}
impl Client {
pub fn new(app_id: &str, client_key: &str) -> Self {
Self {
app_id: app_id.to_string(),
client_key: client_key.to_string(),
}
}
pub fn authenticate_user(&self) -> Result<Data, AuthError> {
let hwid = 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)),
},
};
if let Err(_) = open::that(format!("https://tsar.cc/auth/{}/{}", self.app_id, hwid)) {
return Err(AuthError::FailedToOpenBrowser);
}
let start_time = Instant::now();
loop {
thread::sleep(Duration::from_millis(5000));
match self.validate_user(hwid.as_str()) {
Ok(data) => return Ok(data),
Err(err) => match err {
ValidateError::UserNotFound => {}
_ => return Err(AuthError::ValidateError(err)),
},
};
if start_time.elapsed() >= Duration::from_secs(600) {
return Err(AuthError::Timeout);
}
}
}
pub fn validate_user(&self, hwid: &str) -> Result<Data, ValidateError> {
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;
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 pub_key_bytes = BASE64_STANDARD
.decode(self.client_key.as_str())
.or(Err(ValidateError::FailedToDecodePubKey))?;
let v_pub_key: VerifyingKey =
VerifyingKey::from_public_key_der(pub_key_bytes[..].try_into().unwrap())
.or(Err(ValidateError::FailedToBuildKey))?;
let mut signature = Signature::from_bytes(signature_bytes[..].try_into().unwrap())
.or(Err(ValidateError::FailedToBuildSignature))?;
signature = signature.normalize_s().unwrap_or(signature);
let result = v_pub_key.verify(&data_bytes, &signature);
if result.is_ok() {
return Ok(json);
}
Err(ValidateError::InvalidSignature)
}
}