5 Commits

Author SHA1 Message Date
f214aa454e chore: remove todo note 2025-07-16 22:02:13 +02:00
9d2599dd2f chore: code cleanup 2025-07-16 22:01:41 +02:00
eddaaaaa2f fix: add fallback for non windows 2025-07-16 22:01:07 +02:00
a21abe86bb chore: bump version 2025-07-16 21:56:46 +02:00
d685114bd7 feat: use dpapi for windows to ensure longer passwords work 2025-07-16 21:48:17 +02:00
9 changed files with 77 additions and 85 deletions

9
src-tauri/Cargo.lock generated
View File

@@ -1094,7 +1094,7 @@ dependencies = [
[[package]] [[package]]
name = "ezpplauncher" name = "ezpplauncher"
version = "3.0.0" version = "3.0.1"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"discord-rich-presence", "discord-rich-presence",
@@ -1116,6 +1116,7 @@ dependencies = [
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-sql", "tauri-plugin-sql",
"tokio", "tokio",
"widestring",
"winapi", "winapi",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"winreg 0.55.0", "winreg 0.55.0",
@@ -5799,6 +5800,12 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ezpplauncher" name = "ezpplauncher"
version = "3.0.0" version = "3.0.1"
description = "EZPPLauncher redefined." description = "EZPPLauncher redefined."
authors = ["HorizonCode"] authors = ["HorizonCode"]
edition = "2024" edition = "2024"
@@ -41,6 +41,7 @@ once_cell = "1.21.3"
winreg = "0.55.0" winreg = "0.55.0"
winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] } winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
base64 = "0.21" base64 = "0.21"
widestring = "1.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.0" tauri-plugin-single-instance = "2.3.0"

View File

@@ -13,9 +13,9 @@ use tokio::time::{Duration, sleep};
use crate::presence; use crate::presence;
use crate::utils::{ use crate::utils::{
check_folder_completeness, get_osu_config, get_osu_user_config, get_window_title_by_pid, check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config,
is_net8_installed, is_osuwinello_available, is_wmctrl_available, set_osu_config_vals, get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available,
set_osu_user_config_vals, set_osu_config_vals, set_osu_user_config_vals,
}; };
#[tauri::command] #[tauri::command]
@@ -719,3 +719,21 @@ pub fn has_osuwinello() -> bool {
pub async fn has_net8() -> bool { pub async fn has_net8() -> bool {
is_net8_installed().await is_net8_installed().await
} }
#[cfg(windows)]
#[tauri::command]
pub fn encrypt_string(string: String, entropy: String) -> String {
let encrypted = encrypt_password(&string, &entropy);
match encrypted {
Ok(encrypted_vec) => encrypted_vec,
Err(_) => string,
}
}
// NOTE: should not be called by tauri on non windows systems, return the string nonthenless
#[cfg(not(windows))]
#[tauri::command]
pub fn encrypt_string(string: String, _entropy: String) -> String {
string
}

View File

@@ -5,18 +5,17 @@ pub mod commands;
pub mod presence; pub mod presence;
pub mod utils; pub mod utils;
use crate::commands::{ use crate::commands::{
check_for_corruption, download_ezpp_launcher_update_files, exit, find_osu_installation, check_for_corruption, download_ezpp_launcher_update_files, encrypt_string, exit,
get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid, get_launcher_version, find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid,
get_osu_release_stream, get_osu_skin, get_osu_version, get_platform, get_skins_count, get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform,
is_osu_running, open_url_in_browser, presence_connect, presence_disconnect, get_skins_count, has_net8, has_osuwinello, has_wmctrl, is_osu_running, open_url_in_browser,
presence_is_connected, presence_update_status, presence_update_user, replace_ui_files, run_osu, presence_connect, presence_disconnect, presence_is_connected, presence_update_status,
run_osu_updater, set_osu_config_values, set_osu_user_config_values, valid_osu_folder, has_osuwinello, has_wmctrl, presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
has_net8 set_osu_user_config_values, valid_osu_folder,
}; };
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
unsafe { unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
@@ -61,7 +60,8 @@ pub fn run() {
presence_is_connected, presence_is_connected,
has_osuwinello, has_osuwinello,
has_wmctrl, has_wmctrl,
has_net8 has_net8,
encrypt_string
]) ])
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())

View File

@@ -308,17 +308,20 @@ pub async fn is_net8_installed() -> bool {
} }
} }
//TODO: maybe switch to this crate: https://crates.io/crates/windows-dpapi
#[cfg(windows)] #[cfg(windows)]
pub fn encrypt_password(password: &str) -> Result<String, String> { pub fn encrypt_password(password: &str, entropy: &str) -> Result<String, String> {
use base64::{Engine as _, engine::general_purpose};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr; use std::ptr;
use std::slice; use std::slice;
use winapi::shared::minwindef::{BYTE, DWORD, LPVOID}; use winapi::shared::minwindef::{BYTE, DWORD, LPVOID};
use winapi::shared::ntdef::LPCWSTR;
use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData}; use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData};
use winapi::um::winbase::LocalFree; use winapi::um::winbase::LocalFree;
use winapi::um::wincrypt::DATA_BLOB; use winapi::um::wincrypt::DATA_BLOB;
use base64::{engine::general_purpose, Engine as _};
let description = "Encrypted";
let password_bytes = password.as_bytes(); let password_bytes = password.as_bytes();
let mut input_blob = DATA_BLOB { let mut input_blob = DATA_BLOB {
@@ -326,16 +329,28 @@ pub fn encrypt_password(password: &str) -> Result<String, String> {
pbData: password_bytes.as_ptr() as *mut BYTE, pbData: password_bytes.as_ptr() as *mut BYTE,
}; };
let entropy_bytes = entropy.as_bytes();
let mut entropy_blob = DATA_BLOB {
cbData: entropy_bytes.len() as DWORD,
pbData: entropy_bytes.as_ptr() as *mut BYTE,
};
let mut output_blob = DATA_BLOB { let mut output_blob = DATA_BLOB {
cbData: 0, cbData: 0,
pbData: ptr::null_mut(), pbData: ptr::null_mut(),
}; };
let wide_description: Vec<u16> = OsStrExt::encode_wide(OsStr::new(description))
.chain(Some(0))
.collect();
let p_description: LPCWSTR = wide_description.as_ptr();
let result = unsafe { let result = unsafe {
CryptProtectData( CryptProtectData(
&mut input_blob, &mut input_blob,
ptr::null(), p_description,
ptr::null_mut(), &mut entropy_blob,
ptr::null_mut(), ptr::null_mut(),
ptr::null_mut(), ptr::null_mut(),
CRYPTPROTECT_UI_FORBIDDEN, CRYPTPROTECT_UI_FORBIDDEN,
@@ -354,66 +369,7 @@ pub fn encrypt_password(password: &str) -> Result<String, String> {
LocalFree(output_blob.pbData as LPVOID); LocalFree(output_blob.pbData as LPVOID);
} }
let base64_string = general_purpose::STANDARD.encode(&encrypted_data); let base64_string = general_purpose::STANDARD_NO_PAD.encode(&encrypted_data);
Ok(base64_string) Ok(base64_string)
} }
#[cfg(windows)]
pub fn decrypt_password(encrypted_password_base64: &str) -> Result<String, String> {
use std::ptr;
use std::slice;
use winapi::shared::minwindef::{BYTE, DWORD, LPVOID};
use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptUnprotectData};
use winapi::um::winbase::LocalFree;
use winapi::um::wincrypt::DATA_BLOB;
use base64::{engine::general_purpose, Engine as _};
// 1. Decode the Base64 string back into raw bytes
let encrypted_password_bytes = general_purpose::STANDARD.decode(encrypted_password_base64)
.map_err(|e| format!("Base64 decoding failed: {}", e))?;
// 2. Prepare the input data structure
let mut input_blob = DATA_BLOB {
cbData: encrypted_password_bytes.len() as DWORD,
pbData: encrypted_password_bytes.as_ptr() as *mut BYTE,
};
// 3. Prepare the output data structure
let mut output_blob = DATA_BLOB {
cbData: 0,
pbData: ptr::null_mut(),
};
// 4. Call the Windows API function
let result = unsafe {
CryptUnprotectData(
&mut input_blob,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
CRYPTPROTECT_UI_FORBIDDEN,
&mut output_blob,
)
};
// 5. Check the result
if result == 0 {
return Err("CryptUnprotectData failed".to_string());
}
// 6. Copy the decrypted data into a safe Rust Vec
let decrypted_bytes = unsafe {
slice::from_raw_parts(output_blob.pbData, output_blob.cbData as usize).to_vec()
};
// 7. Free the memory allocated by the Windows API
unsafe {
LocalFree(output_blob.pbData as LPVOID);
}
// 8. Convert the decrypted bytes back to a UTF-8 string
String::from_utf8(decrypted_bytes)
.map_err(|e| format!("UTF-8 conversion failed: {}", e))
}

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ezpplauncher", "productName": "ezpplauncher",
"version": "3.0.0", "version": "3.0.1",
"identifier": "farm.ezpp.launcher", "identifier": "farm.ezpp.launcher",
"build": { "build": {
"beforeDevCommand": "bun run vite:dev", "beforeDevCommand": "bun run vite:dev",

View File

@@ -138,4 +138,6 @@ export const hasWMCTRL = async () =>
export const hasOsuWinello = async () => export const hasOsuWinello = async () =>
await invoke<boolean>('has_osuwinello'); await invoke<boolean>('has_osuwinello');
export const hasNet8 = async () => export const hasNet8 = async () =>
await invoke<boolean>('has_net8'); await invoke<boolean>('has_net8');
export const encryptString = async (str: string, entropy: string) =>
await invoke<string>('encrypt_string', {string: str, entropy});

View File

@@ -83,6 +83,7 @@
import { osuapi } from '@/api/osuapi'; import { osuapi } from '@/api/osuapi';
import { import {
downloadEZPPLauncherUpdateFiles, downloadEZPPLauncherUpdateFiles,
encryptString,
exit, exit,
getBeatmapSetsCount, getBeatmapSetsCount,
getEZPPLauncherUpdateFiles, getEZPPLauncherUpdateFiles,
@@ -296,7 +297,7 @@
}, },
{ {
key: 'Password', key: 'Password',
value: password, value: $platform === "windows" ? await encryptString(password, "cu24180ncjeiu0ci1nwui") : password,
}, },
{ {
key: 'SaveUsername', key: 'SaveUsername',

View File

@@ -26,7 +26,7 @@
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
import { userAuth } from '@/userAuthentication'; import { userAuth } from '@/userAuthentication';
import { exit, getLauncherVersion, getPlatform } from '@/osuUtil'; import { encryptString, exit, getLauncherVersion, getPlatform } from '@/osuUtil';
import Button from '@/components/ui/button/button.svelte'; import Button from '@/components/ui/button/button.svelte';
import * as presence from '@/presence'; import * as presence from '@/presence';
@@ -88,6 +88,13 @@
onMount(async () => { onMount(async () => {
window.Buffer = Buffer; window.Buffer = Buffer;
/* const decryptTest = "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAs463WdMtG0mr/mVLQCZ6dQAAAAAIAAAAUgBhAHcAAAAQZgAAAAEAACAAAABnyhFo8QK2iL5aTehKmsZSUpihGWBwlKfYj7cL2/lDagAAAAAOgAAAAAIAACAAAABMxyG6EdkLORSMB8isbltZhPQV2iVQ2r+yJLJ2Tw9yWiAAAABE1EzBPD9PRFQ3evk0vneNpfQTNTvMpjDVUTl3kAaKWUAAAADBshJzPDx6qcYfYh2zh6cKJWClZpIp6H50IKriW936XGhLFQboK/m18O77TBnpSrs0YosFjzsYVJTkrWjetFAf";
const decrypted = await decryptString(decryptTest);
console.log(decrypted); */
const encrypted = await encryptString("Todesengel007008009!", "cu24180ncjeiu0ci1nwui");
console.log(encrypted);
disableReload(); disableReload();
setupValues(); setupValues();
launcherVersion.set(await getLauncherVersion()); launcherVersion.set(await getLauncherVersion());