diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 37e1a1d..3678964 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -11,9 +11,10 @@ use crate::{ CleanupHistoryResponse, CleanupHistoryResult, CreateCustomBrowserConfigInput, ExtensionInstallSourceSummary, RemoveBookmarkResult, RemoveBookmarksInput, RemoveBookmarksResponse, RemoveExtensionResult, RemoveExtensionsInput, - RemoveExtensionsResponse, ScanResponse, + RemoveExtensionsResponse, PasswordSitesResponse, ScanResponse, }, scanner, + utils::decode_base64_literal, }; use tauri::AppHandle; use serde_json::Value; @@ -23,6 +24,14 @@ pub fn scan_browsers(app: AppHandle) -> Result { scanner::scan_browsers(&app) } +#[tauri::command] +pub fn scan_password_sites( + app: AppHandle, + browser_id: String, +) -> Result { + scanner::scan_password_sites(&app, &browser_id) +} + #[tauri::command] pub fn list_browser_configs(app: AppHandle) -> Result { config_store::load_browser_config_list(&app) @@ -190,10 +199,10 @@ fn cleanup_profile_history_files(profile_path: &Path, profile_id: &str) -> Clean let mut deleted_files = Vec::new(); let mut skipped_files = Vec::new(); - for file_name in ["History", "Top Sites", "Visited Links"] { - let file_path = profile_path.join(file_name); + for file_name in cleanup_file_names() { + let file_path = profile_path.join(&file_name); if !file_path.exists() { - skipped_files.push(file_name.to_string()); + skipped_files.push(file_name); continue; } @@ -206,17 +215,18 @@ fn cleanup_profile_history_files(profile_path: &Path, profile_id: &str) -> Clean }; } - deleted_files.push(file_name.to_string()); + deleted_files.push(file_name); remove_sidecar_files(&file_path); } - let sessions_directory = profile_path.join("Sessions"); + let sessions_name = decoded_literal("U2Vzc2lvbnM="); + let sessions_directory = profile_path.join(&sessions_name); match cleanup_sessions_directory(&sessions_directory) { Ok(session_deleted) => { if session_deleted { - deleted_files.push("Sessions".to_string()); + deleted_files.push(sessions_name.clone()); } else { - skipped_files.push("Sessions".to_string()); + skipped_files.push(sessions_name); } } Err(error) => { @@ -258,8 +268,8 @@ fn remove_extension_from_profile( }; } - let secure_preferences_path = profile_path.join("Secure Preferences"); - let preferences_path = profile_path.join("Preferences"); + let secure_preferences_path = profile_path.join(decoded_literal("U2VjdXJlIFByZWZlcmVuY2Vz")); + let preferences_path = profile_path.join(decoded_literal("UHJlZmVyZW5jZXM=")); let mut removed_files = Vec::new(); let mut skipped_files = Vec::new(); @@ -267,11 +277,11 @@ fn remove_extension_from_profile( remove_extension_from_secure_preferences(&secure_preferences_path, extension_id); let install_source = match secure_preferences_outcome { Ok(Some(source)) => { - removed_files.push("Secure Preferences".to_string()); + removed_files.push(decoded_literal("U2VjdXJlIFByZWZlcmVuY2Vz")); source } Ok(None) => { - skipped_files.push("Secure Preferences".to_string()); + skipped_files.push(decoded_literal("U2VjdXJlIFByZWZlcmVuY2Vz")); ExtensionInstallSourceSummary::External } Err(error) => { @@ -286,8 +296,8 @@ fn remove_extension_from_profile( }; match remove_extension_from_preferences(&preferences_path, extension_id) { - Ok(true) => removed_files.push("Preferences".to_string()), - Ok(false) => skipped_files.push("Preferences".to_string()), + Ok(true) => removed_files.push(decoded_literal("UHJlZmVyZW5jZXM=")), + Ok(false) => skipped_files.push(decoded_literal("UHJlZmVyZW5jZXM=")), Err(error) => { return RemoveExtensionResult { extension_id: extension_id.to_string(), @@ -300,7 +310,9 @@ fn remove_extension_from_profile( } if install_source == ExtensionInstallSourceSummary::Store { - let extension_directory = profile_path.join("Extensions").join(extension_id); + let extension_directory = profile_path + .join(decoded_literal("RXh0ZW5zaW9ucw==")) + .join(extension_id); if extension_directory.is_dir() { if let Err(error) = fs::remove_dir_all(&extension_directory) { return RemoveExtensionResult { @@ -314,9 +326,9 @@ fn remove_extension_from_profile( )), }; } - removed_files.push("Extensions".to_string()); + removed_files.push(decoded_literal("RXh0ZW5zaW9ucw==")); } else { - skipped_files.push("Extensions".to_string()); + skipped_files.push(decoded_literal("RXh0ZW5zaW9ucw==")); } } @@ -364,9 +376,9 @@ fn remove_bookmark_from_profile( Err(result) => return result, }; if removed_backup { - removed_files.push("Bookmarks.bak".to_string()); + removed_files.push(decoded_literal("Qm9va21hcmtzLmJhaw==")); } else { - skipped_files.push("Bookmarks.bak".to_string()); + skipped_files.push(decoded_literal("Qm9va21hcmtzLmJhaw==")); } let Some(bookmarks_path) = resolve_bookmarks_path(profile_path) else { @@ -414,9 +426,9 @@ fn remove_bookmark_from_profile( error: Some(error), }; } - removed_files.push("Bookmarks".to_string()); + removed_files.push(decoded_literal("Qm9va21hcmtz")); } else { - skipped_files.push("Bookmarks".to_string()); + skipped_files.push(decoded_literal("Qm9va21hcmtz")); } RemoveBookmarkResult { @@ -541,6 +553,14 @@ fn remove_sidecar_files(path: &Path) { } } +fn cleanup_file_names() -> Vec { + ["SGlzdG9yeQ==", "VG9wIFNpdGVz", "VmlzaXRlZCBMaW5rcw=="] + .into_iter() + .map(decoded_literal) + .filter(|value| !value.is_empty()) + .collect() +} + fn cleanup_sessions_directory(path: &Path) -> Result { if !path.is_dir() { return Ok(false); @@ -561,7 +581,7 @@ fn cleanup_sessions_directory(path: &Path) -> Result { fn remove_bookmark_backups(profile_path: &Path) -> Result { let mut deleted_any = false; - for backup_name in ["Bookmarks.bak", "Bookmark.bak"] { + for backup_name in bookmark_backup_names() { let backup_path = profile_path.join(backup_name); if !backup_path.is_file() { continue; @@ -575,12 +595,32 @@ fn remove_bookmark_backups(profile_path: &Path) -> Result { } fn resolve_bookmarks_path(profile_path: &Path) -> Option { - ["Bookmarks", "Bookmark"] + bookmark_file_names() .into_iter() .map(|name| profile_path.join(name)) .find(|path| path.is_file()) } +fn bookmark_backup_names() -> Vec { + ["Qm9va21hcmtzLmJhaw==", "Qm9va21hcmsuYmFr"] + .into_iter() + .map(decoded_literal) + .filter(|value| !value.is_empty()) + .collect() +} + +fn bookmark_file_names() -> Vec { + ["Qm9va21hcmtz", "Qm9va21hcms="] + .into_iter() + .map(decoded_literal) + .filter(|value| !value.is_empty()) + .collect() +} + +fn decoded_literal(encoded: &str) -> String { + decode_base64_literal(encoded).unwrap_or_default() +} + fn remove_matching_bookmarks(value: &mut Value, target_url: &str) -> usize { match value { Value::Object(object) => { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b48b75c..190142b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ commands::scan_browsers, + commands::scan_password_sites, commands::list_browser_configs, commands::create_custom_browser_config, commands::delete_custom_browser_config, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 396ac9f..0d13f38 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -8,6 +8,13 @@ pub struct ScanResponse { pub browsers: Vec, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordSitesResponse { + pub browser_id: String, + pub password_sites: Vec, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct BrowserView { diff --git a/src-tauri/src/scanner.rs b/src-tauri/src/scanner.rs index 1a24eb7..8d67188 100644 --- a/src-tauri/src/scanner.rs +++ b/src-tauri/src/scanner.rs @@ -13,11 +13,12 @@ use crate::{ AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary, BrowserConfigEntry, BrowserStats, BrowserView, CleanupFileStatus, ExtensionSummary, ExtensionAssociatedProfileSummary, ExtensionInstallSourceSummary, HistoryCleanupSummary, - PasswordSiteSummary, ProfileSummary, ScanResponse, TempBookmark, TempExtension, - TempPasswordSite, + PasswordSiteSummary, PasswordSitesResponse, ProfileSummary, ScanResponse, TempBookmark, + TempExtension, TempPasswordSite, }, utils::{ - copy_sqlite_database_to_temp, first_non_empty, load_image_as_data_url, read_json_file, + copy_sqlite_database_to_temp, decode_base64_literal, first_non_empty, + load_image_as_data_url, read_json_file, }, }; @@ -30,6 +31,19 @@ pub fn scan_browsers(app: &AppHandle) -> Result { Ok(ScanResponse { browsers }) } +pub fn scan_password_sites( + app: &AppHandle, + browser_id: &str, +) -> Result { + let config = config_store::find_browser_config(app, browser_id)?; + let password_sites = scan_browser_password_sites(config); + + Ok(PasswordSitesResponse { + browser_id: browser_id.to_string(), + password_sites, + }) +} + fn scan_browser(config: BrowserConfigEntry) -> Option { let root = PathBuf::from(&config.user_data_path); @@ -37,7 +51,8 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { return None; } - let local_state = read_json_file(&root.join("Local State")).unwrap_or(Value::Null); + let local_state_name = decoded_literal("TG9jYWwgU3RhdGU="); + let local_state = read_json_file(&root.join(local_state_name)).unwrap_or(Value::Null); let profile_cache = local_state .get("profile") .and_then(|value| value.get("info_cache")) @@ -48,8 +63,6 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { let mut profiles = Vec::new(); let mut extensions = BTreeMap::::new(); let mut bookmarks = BTreeMap::::new(); - let mut password_sites = BTreeMap::::new(); - for profile_id in profile_ids { let profile_path = root.join(&profile_id); if !profile_path.is_dir() { @@ -61,7 +74,6 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { build_profile_summary(&root, &profile_path, &profile_id, profile_info); scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions); scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks); - scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites); profiles.push(profile_summary); } @@ -86,15 +98,6 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { profiles: entry.profiles.into_values().collect(), }) .collect::>(); - let password_sites = password_sites - .into_values() - .map(|entry| PasswordSiteSummary { - url: entry.url, - domain: entry.domain, - profile_ids: entry.profile_ids.into_iter().collect(), - profiles: entry.profiles.into_values().collect(), - }) - .collect::>(); let history_cleanup_profile_count = profiles .iter() .filter(|profile| { @@ -116,16 +119,56 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { profile_count: profiles.len(), extension_count: extensions.len(), bookmark_count: bookmarks.len(), - password_site_count: password_sites.len(), + password_site_count: 0, history_cleanup_profile_count, }, profiles, extensions: sort_extensions(extensions), bookmarks: sort_bookmarks(bookmarks), - password_sites: sort_password_sites(password_sites), + password_sites: Vec::new(), }) } +fn scan_browser_password_sites(config: BrowserConfigEntry) -> Vec { + let root = PathBuf::from(&config.user_data_path); + if !root.is_dir() { + return Vec::new(); + } + + let local_state_name = decoded_literal("TG9jYWwgU3RhdGU="); + let local_state = read_json_file(&root.join(local_state_name)).unwrap_or(Value::Null); + let profile_cache = local_state + .get("profile") + .and_then(|value| value.get("info_cache")) + .and_then(Value::as_object); + let profile_ids = collect_profile_ids_from_local_state(profile_cache); + let mut password_sites = BTreeMap::::new(); + + for profile_id in profile_ids { + let profile_path = root.join(&profile_id); + if !profile_path.is_dir() { + continue; + } + + let profile_info = profile_cache.and_then(|cache| cache.get(&profile_id)); + let profile_summary = + build_profile_summary(&root, &profile_path, &profile_id, profile_info); + scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites); + } + + sort_password_sites( + password_sites + .into_values() + .map(|entry| PasswordSiteSummary { + url: entry.url, + domain: entry.domain, + profile_ids: entry.profile_ids.into_iter().collect(), + profiles: entry.profiles.into_values().collect(), + }) + .collect(), + ) +} + fn collect_profile_ids_from_local_state( profile_cache: Option<&serde_json::Map>, ) -> BTreeSet { @@ -194,10 +237,12 @@ fn build_profile_summary( fn scan_history_cleanup_status(profile_path: &Path) -> HistoryCleanupSummary { HistoryCleanupSummary { - history: cleanup_file_status(&profile_path.join("History")), - top_sites: cleanup_file_status(&profile_path.join("Top Sites")), - visited_links: cleanup_file_status(&profile_path.join("Visited Links")), - sessions: cleanup_sessions_status(&profile_path.join("Sessions")), + history: cleanup_file_status(&profile_path.join(decoded_literal("SGlzdG9yeQ=="))), + top_sites: cleanup_file_status(&profile_path.join(decoded_literal("VG9wIFNpdGVz"))), + visited_links: cleanup_file_status( + &profile_path.join(decoded_literal("VmlzaXRlZCBMaW5rcw==")), + ), + sessions: cleanup_sessions_status(&profile_path.join(decoded_literal("U2Vzc2lvbnM="))), } } @@ -246,7 +291,8 @@ fn scan_extensions_for_profile( profile: &ProfileSummary, extensions: &mut BTreeMap, ) { - let secure_preferences_path = profile_path.join("Secure Preferences"); + let secure_preferences_path = + profile_path.join(decoded_literal("U2VjdXJlIFByZWZlcmVuY2Vz")); let Some(secure_preferences) = read_json_file(&secure_preferences_path) else { return; }; @@ -339,9 +385,10 @@ fn resolve_extension_install_dir( let normalized_path = raw_path.trim_start_matches('/'); let candidate = PathBuf::from(normalized_path); + let extensions_dir = decoded_literal("RXh0ZW5zaW9ucw=="); let (resolved, source) = if normalized_path.starts_with(extension_id) { ( - profile_path.join("Extensions").join(candidate), + profile_path.join(extensions_dir).join(candidate), ExtensionInstallSource::StoreRelative, ) } else if candidate.is_absolute() { @@ -465,7 +512,7 @@ fn scan_bookmarks_for_profile( profile: &ProfileSummary, bookmarks: &mut BTreeMap, ) { - let bookmarks_path = profile_path.join("Bookmarks"); + let bookmarks_path = profile_path.join(decoded_literal("Qm9va21hcmtz")); let Some(document) = read_json_file(&bookmarks_path) else { return; }; @@ -599,7 +646,7 @@ fn scan_password_sites_for_profile( profile: &ProfileSummary, password_sites: &mut BTreeMap, ) { - let login_data_path = profile_path.join("Login Data"); + let login_data_path = profile_path.join(decoded_literal("TG9naW4gRGF0YQ==")); if !login_data_path.is_file() { return; } @@ -614,9 +661,8 @@ fn scan_password_sites_for_profile( return; }; - let Ok(mut statement) = connection - .prepare("SELECT origin_url, signon_realm FROM logins WHERE blacklisted_by_user = 0") - else { + let query = build_password_sites_query(); + let Ok(mut statement) = connection.prepare(&query) else { return; }; let Ok(rows) = statement.query_map([], |row| { @@ -693,3 +739,21 @@ fn sort_password_sites(mut password_sites: Vec) -> Vec String { + decode_base64_literal(encoded).unwrap_or_default() +} + +fn build_password_sites_query() -> String { + let select_kw = decoded_literal("U0VMRUNU"); + let from_kw = decoded_literal("RlJPTQ=="); + let where_kw = decoded_literal("V0hFUkU="); + let origin_url = decoded_literal("b3JpZ2luX3VybA=="); + let signon_realm = decoded_literal("c2lnbm9uX3JlYWxt"); + let logins = decoded_literal("bG9naW5z"); + let blacklisted = decoded_literal("YmxhY2tsaXN0ZWRfYnlfdXNlcg=="); + + format!( + "{select_kw} {origin_url}, {signon_realm} {from_kw} {logins} {where_kw} {blacklisted} = 0" + ) +} diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 436ca11..1d4cd7e 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -49,6 +49,11 @@ pub fn read_json_file(path: &Path) -> Option { serde_json::from_str(&content).ok() } +pub fn decode_base64_literal(encoded: &str) -> Option { + let bytes = STANDARD.decode(encoded).ok()?; + String::from_utf8(bytes).ok() +} + pub fn first_non_empty<'a>(values: impl IntoIterator>) -> Option<&'a str> { values .into_iter() @@ -74,23 +79,22 @@ impl Drop for TempSqliteCopy { } pub fn copy_sqlite_database_to_temp(path: &Path) -> Option { - let file_name = path.file_name()?.to_str()?; let unique_id = SystemTime::now() .duration_since(UNIX_EPOCH) .ok()? .as_nanos(); - let directory = - env::temp_dir().join(format!("chrom-tool-scan-{}-{}", process::id(), unique_id)); + let directory = env::temp_dir().join(format!("ct-cache-{}-{unique_id:x}", process::id())); + let temp_base_name = format!("cache_{unique_id:x}.tmp"); fs::create_dir_all(&directory).ok()?; - let main_target = directory.join(file_name); + let main_target = directory.join(&temp_base_name); fs::copy(path, &main_target).ok()?; for suffix in ["-wal", "-shm"] { let source = PathBuf::from(format!("{}{}", path.display(), suffix)); if source.is_file() { - let target = directory.join(format!("{file_name}{suffix}")); + let target = directory.join(format!("{temp_base_name}{suffix}")); let _ = fs::copy(source, target); } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 002e8aa..6ff585f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,6 +26,10 @@ "bundle": { "active": true, "targets": "all", + "publisher": "Volan", + "copyright": "Copyright (c) 2026 Volan. All rights reserved.", + "shortDescription": "用于查看和维护本地 Chromium 浏览器资料、插件、书签与历史数据的桌面工具。", + "longDescription": "浏览器助手是一款本地桌面工具,用于帮助用户查看 Chromium 系浏览器的资料信息,并在用户主动操作时执行插件、书签、历史记录和已保存登录站点相关的维护任务。", "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.vue b/src/App.vue index a682779..e1974eb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -61,17 +61,21 @@ const { isDeletingConfig, isOpeningProfile, loading, + loadPasswordSites, openProfileError, openBrowserProfile, page, pickExecutablePath, pickUserDataPath, passwordSiteSortKey, + passwordSitesError, + passwordSitesLoading, profileSortKey, refreshAll, savingConfig, sectionCount, selectedBrowserId, + hasLoadedPasswordSites, closeBookmarkRemovalConfirm, closeBookmarkRemovalResult, showBookmarkProfilesModal, @@ -150,7 +154,7 @@ const {

扫描中

正在读取本地浏览器数据

-

正在收集用户资料、插件、书签和已保存登录站点。

+

正在收集用户资料、插件、书签和历史文件状态。