Compare commits

..

No commits in common. "3e51a44e30b601ce16eda5ba20d295e1ed2bbd02" and "808420e6015bc476f6a8d59f7c9d21bb03ecaf34" have entirely different histories.

2 changed files with 78 additions and 71 deletions

View File

@ -1,12 +1,11 @@
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::sync::Mutex;
use tokio::fs; use tokio::fs;
use tauri::{Manager, State, WebviewWindow}; use tauri::{Manager, State, WebviewWindow};
use window_vibrancy::apply_acrylic; use window_vibrancy::apply_acrylic;
use sysinfo::{ProcessRefreshKind, RefreshKind, System}; use sysinfo::System;
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
// --- types --- // --- types ---
@ -17,36 +16,26 @@ pub struct Status {
pub is_injected: bool, pub is_injected: bool,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize)]
pub struct UserInfo { pub struct UserInfo {
#[serde(rename = "userId")] pub userId: Option<String>,
pub user_id: Option<String>, pub discordId: Option<String>,
#[serde(rename = "discordId")] pub epicId: Option<String>,
pub discord_id: Option<String>,
#[serde(rename = "epicId")]
pub epic_id: Option<String>,
pub username: Option<String>, pub username: Option<String>,
#[serde(rename = "globalName")] pub globalName: Option<String>,
pub global_name: Option<String>,
pub logins: Option<i32>, pub logins: Option<i32>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize)]
pub struct KeyValidationResponse { pub struct KeyValidationResponse {
pub status: String, pub status: String,
pub user: Option<UserInfo>, pub user: Option<UserInfo>,
} }
#[derive(Deserialize)] // global state for optimization
struct AssetManifest {
injector_hash: String,
dll_hash: String,
}
struct AppState { struct AppState {
client: Client, client: Client,
app_data: PathBuf, app_data: PathBuf,
sys: Mutex<System>,
} }
// --- helpers --- // --- helpers ---
@ -66,6 +55,7 @@ async fn get_last_epic_id(base_path: &Path) -> String {
#[tauri::command] #[tauri::command]
fn get_hwid() -> String { fn get_hwid() -> String {
// direct registry query for speed
let output = Command::new("reg") let output = Command::new("reg")
.args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"]) .args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"])
.output(); .output();
@ -78,10 +68,11 @@ fn get_hwid() -> String {
} }
} }
} }
// strictly avoid returning "unknown" to prevent key sharing bypasses "UNKNOWN-HWID".to_string()
"00000000-0000-0000-0000-000000000000".to_string()
} }
#[tauri::command] #[tauri::command]
async fn validate_key( async fn validate_key(
key: String, key: String,
@ -96,9 +87,19 @@ async fn validate_key(
.await .await
.map_err(|e| format!("network error: {}", e))?; .map_err(|e| format!("network error: {}", e))?;
res.json::<KeyValidationResponse>() let json: serde_json::Value = res.json().await.map_err(|e| format!("json error: {}", e))?;
.await
.map_err(|e| format!("api schema mismatch: {}", e)) let status = json.get("status").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let user = json.get("user").map(|u| UserInfo {
userId: u.get("userId").and_then(|v| v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string()))),
discordId: u.get("discordId").and_then(|v| v.as_str()).map(|s| s.to_string()),
epicId: u.get("epicId").and_then(|v| v.as_str()).map(|s| s.to_string()),
username: u.get("username").and_then(|v| v.as_str()).map(|s| s.to_string()),
globalName: u.get("globalName").and_then(|v| v.as_str()).map(|s| s.to_string()),
logins: u.get("logins").and_then(|v| v.as_i64()).map(|n| n as i32),
});
Ok(KeyValidationResponse { status, user })
} }
#[tauri::command] #[tauri::command]
@ -106,29 +107,26 @@ async fn inject_dll(state: State<'_, AppState>) -> Result<String, String> {
let injector_path = state.app_data.join("injector.exe"); let injector_path = state.app_data.join("injector.exe");
let dll_path = state.app_data.join("RLIdentity.dll"); let dll_path = state.app_data.join("RLIdentity.dll");
{ let mut s = System::new_all();
let mut sys = state.sys.lock().unwrap(); s.refresh_processes();
// fix: use refresh_processes_specifics and add the remove_dead_processes bool if s.processes_by_exact_name("RocketLeague.exe").next().is_none() {
sys.refresh_processes_specifics(ProcessRefreshKind::new()); return Err("rocket league is not running".into());
if sys.processes_by_exact_name("RocketLeague.exe").next().is_none() {
return Err("rocket league is not running".into());
}
} }
if !injector_path.exists() || !dll_path.exists() { if !injector_path.exists() || !dll_path.exists() {
return Err("files missing, please update".into()); return Err("files missing, wait for update".into());
} }
let output = Command::new(injector_path) let output = Command::new(injector_path)
.arg("RocketLeague.exe") .arg("RocketLeague.exe")
.arg(&dll_path) .arg(dll_path)
.output() .output()
.map_err(|e| format!("exec failed: {}", e))?; .map_err(|e| format!("exec failed: {}", e))?;
if output.status.success() { if output.status.success() {
Ok("injected".into()) Ok("injected".into())
} else { } else {
Err("injection failed: check admin privileges".into()) Err("injection failed".into())
} }
} }
@ -136,51 +134,59 @@ sys.refresh_processes_specifics(ProcessRefreshKind::new());
async fn download_assets(state: State<'_, AppState>) -> Result<(), String> { async fn download_assets(state: State<'_, AppState>) -> Result<(), String> {
fs::create_dir_all(&state.app_data).await.map_err(|e| e.to_string())?; fs::create_dir_all(&state.app_data).await.map_err(|e| e.to_string())?;
// dynamically fetch latest hashes to avoid brittle hardcoding // in a real scenario, you'd fetch these hashes from your api first
let manifest: AssetManifest = state.client.get("https://api.rlidentity.me/manifest")
.send().await.map_err(|e| e.to_string())?
.json().await.map_err(|e| e.to_string())?;
let assets = [ let assets = [
("injector.exe", "https://git.rlidentity.me/bits/RLidentity/raw/branch/main/injector.exe", manifest.injector_hash), (
("RLIdentity.dll", "https://git.rlidentity.me/bits/RLidentity/raw/branch/main/RLIdentity.dll", manifest.dll_hash), "injector.exe",
"https://git.rlidentity.me/bits/RLidentity/raw/branch/main/injector.exe",
"a5fa657f6336d257d1c29808f4f7df6366a6110a1191a7c704c32b16a491bd14"
),
(
"RLIdentity.dll",
"https://git.rlidentity.me/bits/RLidentity/raw/branch/main/RLIdentity.dll",
"24ca50f56904d7a042f08ce87f2e027eb8b7006d1f12dd2512b3a7efbfc7bba6"
),
]; ];
for (name, url, expected_hash) in assets { for (name, url, expected_hash) in assets {
let file_path = state.app_data.join(name); let file_path = state.app_data.join(name);
// download
let res = state.client.get(url).send().await.map_err(|e| e.to_string())?; let res = state.client.get(url).send().await.map_err(|e| e.to_string())?;
let bytes = res.bytes().await.map_err(|e| e.to_string())?; let bytes = res.bytes().await.map_err(|e| e.to_string())?;
// verify integrity (signature check)
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&bytes); hasher.update(&bytes);
let actual_hash = hex::encode(hasher.finalize()); let actual_hash = hex::encode(hasher.finalize());
if actual_hash != expected_hash { if actual_hash != expected_hash {
return Err(format!("integrity check failed for {}", name)); return Err(format!("integrity check failed for {}: hash mismatch", name));
} }
// only write if the "signature" (hash) is correct
fs::write(file_path, bytes).await.map_err(|e| e.to_string())?; fs::write(file_path, bytes).await.map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
async fn check_status(state: State<'_, AppState>) -> Result<Status, String> { fn get_app_version(app: tauri::AppHandle) -> String {
let mut sys = state.sys.lock().unwrap(); app.package_info().version.to_string()
// fix: use refresh_processes_specifics and add the remove_dead_processes bool
sys.refresh_processes_specifics(ProcessRefreshKind::new());
let is_running = sys.processes_by_exact_name("RocketLeague.exe").next().is_some();
Ok(Status { is_running, is_injected: false })
} }
#[tauri::command]
async fn check_status() -> Status {
let mut s = System::new_all();
s.refresh_processes();
let is_running = s.processes_by_exact_name("RocketLeague.exe").next().is_some();
Status { is_running, is_injected: false }
}
#[tauri::command] #[tauri::command]
async fn save_config(config_data: String, state: State<'_, AppState>) -> Result<(), String> { async fn save_config(config_data: String, state: State<'_, AppState>) -> Result<(), String> {
let config_path = state.app_data.join("config.json"); let config_path = state.app_data.join("config.json");
let temp_path = state.app_data.join("config.json.tmp"); fs::write(config_path, config_data)
.await
// atomic write: write to tmp then rename to prevent corruption on crash .map_err(|e| format!("failed to save config: {}", e))
fs::write(&temp_path, config_data).await.map_err(|e| e.to_string())?;
fs::rename(temp_path, config_path).await.map_err(|e| e.to_string())
} }
#[tauri::command] #[tauri::command]
@ -195,14 +201,13 @@ pub fn run() {
.manage(AppState { .manage(AppState {
client: Client::builder() client: Client::builder()
.danger_accept_invalid_certs(false) .danger_accept_invalid_certs(false)
.timeout(std::time::Duration::from_secs(30))
.build() .build()
.unwrap(), .unwrap(),
app_data: dirs::data_dir().expect("could not find data dir").join("RLidentity"), app_data: dirs::data_dir().unwrap().join("RLidentity"),
sys: Mutex::new(System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::new())
)),
}) })
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
minimize_to_tray, minimize_to_tray,
inject_dll, inject_dll,
@ -210,6 +215,7 @@ pub fn run() {
check_status, check_status,
get_hwid, get_hwid,
download_assets, download_assets,
get_app_version,
save_config save_config
]) ])
.setup(|app| { .setup(|app| {
@ -240,4 +246,4 @@ pub fn run() {
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,5 +1,6 @@
import { useMemo, useState, useEffect } from "react"; import { useMemo, useState, useEffect } from "react";
import { invoke } from '@tauri-apps/api/core';
const [version, setVersion] = useState('');
type Status = type Status =
| "ready" | "ready"
| "saved successfully" | "saved successfully"
@ -12,11 +13,11 @@ type Status =
| string; | string;
interface UserInfo { interface UserInfo {
user_id: string | null; userId: string | null;
discord_id: string | null; discordId: string | null;
epic_id: string | null; epicId: string | null;
username: string | null; username: string | null;
global_name: string | null; globalName: string | null;
logins?: number; logins?: number;
} }
@ -88,7 +89,7 @@ export default function App() {
const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []); const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []);
const initialTheme = useMemo(() => (localStorage.getItem(LS_KEYS.theme) ?? "phantom") as ThemeId, []); const initialTheme = useMemo(() => (localStorage.getItem(LS_KEYS.theme) ?? "phantom") as ThemeId, []);
const [version, setVersion] = useState(''); const [version, setVersion] = useState('');
const [apiKey, setApiKey] = useState(initialApiKey); const [apiKey, setApiKey] = useState(initialApiKey);
const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed); const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed);
const [isAuthorized, setIsAuthorized] = useState(false); const [isAuthorized, setIsAuthorized] = useState(false);
const [isRevoked, setIsRevoked] = useState(false); const [isRevoked, setIsRevoked] = useState(false);
@ -244,7 +245,7 @@ export default function App() {
async function inject() { async function inject() {
setStatus("injecting..."); setStatus("injecting...");
try { try {
const res = await tryInvoke<string>("inject_dll", { discordId: userData?.discord_id }); const res = await tryInvoke<string>("inject_dll", { discordId: userData?.discordId });
setLastLog(res || "Successfully Injected!"); setLastLog(res || "Successfully Injected!");
setStatus("Successfully Injected!"); setStatus("Successfully Injected!");
window.setTimeout(() => setStatus("ready"), 1400); window.setTimeout(() => setStatus("ready"), 1400);
@ -393,9 +394,9 @@ export default function App() {
<main className="panel-wrap"> <main className="panel-wrap">
<header className="welcome-section"> <header className="welcome-section">
<h2 className="welcome-text"> <h2 className="welcome-text">
Welcome, <span className="neon-text-soft">{userData?.global_name || userData?.username || "User"}</span> Welcome, <span className="neon-text-soft">{userData?.globalName || userData?.username || "User"}</span>
</h2> </h2>
<div className="user-id-badge">User #{userData?.user_id || "0"}</div> <div className="user-id-badge">User #{userData?.userId || "0"}</div>
</header> </header>
<section className="glass-card neon-ring"> <section className="glass-card neon-ring">
@ -456,7 +457,7 @@ export default function App() {
<h2 className="modal-title neon-text-soft">System Credits</h2> <h2 className="modal-title neon-text-soft">System Credits</h2>
<div className="credits-grid glass-input"> <div className="credits-grid glass-input">
{[ {[
{ role: "Lead Dev & Owner", name: "Bits", accent: true }, { role: "Lead Dev & Owner", name: "Bits", accent: true },
{ role: "Dev & Admin", name: "Danni" }, { role: "Dev & Admin", name: "Danni" },
{ role: "Co-Owner", name: "Deniz" }, { role: "Co-Owner", name: "Deniz" },
{ role: "Administrator", name: "Kairo" }, { role: "Administrator", name: "Kairo" },