mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-19 08:47:19 +00:00
fix(LCEL-08): move LCEOnline token to Rust-side secret store
the access token used to live in localStorage under lceonline_session.
localStorage is readable by any JS in the webview origin (XSS exposure).
added three new IPC commands:
- lceonline_token_store(token, username): writes session.json under
app_data_dir with 0600 perms (POSIX).
- lceonline_token_load(): reads session.json and returns {token, username}.
- lceonline_token_clear(): removes session.json.
the frontend now uses invoke() to call these instead of localStorage.
the token never enters the webview's storage surface.
This commit is contained in:
parent
3ab1e2cffa
commit
8f6aea866f
57
src-tauri/src/commands/lceonline_token.rs
Normal file
57
src-tauri/src/commands/lceonline_token.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
|
||||
// security: store the LCEOnline access token in a file under app_data_dir
|
||||
// with 0600 permissions instead of localStorage. localStorage is readable
|
||||
// by any JS in the webview origin (XSS exposure). the file is only
|
||||
// readable by the user account that runs the launcher. (LCEL-08)
|
||||
const TOKEN_FILE: &str = "session.json";
|
||||
|
||||
fn token_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| format!("no app data dir: {}", e))?;
|
||||
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir.join(TOKEN_FILE))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn lceonline_token_store(app: AppHandle, token: String, username: String) -> Result<(), String> {
|
||||
let path = token_path(&app)?;
|
||||
let json = serde_json::json!({
|
||||
"token": token,
|
||||
"username": username,
|
||||
})
|
||||
.to_string();
|
||||
fs::write(&path, json).map_err(|e| e.to_string())?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&path).map_err(|e| e.to_string())?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
fs::set_permissions(&path, perms).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn lceonline_token_load(app: AppHandle) -> Result<Option<serde_json::Value>, String> {
|
||||
let path = token_path(&app)?;
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn lceonline_token_clear(app: AppHandle) -> Result<(), String> {
|
||||
let path = token_path(&app)?;
|
||||
if path.exists() {
|
||||
fs::remove_file(&path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ pub mod dlc;
|
|||
pub mod download;
|
||||
pub mod file_dialogs;
|
||||
pub mod game;
|
||||
pub mod lceonline_token;
|
||||
pub mod macos_setup;
|
||||
pub mod plugins;
|
||||
pub mod proxy_cmd;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,9 @@ pub fn run() {
|
|||
plugins::list_directory,
|
||||
plugins::create_plugin_dir,
|
||||
plugins::remove_plugin_dir,
|
||||
commands::lceonline_token::lceonline_token_store,
|
||||
commands::lceonline_token::lceonline_token_load,
|
||||
commands::lceonline_token::lceonline_token_clear,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// security: the access token currently lives in localStorage under
|
||||
// SESSION_KEY, which is readable by any JS in the webview origin
|
||||
// (XSS exposure). the proper fix is to move it to a Rust-side secret
|
||||
// store accessed via a narrow IPC that returns only scoped capability,
|
||||
// or to use an httpOnly cookie + same-site strict backend auth flow.
|
||||
// (LCEL-08)
|
||||
// security: the access token now lives in a file under app_data_dir with
|
||||
// 0600 perms, accessed via narrow IPC commands (lceonline_token_store /
|
||||
// _load / _clear). localStorage was readable by any JS in the webview
|
||||
// origin (XSS exposure). (LCEL-08)
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
const SESSION_KEY = "lceonline_session";
|
||||
const SOCIAL_BASE_URL = "https://social.mclegacyedition.xyz";
|
||||
const AUTH_BASE_URL = "https://auth.mclegacyedition.xyz"; //neo: yeah bro im hardcoding all three
|
||||
|
|
@ -132,22 +131,30 @@ export class LceOnlineService {
|
|||
}
|
||||
}
|
||||
|
||||
private loadSession() {
|
||||
private async loadSession() {
|
||||
try {
|
||||
const data = localStorage.getItem(SESSION_KEY);
|
||||
const data = await invoke<{ token: string; username: string } | null>(
|
||||
"lceonline_token_load"
|
||||
);
|
||||
if (data) {
|
||||
this._session = JSON.parse(data);
|
||||
this._session = {
|
||||
accessToken: data.token,
|
||||
account: { username: data.username, displayName: data.username },
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load LCE Online session", e);
|
||||
}
|
||||
}
|
||||
|
||||
private saveSession() {
|
||||
private async saveSession() {
|
||||
if (this._session) {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(this._session));
|
||||
await invoke("lceonline_token_store", {
|
||||
token: this._session.accessToken,
|
||||
username: this._session.account.username,
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
await invoke("lceonline_token_clear");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue