From d685114bd7fadcf8336bee4231a5436a72cfce46 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Wed, 16 Jul 2025 21:48:17 +0200 Subject: [PATCH] feat: use dpapi for windows to ensure longer passwords work --- src-tauri/Cargo.lock | 7 ++++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands.rs | 28 +++++++++++-- src-tauri/src/lib.rs | 18 ++++---- src-tauri/src/utils.rs | 88 ++++++++++----------------------------- src/lib/osuUtil.ts | 4 +- src/pages/Launch.svelte | 3 +- src/routes/+layout.svelte | 9 +++- 8 files changed, 78 insertions(+), 80 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c371dab..a223d13 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1116,6 +1116,7 @@ dependencies = [ "tauri-plugin-single-instance", "tauri-plugin-sql", "tokio", + "widestring", "winapi", "windows-sys 0.60.2", "winreg 0.55.0", @@ -5799,6 +5800,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1149d8b..626ede5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ once_cell = "1.21.3" winreg = "0.55.0" winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] } base64 = "0.21" +widestring = "1.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-single-instance = "2.3.0" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b5213d9..b863bee 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -13,9 +13,9 @@ use tokio::time::{Duration, sleep}; use crate::presence; use crate::utils::{ - check_folder_completeness, get_osu_config, get_osu_user_config, get_window_title_by_pid, - is_net8_installed, is_osuwinello_available, is_wmctrl_available, set_osu_config_vals, - set_osu_user_config_vals, + check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config, + get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available, + set_osu_config_vals, set_osu_user_config_vals, }; #[tauri::command] @@ -719,3 +719,25 @@ pub fn has_osuwinello() -> bool { pub async fn has_net8() -> bool { is_net8_installed().await } + +/* +* osu!.exe decompile result: +* dpapi cipher is most likely: cu24180ncjeiu0ci1nwui +* dpapi key type is: 1 +* +* example call for encryption: DPAPI.Encrypt((DPAPI.KeyType)1, this.storage.UnsecureRepresentation(), "cu24180ncjeiu0ci1nwui", this.representation.ToString()); +* the method args are following: Encrypt(DPAPI.KeyType keyType, byte[] plainTextBytes, byte[] entropyBytes, string description) +*/ + +#[tauri::command] +pub fn encrypt_string(string: String, entropy: String) -> String { + let encrypted = encrypt_password(&string, &entropy); + + match encrypted { + Ok(encrypted_vec) => { + // encrypted_vec is already a String, not Vec + encrypted_vec + } + Err(_) => string, + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 34ae214..29bbb22 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,18 +5,17 @@ pub mod commands; pub mod presence; pub mod utils; use crate::commands::{ - check_for_corruption, download_ezpp_launcher_update_files, exit, find_osu_installation, - get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid, get_launcher_version, - get_osu_release_stream, get_osu_skin, get_osu_version, get_platform, get_skins_count, - is_osu_running, open_url_in_browser, presence_connect, presence_disconnect, - presence_is_connected, presence_update_status, presence_update_user, replace_ui_files, run_osu, - run_osu_updater, set_osu_config_values, set_osu_user_config_values, valid_osu_folder, has_osuwinello, has_wmctrl, - has_net8 + check_for_corruption, download_ezpp_launcher_update_files, encrypt_string, exit, + find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid, + get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform, + get_skins_count, has_net8, has_osuwinello, has_wmctrl, is_osu_running, open_url_in_browser, + presence_connect, presence_disconnect, presence_is_connected, presence_update_status, + presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values, + set_osu_user_config_values, valid_osu_folder, }; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - #[cfg(target_os = "linux")] unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); @@ -61,7 +60,8 @@ pub fn run() { presence_is_connected, has_osuwinello, has_wmctrl, - has_net8 + has_net8, + encrypt_string ]) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index ff74fca..b9220ad 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -311,14 +311,19 @@ pub async fn is_net8_installed() -> bool { //TODO: maybe switch to this crate: https://crates.io/crates/windows-dpapi #[cfg(windows)] -pub fn encrypt_password(password: &str) -> Result { +pub fn encrypt_password(password: &str, entropy: &str) -> Result { + use base64::{Engine as _, engine::general_purpose}; + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; use std::ptr; use std::slice; use winapi::shared::minwindef::{BYTE, DWORD, LPVOID}; + use winapi::shared::ntdef::LPCWSTR; use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData}; use winapi::um::winbase::LocalFree; use winapi::um::wincrypt::DATA_BLOB; - use base64::{engine::general_purpose, Engine as _}; + + let description = "Encrypted"; let password_bytes = password.as_bytes(); let mut input_blob = DATA_BLOB { @@ -326,16 +331,28 @@ pub fn encrypt_password(password: &str) -> Result { 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 { cbData: 0, pbData: ptr::null_mut(), }; + let wide_description: Vec = OsStrExt::encode_wide(OsStr::new(description)) + .chain(Some(0)) + .collect(); + + let p_description: LPCWSTR = wide_description.as_ptr(); + let result = unsafe { CryptProtectData( &mut input_blob, - ptr::null(), - ptr::null_mut(), + p_description, + &mut entropy_blob, ptr::null_mut(), ptr::null_mut(), CRYPTPROTECT_UI_FORBIDDEN, @@ -354,66 +371,7 @@ pub fn encrypt_password(password: &str) -> Result { 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) -} - -#[cfg(windows)] -pub fn decrypt_password(encrypted_password_base64: &str) -> Result { - 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)) -} +} \ No newline at end of file diff --git a/src/lib/osuUtil.ts b/src/lib/osuUtil.ts index a998896..eabe9bc 100644 --- a/src/lib/osuUtil.ts +++ b/src/lib/osuUtil.ts @@ -138,4 +138,6 @@ export const hasWMCTRL = async () => export const hasOsuWinello = async () => await invoke('has_osuwinello'); export const hasNet8 = async () => - await invoke('has_net8'); \ No newline at end of file + await invoke('has_net8'); +export const encryptString = async (str: string, entropy: string) => + await invoke('encrypt_string', {string: str, entropy}); \ No newline at end of file diff --git a/src/pages/Launch.svelte b/src/pages/Launch.svelte index 36c20c6..dc8b60e 100644 --- a/src/pages/Launch.svelte +++ b/src/pages/Launch.svelte @@ -83,6 +83,7 @@ import { osuapi } from '@/api/osuapi'; import { downloadEZPPLauncherUpdateFiles, + encryptString, exit, getBeatmapSetsCount, getEZPPLauncherUpdateFiles, @@ -296,7 +297,7 @@ }, { key: 'Password', - value: password, + value: $platform === "windows" ? await encryptString(password, "cu24180ncjeiu0ci1nwui") : password, }, { key: 'SaveUsername', diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 31e3bd1..199c116 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -26,7 +26,7 @@ import { Buffer } from 'buffer'; import { Toaster } from '@/components/ui/sonner'; 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 * as presence from '@/presence'; @@ -88,6 +88,13 @@ onMount(async () => { 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(); setupValues(); launcherVersion.set(await getLauncherVersion());