diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a3b14..d194041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Features +- Added TEESimulator v4 support for profile apps, keyboxes, and patch levels without replacing its WebUI. Multi-profile setups can select the managed profile in this addon's WebUI. + ## v5.53.1 (2026-05-01) ### Bug Fixes diff --git a/README.md b/README.md index 7f38e18..381f4e4 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ A single native daemon manages all background tasks — if anything dies, it res | **APatch** | 11159+ | Built-in | | **Magisk** | 20.4+ | [KSUWebUIStandalone](https://github.com/5ec1cff/KSUWebUIStandalone) or [WebUI-X](https://github.com/5ec1cff/WebUI-X) required | -**Requires:** [TEESimulator](https://github.com/JingMatrix/TEESimulator) or [TrickyStore](https://github.com/5ec1cff/TrickyStore) installed as the attestation engine. +**Requires:** [TEESimulator v4](https://github.com/JingMatrix/TEESimulator) or [TrickyStore](https://github.com/5ec1cff/TrickyStore) as the attestation engine. --- @@ -144,7 +144,15 @@ During install, press **Vol−** for manual target mode (GMS/GSF only) or **Vol+ Conflicting modules are detected and `rm -rf`'d at install time, so an old TA fork or competing keybox/VBHash module is removed automatically. -The module captures VBHash, builds the exclude list, generates `target.txt`, fetches a valid keybox, sets security patch dates, and starts the daemon. Nothing else to do. +The module captures VBHash, configures protected apps, manages the keybox and patch levels, and starts the daemon. With TEESimulator v4, changes are applied only to the selected native profile. + +TEESimulator and Tricky Addon Enhanced keep separate WebUIs. + +One TEESimulator profile is selected automatically. With multiple profiles, choose one under **Tricky Addon Enhanced → Automation Settings → TEESimulator Profile**, or run: + +```sh +ta-enhanced automation select-profile PROFILE_NAME +``` --- @@ -183,7 +191,7 @@ ta-enhanced config get keybox.source ta-enhanced config set keybox.interval 3600 ``` -Config lives at `/data/adb/tricky_store/config.toml` and is preserved across reinstalls. +Addon config lives at `/data/adb/tricky_store/ta-enhanced/config.toml` and is preserved across reinstalls.
Config Reference @@ -195,7 +203,7 @@ Config lives at `/data/adb/tricky_store/config.toml` and is preserved across rei | `keybox.interval` | `300` | Seconds between fetch attempts | | `security_patch.auto_update` | `true` | Auto patch date updates | | `security_patch.interval` | `86400` | Seconds between patch checks | -| `automation.enabled` | `true` | Auto target.txt population | +| `automation.enabled` | `true` | Auto-populate TrickyStore targets or TEESimulator profile apps | | `automation.use_inotify` | `true` | Use inotify for instant app detection | | `health.enabled` | `true` | Attestation engine health monitor | | `health.interval` | `10` | Seconds between health checks | @@ -208,13 +216,18 @@ Config lives at `/data/adb/tricky_store/config.toml` and is preserved across rei File Locations ``` +/data/adb/teesim/ # TEESimulator-owned files +├── config.json # Profiles, apps, and patch levels +└── *.xml # Profile keyboxes + /data/adb/tricky_store/ -├── config.toml # Module configuration -├── target.txt # Apps to protect -├── keybox.xml # Current keybox -├── keybox.xml.bak # Keybox backup -├── security_patch.txt # Patch dates +├── target.txt # TrickyStore target list / TEESimulator UI mirror +├── keybox.xml # TrickyStore current keybox +├── keybox.xml.bak # TrickyStore keybox backup +├── security_patch.txt # TrickyStore patch dates ├── .health_state # Health monitor state +└── ta-enhanced/ + └── config.toml # Addon configuration /data/adb/Tricky-addon-enhanced/logs/ ├── daemon.log # Unified daemon log diff --git a/action.sh b/action.sh index d5acced..db6dbac 100644 --- a/action.sh +++ b/action.sh @@ -7,6 +7,9 @@ APK_PATH="$TMP_DIR/base.apk" . "$MODPATH/common/common.sh" +WEBUI_MODULE_ID="tricky_store" +[ "$ENGINE" = "teesim" ] && WEBUI_MODULE_ID="TA_enhanced" + download() { PATH=/data/adb/magisk:/data/data/com.termux/files/usr/bin:$PATH if command -v curl >/dev/null 2>&1; then @@ -47,16 +50,16 @@ get_webui() { rm -f "$APK_PATH" echo "- Launching WebUI..." - am start -n "io.github.a13e300.ksuwebui/.WebUIActivity" -e id "tricky_store" + am start -n "io.github.a13e300.ksuwebui/.WebUIActivity" -e id "$WEBUI_MODULE_ID" } if pm path io.github.a13e300.ksuwebui >/dev/null 2>&1; then echo "- Launching WebUI in KSUWebUIStandalone..." - am start -n "io.github.a13e300.ksuwebui/.WebUIActivity" -e id "tricky_store" + am start -n "io.github.a13e300.ksuwebui/.WebUIActivity" -e id "$WEBUI_MODULE_ID" elif pm path com.dergoogler.mmrl.wx > /dev/null 2>&1; then echo "- Launching WebUI in WebUI X..." am start -n "com.dergoogler.mmrl.wx/.ui.activity.webui.WebUIActivity" \ - -e MOD_ID "tricky_store" + -e MOD_ID "$WEBUI_MODULE_ID" else echo "! No WebUI app found" get_webui diff --git a/bin/x86/ta-enhanced b/bin/x86/ta-enhanced index fa1d608..35e5920 100755 Binary files a/bin/x86/ta-enhanced and b/bin/x86/ta-enhanced differ diff --git a/bin/x86_64/ta-enhanced b/bin/x86_64/ta-enhanced index 836f795..d82a084 100755 Binary files a/bin/x86_64/ta-enhanced and b/bin/x86_64/ta-enhanced differ diff --git a/common/common.sh b/common/common.sh index 417ac14..4c7f22a 100644 --- a/common/common.sh +++ b/common/common.sh @@ -33,6 +33,9 @@ RP="/data/adb/tricky_store/ta-enhanced/bin/resetprop-rs" TS="/data/adb/modules/tricky_store" TS_DIR="/data/adb/tricky_store" +# TEESimulator v4 owns /data/adb/teesim and its own WebUI. +. "$MODDIR/common/detect_engine.sh" + # Unified log directory -- shell and Rust daemon both log here LOG_BASE_DIR="/data/adb/tricky_store/ta-enhanced/logs" mkdir -p "$LOG_BASE_DIR" 2>/dev/null || true @@ -157,4 +160,3 @@ ensure_prop() { _log "ERROR" "Failed to ensure: $name" fi } - diff --git a/common/detect_engine.sh b/common/detect_engine.sh new file mode 100644 index 0000000..3f5dd46 --- /dev/null +++ b/common/detect_engine.sh @@ -0,0 +1,15 @@ +# Shared attestation-engine detection. Keep this in sync with rust/src/engine.rs. +ENGINE="tricky_store" +ENGINE_MODULE="/data/adb/modules/tricky_store" + +for _tee_candidate in /data/adb/modules/teesim /data/adb/modules_update/teesim; do + [ -d "$_tee_candidate" ] || continue + [ -f "$_tee_candidate/remove" ] && continue + [ -f "$_tee_candidate/module.prop" ] || continue + grep -q '^id=teesim$' "$_tee_candidate/module.prop" 2>/dev/null || continue + ENGINE="teesim" + ENGINE_MODULE="$_tee_candidate" + break +done + +unset _tee_candidate diff --git a/customize.sh b/customize.sh index 63eda40..b272ba9 100644 --- a/customize.sh +++ b/customize.sh @@ -2,6 +2,7 @@ SKIPUNZIP=0 DEBUG=false COMPATH="$MODPATH/common" TS="/data/adb/modules/tricky_store" +. "$MODPATH/common/detect_engine.sh" SCRIPT_DIR="/data/adb/tricky_store" CONFIG_DIR="$SCRIPT_DIR/target_list_config" MODID=$(grep_prop id "$TMPDIR/module.prop") @@ -38,7 +39,10 @@ else abort " " fi -if [ -d "$TS" ]; then +if [ "$ENGINE" = "teesim" ]; then + engine_name=$(grep_prop name "$ENGINE_MODULE/module.prop") + ui_print " 🔒 ${engine_name:-TEESimulator v4} detected" +elif [ -d "$TS" ]; then engine_name="" if [ -f "$TS/daemon" ]; then engine_name=$(grep -o '\-\-nice-name=[^ ]*' "$TS/daemon" 2>/dev/null | cut -d= -f2) @@ -57,6 +61,7 @@ case "$ABI" in *) abort " ❌ Unsupported ABI: $ABI" ;; esac BIN="$MODPATH/bin/$ABI/ta-enhanced" +initialize # Aggressive conflict purge. Hot-install means we cannot wait for the # manager to process disable+remove on next boot, so rm -rf conflicting @@ -97,6 +102,22 @@ done [ "$PURGED_COUNT" -eq 0 ] && ui_print " ✅ $(_msg no_conflicts)" HAS_TARGET=0 +TEESIM_READY=1 +if [ "$ENGINE" = "teesim" ]; then + if "$BIN" automation profile-ready >/dev/null 2>&1; then + "$BIN" automation export-target >/dev/null 2>&1 \ + || abort " ❌ Failed to read TEESimulator config.json" + elif PROFILE_ERROR=$("$BIN" automation profiles 2>&1); then + TEESIM_READY=0 + ui_print " ⚠️ Select a TEESimulator profile in this addon's WebUI" + ui_print " ℹ️ TEESimulator will remain unchanged until then" + elif [ -f /data/adb/teesim/config.json ]; then + abort " ❌ Invalid TEESimulator config: $PROFILE_ERROR" + else + TEESIM_READY=0 + ui_print " ⚠️ TEESimulator config is not available yet" + fi +fi if [ -f "/data/adb/tricky_store/target.txt" ] && [ -s "/data/adb/tricky_store/target.txt" ]; then HAS_TARGET=1 fi @@ -131,7 +152,6 @@ fi ui_print " " ui_print " 📦 $(_msg installing)" -initialize populate_system_app if [ -x "$BIN" ]; then @@ -169,6 +189,11 @@ else generate_minimal_target fi +if [ "$ENGINE" = "teesim" ] && [ "$TEESIM_READY" = "1" ]; then + "$BIN" automation sync-target >/dev/null 2>&1 \ + || abort " ❌ Failed to update TEESimulator config.json" +fi + TA_DIR="$SCRIPT_DIR/ta-enhanced" mkdir -p "$TA_DIR/logs" @@ -213,14 +238,22 @@ if [ -f "$SCRIPT_DIR/enhanced.conf" ]; then || ui_print " ⚠️ Legacy config migration failed" fi -ui_print " 🛡️ Setting security patch dates..." -if "$BIN" security-patch update --force 2>/dev/null; then - ui_print " ✅ $(_msg sec_patch_ok)" +if [ "$ENGINE" = "teesim" ] && [ "$TEESIM_READY" = "0" ]; then + ui_print " ⚠️ TEESimulator updates paused: no profile selected" + ui_print " ℹ️ CLI: ta-enhanced automation select-profile PROFILE_NAME" else - ui_print " ⚠️ $(_msg sec_patch_fail)" + ui_print " 🛡️ Setting security patch dates..." + if "$BIN" security-patch update --force 2>/dev/null; then + ui_print " ✅ $(_msg sec_patch_ok)" + else + ui_print " ⚠️ $(_msg sec_patch_fail)" + fi fi -if [ -f "$SCRIPT_DIR/keybox.xml" ]; then +if [ "$ENGINE" = "teesim" ] && [ "$TEESIM_READY" = "0" ]; then + : # Do not read or replace a keybox until the user selects its owning profile. +elif { [ "$ENGINE" = "tricky_store" ] && [ -f "$SCRIPT_DIR/keybox.xml" ]; } \ + || { [ "$ENGINE" = "teesim" ] && "$BIN" keybox validate >/dev/null 2>&1; }; then ui_print " 🔑 $(_msg keybox_kept)" elif timeout 3 ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then ui_print " 🔑 $(_msg keybox_fetch)" diff --git a/package.sh b/package.sh index 9876d6a..868f3fa 100755 --- a/package.sh +++ b/package.sh @@ -226,6 +226,7 @@ rm -f "$ZIP_PATH" cd "$REPO_DIR" zip -r9 "$ZIP_PATH" . \ -x ".git/*" \ + -x ".amp/*" \ -x ".claude/*" \ -x ".mcp-vector-search/*" \ -x ".mcp.json" \ diff --git a/post-fs-data.sh b/post-fs-data.sh index 2614669..ae29c10 100644 --- a/post-fs-data.sh +++ b/post-fs-data.sh @@ -24,10 +24,11 @@ while [ -z "$(ls -A /data/adb/modules/ 2>/dev/null)" ]; do sleep 0.5 done _pfd_log "Modules directory ready (waited ${_wait_count} iterations)" +. "$MODPATH/common/detect_engine.sh" -# Self-removal if TrickyStore missing -if [ ! -d "$TS" ] || [ -f "$TS/remove" ]; then - _pfd_log "TrickyStore missing or removing - marking self for removal" +# Self-removal only when neither supported engine is present. +if { [ ! -d "$TS" ] || [ -f "$TS/remove" ]; } && [ "$ENGINE" != "teesim" ]; then + _pfd_log "No supported attestation engine - marking self for removal" if [ -f "$MODPATH/action.sh" ]; then # Magisk hidden module: recreate stub at real ID rm -rf "/data/adb/modules/TA_enhanced" 2>/dev/null @@ -38,10 +39,12 @@ if [ ! -d "$TS" ] || [ -f "$TS/remove" ]; then fi fi -# Clean stale symlinks -[ -L "$TS/webroot" ] && rm -f "$TS/webroot" -[ -L "$TS/action.sh" ] && rm -f "$TS/action.sh" -[ -L "$TS/banner.png" ] && rm -f "$TS/banner.png" +# Clean stale links only from TrickyStore. Never touch TEESimulator's WebUI. +if [ -d "$TS" ] && [ "$ENGINE" != "teesim" ]; then + [ -L "$TS/webroot" ] && rm -f "$TS/webroot" + [ -L "$TS/action.sh" ] && rm -f "$TS/action.sh" + [ -L "$TS/banner.png" ] && rm -f "$TS/banner.png" +fi # Root Manager Detection if [ -n "$APATCH" ]; then diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f8b7ade..2a5d0d3 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1093,6 +1093,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 126ade6..2525b5d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] clap = { version = "= 4.5.57", features = ["derive"] } serde = { version = "= 1.0.228", features = ["derive"] } -serde_json = "= 1.0.149" +serde_json = { version = "= 1.0.149", features = ["preserve_order"] } toml = "= 0.8.23" tracing = { version = "= 0.1.44", features = ["log"] } tracing-subscriber = { version = "= 0.3.22", features = ["env-filter"] } diff --git a/rust/src/automation/mod.rs b/rust/src/automation/mod.rs index 2c2d16e..07ce711 100644 --- a/rust/src/automation/mod.rs +++ b/rust/src/automation/mod.rs @@ -1,9 +1,9 @@ -pub mod watcher; pub mod target; +pub mod watcher; -use serde::Serialize; -use crate::config::Config; use crate::cli::AutomationAction; +use crate::config::Config; +use serde::Serialize; #[derive(Debug, Serialize)] pub struct DaemonStatus { @@ -14,15 +14,53 @@ pub struct DaemonStatus { } pub fn handle_automation(action: AutomationAction, cfg: &Config) -> anyhow::Result<()> { - if !cfg.automation.enabled { - println!("automation disabled"); - return Ok(()); - } - match action { + AutomationAction::SyncTarget => { + crate::engine::import_target_mirror()?; + println!("target synchronized"); + Ok(()) + } + AutomationAction::ExportTarget => { + crate::engine::export_target_mirror()?; + println!("target synchronized"); + Ok(()) + } + AutomationAction::ProfileReady => { + crate::engine::ensure_profile_selected()?; + println!("profile ready"); + Ok(()) + } + AutomationAction::Profiles => { + println!( + "{}", + serde_json::to_string(&crate::engine::profile_status( + &cfg.general.teesim_profile + )?)? + ); + Ok(()) + } + AutomationAction::SelectProfile { name } => { + crate::engine::validate_profile_choice(name.trim())?; + let mut current = Config::load(None)?; + current.set("general.teesim_profile", &name)?; + Config::backup(None)?; + current.save(None)?; + if !name.trim().is_empty() { + crate::engine::export_target_mirror()?; + crate::security_patch::handle_security_patch( + crate::cli::SecurityPatchAction::ExportLegacy, + ¤t, + )?; + } + println!("TEESimulator profile selection updated"); + Ok(()) + } + _ if !cfg.automation.enabled => { + println!("automation disabled"); + Ok(()) + } AutomationAction::Status => { - let status = watcher::show_status(); - println!("{}", serde_json::to_string_pretty(&status)?); + println!("{}", serde_json::to_string_pretty(&watcher::show_status())?); Ok(()) } AutomationAction::Check => { diff --git a/rust/src/automation/target.rs b/rust/src/automation/target.rs index a61fd63..c064f2d 100644 --- a/rust/src/automation/target.rs +++ b/rust/src/automation/target.rs @@ -1,42 +1,35 @@ -use std::path::Path; use crate::platform::fs::atomic_write; +use std::path::Path; -const TARGET_FILE: &str = "/data/adb/tricky_store/target.txt"; pub(crate) const AUTO_ADDED: &str = "/data/adb/tricky_store/.automation/auto_added.txt"; pub fn read_target() -> anyhow::Result> { - let path = Path::new(TARGET_FILE); - if !path.exists() { - return Ok(Vec::new()); - } - let content = std::fs::read_to_string(path)?; - Ok(content - .lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .map(|l| strip_suffix(l).to_string()) + Ok(crate::engine::read_targets()? + .into_iter() + .map(|line| strip_suffix(&line).to_owned()) .collect()) } pub fn read_target_raw() -> anyhow::Result> { - let path = Path::new(TARGET_FILE); - if !path.exists() { - return Ok(Vec::new()); + if crate::engine::Engine::detect() == crate::engine::Engine::TrickyStore { + let path = Path::new(crate::engine::TARGET_MIRROR); + if !path.exists() { + return Ok(Vec::new()); + } + return Ok(std::fs::read_to_string(path)? + .lines() + .map(|line| line.trim().to_owned()) + .filter(|line| !line.is_empty()) + .collect()); } - let content = std::fs::read_to_string(path)?; - Ok(content - .lines() - .map(|l| l.trim().to_string()) - .filter(|l| !l.is_empty()) - .collect()) + crate::engine::read_targets() } pub fn write_target(entries: &[String]) -> anyhow::Result<()> { - let mut content = entries.join("\n"); - if !content.is_empty() { - content.push('\n'); + if crate::engine::Engine::detect() == crate::engine::Engine::TrickyStore { + return crate::engine::write_target_mirror(entries); } - atomic_write(Path::new(TARGET_FILE), content.as_bytes()) + crate::engine::write_targets(entries) } pub fn add_package(pkg: &str, exclude_list: &[String]) -> anyhow::Result { diff --git a/rust/src/automation/watcher.rs b/rust/src/automation/watcher.rs index ce2e005..5ecf044 100644 --- a/rust/src/automation/watcher.rs +++ b/rust/src/automation/watcher.rs @@ -69,7 +69,7 @@ pub fn cleanup_dead_apps() -> anyhow::Result { let mut removed = 0u32; for pkg in &target_list { - if installed.contains(pkg) || app_data_exists(pkg) { + if pkg.starts_with("uid:") || installed.contains(pkg) || app_data_exists(pkg) { continue; } if target::remove_package(pkg)? { diff --git a/rust/src/cli/applist.rs b/rust/src/cli/applist.rs index 67ff8e6..3569342 100644 --- a/rust/src/cli/applist.rs +++ b/rust/src/cli/applist.rs @@ -1,11 +1,11 @@ -use std::path::Path; -use std::process::Command; -use serde::Serialize; -use crate::config::Config; +use super::ApplistAction; use crate::automation::target; use crate::automation::watcher; +use crate::config::Config; use crate::platform::packages; -use super::ApplistAction; +use serde::Serialize; +use std::path::Path; +use std::process::Command; const TA_DIR: &str = "/data/adb/tricky_store/ta-enhanced"; @@ -63,7 +63,11 @@ fn build_applist(cfg: &Config) -> anyhow::Result> { let bare = line.trim_end_matches('!').trim_end_matches('?'); let is_excluded = exclude.iter().any(|p| { - if p.ends_with('*') { bare.starts_with(&p[..p.len() - 1]) } else { bare == p } + if p.ends_with('*') { + bare.starts_with(&p[..p.len() - 1]) + } else { + bare == p + } }); if is_excluded { continue; diff --git a/rust/src/cli/handlers.rs b/rust/src/cli/handlers.rs index 171d08e..6ccafb4 100644 --- a/rust/src/cli/handlers.rs +++ b/rust/src/cli/handlers.rs @@ -28,7 +28,9 @@ pub fn dispatch(command: Commands, cfg: &Config) -> anyhow::Result<()> { Commands::DaemonStop => crate::daemon::handle_daemon_stop(), Commands::Config { action } => crate::config::handle_config(action, cfg), Commands::Keybox { action } => crate::keybox::handle_keybox(action, cfg), - Commands::SecurityPatch { action } => crate::security_patch::handle_security_patch(action, cfg), + Commands::SecurityPatch { action } => { + crate::security_patch::handle_security_patch(action, cfg) + } Commands::Conflict { action } => crate::conflict::handle_conflict(action, cfg), Commands::Vbhash { action } => crate::vbhash::handle_vbhash(action, cfg), Commands::Health { action } => crate::health::handle_health(action, cfg), diff --git a/rust/src/cli/mod.rs b/rust/src/cli/mod.rs index cb0733b..6c2f1b1 100644 --- a/rust/src/cli/mod.rs +++ b/rust/src/cli/mod.rs @@ -1,8 +1,8 @@ use clap::{Parser, Subcommand}; +pub mod applist; pub mod handlers; pub mod webui_init; -pub mod applist; #[derive(Parser)] #[command(name = "ta-enhanced", version = env!("CARGO_PKG_VERSION"))] @@ -72,8 +72,13 @@ pub enum Commands { #[derive(Subcommand)] pub enum ConfigAction { - Get { key: String }, - Set { key: String, value: String }, + Get { + key: String, + }, + Set { + key: String, + value: String, + }, Migrate, List, Init { @@ -93,9 +98,13 @@ pub enum ConfigAction { #[derive(Subcommand)] pub enum KeyboxAction { Fetch, - Validate { path: Option }, + Validate { + path: Option, + }, #[command(name = "set-custom")] - SetCustom { path: String }, + SetCustom { + path: String, + }, Sources, Generate, Backup, @@ -115,6 +124,10 @@ pub enum SecurityPatchAction { boot: String, vendor: String, }, + #[command(name = "import-legacy")] + ImportLegacy, + #[command(name = "export-legacy")] + ExportLegacy, } #[derive(Subcommand)] @@ -150,8 +163,19 @@ pub enum StatusAction { #[derive(Subcommand)] pub enum AutomationAction { Status, + Profiles, + #[command(name = "select-profile")] + SelectProfile { + name: String, + }, Check, Cleanup, + #[command(name = "sync-target")] + SyncTarget, + #[command(name = "export-target")] + ExportTarget, + #[command(name = "profile-ready")] + ProfileReady, } #[derive(Subcommand)] @@ -175,7 +199,9 @@ pub enum ModuleAction { Uninstall, #[command(name = "update-locales")] UpdateLocales, - Download { url: String }, + Download { + url: String, + }, } pub fn dispatch(command: Commands, cfg: &crate::config::Config) -> anyhow::Result<()> { diff --git a/rust/src/cli/webui_init.rs b/rust/src/cli/webui_init.rs index 0210c9b..8e1d6fd 100644 --- a/rust/src/cli/webui_init.rs +++ b/rust/src/cli/webui_init.rs @@ -1,12 +1,8 @@ -use serde::Serialize; -use std::path::Path; use crate::config::Config; +use serde::Serialize; const VERSION: &str = env!("CARGO_PKG_VERSION"); -const KEYBOX_PATH: &str = "/data/adb/tricky_store/keybox.xml"; -const TARGET_PATH: &str = "/data/adb/tricky_store/target.txt"; const BOOT_HASH_PATH: &str = "/data/adb/boot_hash"; -const SP_PATH: &str = "/data/adb/tricky_store/security_patch.txt"; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -105,32 +101,21 @@ fn read_module_prop(key: &str) -> Option { } fn count_target_entries() -> u32 { - std::fs::read_to_string(TARGET_PATH) - .map(|c| c.lines().filter(|l| !l.trim().is_empty() && !l.starts_with('#')).count() as u32) + crate::engine::read_targets() + .map(|targets| targets.len() as u32) .unwrap_or(0) } fn read_patch_dates() -> (String, String, String) { - let Ok(content) = std::fs::read_to_string(SP_PATH) else { - return (String::new(), String::new(), String::new()); - }; - let mut system = String::new(); - let mut boot = String::new(); - let mut vendor = String::new(); - for line in content.lines() { - if let Some(val) = line.strip_prefix("system=") { - system = val.trim().into(); - } else if let Some(val) = line.strip_prefix("boot=") { - boot = val.trim().into(); - } else if let Some(val) = line.strip_prefix("vendor=") { - vendor = val.trim().into(); - } - } - (system, boot, vendor) + crate::engine::read_patch_dates().unwrap_or_default() } fn check_keybox() -> (bool, String, Vec) { - let path = Path::new(KEYBOX_PATH); + let keybox = match crate::engine::Engine::detect().keybox_path() { + Ok(path) => path, + Err(error) => return (false, "none".into(), vec![error.to_string()]), + }; + let path = keybox.as_path(); if !path.exists() { return (false, "none".into(), vec!["keybox.xml not found".into()]); } @@ -167,27 +152,51 @@ fn detect_aosp_device() -> bool { fn build_conflicts(cfg: &Config) -> ConflictReport { if !cfg.conflict.enabled { - return ConflictReport { modules: Vec::new(), apps: Vec::new() }; + return ConflictReport { + modules: Vec::new(), + apps: Vec::new(), + }; } let status = match crate::conflict::check_all(false) { Ok(s) => s, - Err(_) => return ConflictReport { modules: Vec::new(), apps: Vec::new() }, + Err(_) => { + return ConflictReport { + modules: Vec::new(), + apps: Vec::new(), + } + } }; - let mut modules: Vec = status.aggressive_conflicts.iter() - .map(|id| ConflictModule { id: id.clone(), name: id.clone(), reason: "aggressive".into() }) + let mut modules: Vec = status + .aggressive_conflicts + .iter() + .map(|id| ConflictModule { + id: id.clone(), + name: id.clone(), + reason: "aggressive".into(), + }) .collect(); - modules.extend(status.regular_conflicts.iter() - .map(|id| ConflictModule { id: id.clone(), name: id.clone(), reason: "regular".into() })); + modules.extend(status.regular_conflicts.iter().map(|id| ConflictModule { + id: id.clone(), + name: id.clone(), + reason: "regular".into(), + })); - let apps: Vec = status.app_conflicts.iter() - .map(|pkg| ConflictApp { package_name: pkg.clone(), name: pkg.clone(), reason: "conflicting app".into() }) + let apps: Vec = status + .app_conflicts + .iter() + .map(|pkg| ConflictApp { + package_name: pkg.clone(), + name: pkg.clone(), + reason: "conflicting app".into(), + }) .collect(); ConflictReport { modules, apps } } pub fn handle_webui_init(cfg: &Config) -> anyhow::Result<()> { + let teesim_v4 = crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4; let engine = crate::health::detect_engine(); let engine_running = crate::health::is_engine_enabled(); let total = count_target_entries(); @@ -198,20 +207,23 @@ pub fn handle_webui_init(cfg: &Config) -> anyhow::Result<()> { .map(|h| h.trim().len() == 64 && h.trim().chars().all(|c| c.is_ascii_hexdigit())) .unwrap_or(false); - let restart_count = crate::health::read_state() - .map(|s| s.restarts).unwrap_or(0); + let restart_count = crate::health::read_state().map(|s| s.restarts).unwrap_or(0); - let ts_prop = std::fs::read_to_string("/data/adb/modules/tricky_store/module.prop") + let ts_prop = crate::engine::Engine::detect() + .module_dir() + .and_then(|path| std::fs::read_to_string(path.join("module.prop")).ok()) .unwrap_or_default(); - let ts_ver: u32 = ts_prop.lines() + let ts_ver: u32 = ts_prop + .lines() .find(|l| l.starts_with("versionCode=")) .and_then(|l| l.split_once('=')?.1.trim().parse().ok()) .unwrap_or(0); let has_james = ts_prop.contains("James"); let has_beakthoven = ts_prop.contains("beakthoven"); let has_jingmatrix = ts_prop.contains("JingMatrix"); - let ts_fork_supported = has_james || has_beakthoven || has_jingmatrix || ts_ver >= 158; - let ts_james_fork = has_james && !has_beakthoven; + let ts_fork_supported = + teesim_v4 || has_james || has_beakthoven || has_jingmatrix || ts_ver >= 158; + let ts_james_fork = !teesim_v4 && has_james && !has_beakthoven; let magisk_available = std::process::Command::new("sh") .args(["-c", "command -v magisk"]) @@ -224,7 +236,9 @@ pub fn handle_webui_init(cfg: &Config) -> anyhow::Result<()> { id: read_module_prop("id").unwrap_or_else(|| "TA_enhanced".into()), name: read_module_prop("name").unwrap_or_else(|| "Tricky Addon Enhanced".into()), version: read_module_prop("version").unwrap_or_else(|| format!("v{VERSION}")), - version_code: read_module_prop("versionCode").and_then(|v| v.parse().ok()).unwrap_or(0), + version_code: read_module_prop("versionCode") + .and_then(|v| v.parse().ok()) + .unwrap_or(0), author: read_module_prop("author").unwrap_or_else(|| "KOWX712, Enginex0".into()), }, config: cfg.clone(), @@ -232,7 +246,11 @@ pub fn handle_webui_init(cfg: &Config) -> anyhow::Result<()> { engine_running, active_apps: total, total_targeted: total, - keybox_label: if kb_valid { cfg.keybox.source.clone() } else { "none".into() }, + keybox_label: if kb_valid { + cfg.keybox.source.clone() + } else { + "none".into() + }, patch_level: system.clone(), vbhash_active, engine, @@ -245,7 +263,11 @@ pub fn handle_webui_init(cfg: &Config) -> anyhow::Result<()> { conflicts: build_conflicts(cfg), keybox: KeyboxInfo { valid: kb_valid, - source: if kb_valid { cfg.keybox.source.clone() } else { "none".into() }, + source: if kb_valid { + cfg.keybox.source.clone() + } else { + "none".into() + }, root_type: kb_root_type, last_fetch: None, validation_errors: kb_errors, diff --git a/rust/src/config/migrate.rs b/rust/src/config/migrate.rs index fa862f0..d0d1ab5 100644 --- a/rust/src/config/migrate.rs +++ b/rust/src/config/migrate.rs @@ -17,7 +17,10 @@ const MIGRATION_MAP: &[(&str, &str)] = &[ pub fn migrate_ini_to_toml(ini_path: &Path, toml_path: &Path) -> anyhow::Result<()> { if toml_path.exists() { - tracing::info!("TOML config already exists at {}, skipping migration", toml_path.display()); + tracing::info!( + "TOML config already exists at {}, skipping migration", + toml_path.display() + ); return Ok(()); } @@ -41,7 +44,10 @@ pub fn migrate_ini_to_toml(ini_path: &Path, toml_path: &Path) -> anyhow::Result< for key in ini_map.keys() { if !MIGRATION_MAP.iter().any(|(k, _)| k == key) { - tracing::warn!("unmapped legacy config key: {} (preserved in .conf.bak)", key); + tracing::warn!( + "unmapped legacy config key: {} (preserved in .conf.bak)", + key + ); } } } diff --git a/rust/src/config/mod.rs b/rust/src/config/mod.rs index 3689a2f..f5459a6 100644 --- a/rust/src/config/mod.rs +++ b/rust/src/config/mod.rs @@ -1,9 +1,9 @@ pub mod migrate; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; -use anyhow::anyhow; -use serde::{Serialize, Deserialize}; pub static SELF_WRITE: AtomicBool = AtomicBool::new(false); @@ -28,11 +28,15 @@ pub struct Config { #[serde(default)] pub struct GeneralConfig { pub module_id: String, + pub teesim_profile: String, } impl Default for GeneralConfig { fn default() -> Self { - Self { module_id: "TA_enhanced".into() } + Self { + module_id: "TA_enhanced".into(), + teesim_profile: String::new(), + } } } @@ -165,7 +169,10 @@ pub struct ConflictConfig { impl Default for ConflictConfig { fn default() -> Self { - Self { enabled: true, auto_remove: false } + Self { + enabled: true, + auto_remove: false, + } } } @@ -225,15 +232,17 @@ pub struct UiConfig { impl Default for UiConfig { fn default() -> Self { - Self { language: "en".into() } + Self { + language: "en".into(), + } } } pub const DEFAULT_CONFIG_PATH: &str = "/data/adb/tricky_store/ta-enhanced/config.toml"; const SUPPORTED_LANGS: &[&str] = &[ - "ar", "az", "bn", "de", "el", "en", "es-ES", "fa", "fr", "id", "it", - "ja", "ko", "pl", "pt-BR", "ru", "th", "tl", "tr", "uk", "vi", "zh-CN", "zh-TW", + "ar", "az", "bn", "de", "el", "en", "es-ES", "fa", "fr", "id", "it", "ja", "ko", "pl", "pt-BR", + "ru", "th", "tl", "tr", "uk", "vi", "zh-CN", "zh-TW", ]; fn is_supported_lang(code: &str) -> bool { @@ -250,19 +259,43 @@ fn parse_bool(s: &str) -> anyhow::Result { const ALL_KEYS: &[&str] = &[ "general.module_id", - "keybox.enabled", "keybox.interval", "keybox.source", "keybox.custom_url", - "keybox.boot_retries", "keybox.retry_delay", - "security_patch.auto_update", "security_patch.interval", - "security_patch.custom_date", "security_patch.boot_retries", - "automation.enabled", "automation.interval", "automation.use_inotify", - "automation.exclude_list", "automation.merge_denylist", - "health.enabled", "health.interval", "health.grace_period", - "health.max_restarts", "health.backoff_init", "health.backoff_cap", - "status.enabled", "status.interval", "status.emoji", + "general.teesim_profile", + "keybox.enabled", + "keybox.interval", + "keybox.source", + "keybox.custom_url", + "keybox.boot_retries", + "keybox.retry_delay", + "security_patch.auto_update", + "security_patch.interval", + "security_patch.custom_date", + "security_patch.boot_retries", + "automation.enabled", + "automation.interval", + "automation.use_inotify", + "automation.exclude_list", + "automation.merge_denylist", + "health.enabled", + "health.interval", + "health.grace_period", + "health.max_restarts", + "health.backoff_init", + "health.backoff_cap", + "status.enabled", + "status.interval", + "status.emoji", "vbhash.enabled", - "conflict.enabled", "conflict.auto_remove", - "region.enabled", "region.hwc", "region.hwcountry", "region.mod_device", "region.hardware_sku", - "logging.level", "logging.max_size_mb", "logging.max_files", "logging.log_dir", + "conflict.enabled", + "conflict.auto_remove", + "region.enabled", + "region.hwc", + "region.hwcountry", + "region.mod_device", + "region.hardware_sku", + "logging.level", + "logging.max_size_mb", + "logging.max_files", + "logging.log_dir", "ui.language", ]; @@ -272,9 +305,7 @@ impl Config { if !path.exists() { return Ok(Self::default()); } - let content = std::fs::read_to_string(path)?; - let mut config: Config = toml::from_str(&content)?; - let warnings = config.validate(); + let (config, warnings) = Self::read_validated(path)?; for w in &warnings { tracing::warn!("{}", w); } @@ -285,6 +316,13 @@ impl Config { Ok(config) } + fn read_validated(path: &Path) -> anyhow::Result<(Self, Vec)> { + let content = std::fs::read_to_string(path)?; + let mut config: Config = toml::from_str(&content)?; + let warnings = config.validate(); + Ok((config, warnings)) + } + pub fn save(&self, path: Option<&Path>) -> anyhow::Result<()> { let path = path.unwrap_or(Path::new(DEFAULT_CONFIG_PATH)); let content = toml::to_string_pretty(self)?; @@ -318,6 +356,7 @@ impl Config { pub fn get(&self, key: &str) -> Option { match key { "general.module_id" => Some(self.general.module_id.clone()), + "general.teesim_profile" => Some(self.general.teesim_profile.clone()), "keybox.enabled" => Some(self.keybox.enabled.to_string()), "keybox.interval" => Some(self.keybox.interval.to_string()), "keybox.source" => Some(self.keybox.source.clone()), @@ -361,6 +400,20 @@ impl Config { pub fn set(&mut self, key: &str, value: &str) -> anyhow::Result<()> { match key { + "general.teesim_profile" => { + let value = value.trim(); + if !value.is_empty() + && (value.len() > 32 + || !value + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')) + { + return Err(anyhow!( + "TEESimulator profile must be 1-32 letters, digits, '-' or '_'" + )); + } + self.general.teesim_profile = value.to_string(); + } "keybox.enabled" => self.keybox.enabled = parse_bool(value)?, "keybox.interval" => self.keybox.interval = value.parse()?, "keybox.source" => self.keybox.source = value.to_string(), @@ -376,7 +429,8 @@ impl Config { "automation.use_inotify" => self.automation.use_inotify = parse_bool(value)?, "automation.merge_denylist" => self.automation.merge_denylist = parse_bool(value)?, "automation.exclude_list" => { - self.automation.exclude_list = value.split(',') + self.automation.exclude_list = value + .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); @@ -420,14 +474,21 @@ impl Config { macro_rules! clamp_min { ($field:expr, $min:expr, $name:expr) => { if $field < $min { - warnings.push(format!("{}: clamped {} -> {} (minimum)", $name, $field, $min)); + warnings.push(format!( + "{}: clamped {} -> {} (minimum)", + $name, $field, $min + )); $field = $min; } }; } clamp_min!(self.keybox.interval, 60, "keybox.interval"); - clamp_min!(self.security_patch.interval, 3600, "security_patch.interval"); + clamp_min!( + self.security_patch.interval, + 3600, + "security_patch.interval" + ); clamp_min!(self.automation.interval, 5, "automation.interval"); clamp_min!(self.health.interval, 5, "health.interval"); clamp_min!(self.status.interval, 10, "status.interval"); @@ -445,7 +506,10 @@ impl Config { match self.keybox.source.as_str() { "yurikey" | "upstream" | "custom" => {} other => { - warnings.push(format!("keybox.source: legacy value '{}' migrated to 'yurikey'", other)); + warnings.push(format!( + "keybox.source: legacy value '{}' migrated to 'yurikey'", + other + )); self.keybox.source = "yurikey".into(); } } @@ -455,11 +519,15 @@ impl Config { } if !is_supported_lang(&self.ui.language) { let prev = std::mem::take(&mut self.ui.language); - warnings.push(format!("ui.language: unsupported value '{}' reset to 'en'", prev)); + warnings.push(format!( + "ui.language: unsupported value '{}' reset to 'en'", + prev + )); self.ui.language = "en".into(); } if !self.logging.log_dir.starts_with("/data/adb/") { - warnings.push("logging.log_dir: reset to default (must be under /data/adb/)".to_string()); + warnings + .push("logging.log_dir: reset to default (must be under /data/adb/)".to_string()); self.logging.log_dir = "/data/adb/tricky_store/ta-enhanced/logs".into(); } @@ -500,10 +568,7 @@ impl Config { } } -pub fn handle_config( - action: crate::cli::ConfigAction, - cfg: &Config, -) -> anyhow::Result<()> { +pub fn handle_config(action: crate::cli::ConfigAction, cfg: &Config) -> anyhow::Result<()> { use crate::cli::ConfigAction; match action { ConfigAction::Get { key } => { @@ -514,11 +579,13 @@ pub fn handle_config( Ok(()) } ConfigAction::Set { key, value } => { - let mut cfg = cfg.clone(); - cfg.set(&key, &value)?; + if key == "general.teesim_profile" { + crate::engine::validate_profile_choice(value.trim())?; + } + let mut current = Config::load(None)?; + current.set(&key, &value)?; Config::backup(None)?; - cfg.save(None)?; - Ok(()) + current.save(None) } ConfigAction::Migrate => { let ini_path = std::path::Path::new("/data/adb/tricky_store/enhanced.conf"); diff --git a/rust/src/daemon/tasks.rs b/rust/src/daemon/tasks.rs index 0fe1894..e365ecf 100644 --- a/rust/src/daemon/tasks.rs +++ b/rust/src/daemon/tasks.rs @@ -3,7 +3,6 @@ use std::path::Path; use crate::config::Config; use crate::platform::network::wait_for_network; -const TS_DIR: &str = "/data/adb/tricky_store"; const DATA_DIR: &str = "/data/adb/tricky_store/ta-enhanced"; pub struct TaskBackoff(pub u32); @@ -98,8 +97,8 @@ impl DaemonTask for AutomationTask { if pending.exists() { match std::fs::read_to_string(&pending) { Ok(content) => { - let target = Path::new(TS_DIR).join("target.txt"); - if let Err(e) = crate::platform::fs::atomic_write(&target, content.as_bytes()) { + let entries: Vec = content.lines().map(str::to_owned).collect(); + if let Err(e) = crate::automation::target::write_target(&entries) { tracing::warn!("applist.pending apply failed: {e}"); } else { let _ = std::fs::remove_file(&pending); @@ -180,7 +179,11 @@ impl DaemonTask for KeyboxTask { fn run(&mut self, config: &Config, _manager: Option<&str>) -> Result<(), TaskBackoff> { if !self.boot_done { if !wait_for_network(7) { - if Path::new("/data/adb/tricky_store/keybox.xml").exists() { + if crate::engine::Engine::detect() + .keybox_path() + .map(|path| path.exists()) + .unwrap_or(false) + { tracing::info!("no network at boot, keeping existing keybox"); self.boot_done = true; return Ok(()); @@ -272,4 +275,3 @@ impl DaemonTask for SecurityPatchTask { Ok(()) } } - diff --git a/rust/src/engine.rs b/rust/src/engine.rs new file mode 100644 index 0000000..5035273 --- /dev/null +++ b/rust/src/engine.rs @@ -0,0 +1,797 @@ +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use serde::Serialize; +use serde_json::{Map, Value}; + +use crate::platform::fs::atomic_write; + +pub const TRICKY_MODULE: &str = "/data/adb/modules/tricky_store"; +pub const TRICKY_MODULE_HIDDEN: &str = "/data/adb/modules/.tricky_store"; +pub const TRICKY_DATA: &str = "/data/adb/tricky_store"; +pub const TEESIM_MODULE: &str = "/data/adb/modules/teesim"; +pub const TEESIM_MODULE_UPDATE: &str = "/data/adb/modules_update/teesim"; +pub const TEESIM_DATA: &str = "/data/adb/teesim"; +pub const TARGET_MIRROR: &str = "/data/adb/tricky_store/target.txt"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Engine { + TrickyStore, + TeeSimulatorV4, +} + +#[derive(Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProfileStatus { + pub available: bool, + pub profiles: Vec, + pub selected: Option, + pub automatic: bool, +} + +impl Engine { + pub fn detect() -> Self { + if module_is_present(Path::new(TEESIM_MODULE)) + || module_is_present(Path::new(TEESIM_MODULE_UPDATE)) + { + Self::TeeSimulatorV4 + } else { + Self::TrickyStore + } + } + + pub fn keybox_path(self) -> Result { + match self { + Self::TrickyStore => Ok(PathBuf::from(TRICKY_DATA).join("keybox.xml")), + Self::TeeSimulatorV4 => teesim_keybox_path(), + } + } + + pub fn module_dir(self) -> Option { + let candidates: &[&str] = match self { + Self::TrickyStore => &[TRICKY_MODULE, TRICKY_MODULE_HIDDEN], + Self::TeeSimulatorV4 => &[TEESIM_MODULE, TEESIM_MODULE_UPDATE], + }; + candidates + .iter() + .map(PathBuf::from) + .find(|path| match self { + Self::TeeSimulatorV4 => module_is_present(path), + Self::TrickyStore => path.is_dir() && !path.join("remove").exists(), + }) + } + + pub fn is_enabled(self) -> bool { + self.module_dir() + .map(|path| !path.join("disable").exists()) + .unwrap_or(false) + } +} + +fn module_is_present(path: &Path) -> bool { + if !path.is_dir() || path.join("remove").exists() { + return false; + } + std::fs::read_to_string(path.join("module.prop")) + .map(|content| content.lines().any(|line| line == "id=teesim")) + .unwrap_or(false) +} + +pub fn read_targets() -> Result> { + match Engine::detect() { + Engine::TrickyStore => read_tricky_targets(), + Engine::TeeSimulatorV4 => { + let config = read_teesim_config()?; + let profile = selected_profile(&config)?; + Ok(profile + .get("apps") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect()) + } + } +} + +fn read_tricky_targets() -> Result> { + let path = Path::new(TARGET_MIRROR); + if !path.exists() { + return Ok(Vec::new()); + } + Ok(std::fs::read_to_string(path)? + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .map(str::to_owned) + .collect()) +} + +pub fn write_targets(entries: &[String]) -> Result<()> { + match Engine::detect() { + Engine::TrickyStore => { + let entries: Vec = entries + .iter() + .map(|entry| entry.trim()) + .filter(|entry| !entry.is_empty()) + .map(str::to_owned) + .collect(); + write_target_mirror(&entries) + } + Engine::TeeSimulatorV4 => { + let normalized = normalize_targets(entries); + mutate_teesim_config(|config| update_profile_apps(config, &normalized))?; + write_target_mirror(&normalized) + } + } +} + +pub fn export_target_mirror() -> Result<()> { + if Engine::detect() == Engine::TrickyStore { + return Ok(()); + } + let targets = read_targets()?; + write_target_mirror(&targets) +} + +pub fn import_target_mirror() -> Result<()> { + if Engine::detect() == Engine::TrickyStore { + return Ok(()); + } + let targets = read_tricky_targets()?; + write_targets(&targets) +} + +fn normalize_targets(entries: &[String]) -> Vec { + let mut seen = HashSet::new(); + entries + .iter() + .map(|entry| entry.trim().trim_end_matches(['!', '?'])) + .filter(|entry| !entry.is_empty() && !entry.starts_with('#')) + .filter(|entry| seen.insert((*entry).to_owned())) + .map(str::to_owned) + .collect() +} + +pub(crate) fn write_target_mirror(entries: &[String]) -> Result<()> { + let mut content = entries.join("\n"); + if !content.is_empty() { + content.push('\n'); + } + atomic_write(Path::new(TARGET_MIRROR), content.as_bytes()) +} + +pub fn read_patch_dates() -> Result<(String, String, String)> { + if Engine::detect() == Engine::TrickyStore { + let content = std::fs::read_to_string(Path::new(TRICKY_DATA).join("security_patch.txt"))?; + return Ok(parse_tricky_patch_dates(&content)); + } + + let config = read_teesim_config()?; + let patch = selected_profile(&config)? + .get("patchLevel") + .and_then(Value::as_object); + Ok(( + patch_value(patch, "system"), + patch_value(patch, "boot"), + patch_value(patch, "vendor"), + )) +} + +pub fn write_patch_dates(system: &str, boot: &str, vendor: &str) -> Result<()> { + let system = normalize_patch_value(system)?; + let boot = normalize_patch_value(boot)?; + let vendor = normalize_patch_value(vendor)?; + mutate_teesim_config(|config| { + let profile = selected_profile_mut(config)?; + let patch = profile + .entry("patchLevel") + .or_insert_with(|| Value::Object(Map::new())) + .as_object_mut() + .context("TEESimulator profile patchLevel must be an object")?; + patch.insert("system".into(), Value::String(system)); + patch.insert("boot".into(), Value::String(boot)); + patch.insert("vendor".into(), Value::String(vendor)); + Ok(()) + }) +} + +pub fn import_legacy_patch() -> Result<()> { + let path = Path::new(TRICKY_DATA).join("security_patch.txt"); + if !path.exists() { + return write_patch_dates("no", "no", "no"); + } + let (mut system, mut boot, mut vendor) = + parse_tricky_patch_dates(&std::fs::read_to_string(path)?); + for value in [&mut system, &mut boot, &mut vendor] { + if value.is_empty() { + *value = "no".to_owned(); + } + } + write_patch_dates(&system, &boot, &vendor) +} + +fn normalize_patch_value(value: &str) -> Result { + let value = value.trim(); + let normalized = match value { + "prop" => "system_property".to_owned(), + _ if valid_patch_value(value) => value.to_owned(), + _ if value.len() == 8 && value.chars().all(|c| c.is_ascii_digit()) => { + format!("{}-{}-{}", &value[..4], &value[4..6], &value[6..8]) + } + _ if value.len() == 6 && value.chars().all(|c| c.is_ascii_digit()) => { + format!("{}-{}", &value[..4], &value[4..6]) + } + _ => bail!("invalid TEESimulator patch level value: {value}"), + }; + if !valid_patch_value(&normalized) { + bail!("invalid TEESimulator patch level value: {value}"); + } + Ok(normalized) +} + +fn valid_patch_value(value: &str) -> bool { + if matches!(value, "today" | "no" | "harvested" | "system_property") { + return true; + } + let parts: Vec<&str> = value.split('-').collect(); + if !(2..=3).contains(&parts.len()) { + return false; + } + let year = + parts[0] == "YYYY" || (parts[0].len() == 4 && parts[0].chars().all(|c| c.is_ascii_digit())); + let month = parts[1] == "MM" + || (parts[1].len() == 2 + && parts[1] + .parse::() + .is_ok_and(|month| (1..=12).contains(&month))); + let day = parts.len() == 2 || parts[2] == "DD" || valid_patch_day(parts[2]); + year && month && day +} + +fn valid_patch_day(value: &str) -> bool { + value.len() == 2 + && value.chars().all(|c| c.is_ascii_digit()) + && value.parse::().is_ok_and(|day| (1..=31).contains(&day)) +} + +fn patch_value(patch: Option<&Map>, key: &str) -> String { + patch + .and_then(|values| values.get(key)) + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned() +} + +fn parse_tricky_patch_dates(content: &str) -> (String, String, String) { + let find = |key: &str| { + content + .lines() + .find_map(|line| line.strip_prefix(key)) + .map(str::trim) + .unwrap_or_default() + .to_owned() + }; + let all = find("all="); + if !all.is_empty() { + return (all.clone(), all.clone(), all); + } + (find("system="), find("boot="), find("vendor=")) +} + +fn teesim_config_path() -> PathBuf { + PathBuf::from(TEESIM_DATA).join("config.json") +} + +fn teesim_keybox_path() -> Result { + let config = read_teesim_config()?; + let keybox = selected_profile(&config)? + .get("keybox") + .and_then(Value::as_str) + .context("TEESimulator selected profile has no keybox")?; + Ok(PathBuf::from(TEESIM_DATA).join(keybox)) +} + +fn read_teesim_config() -> Result { + let path = teesim_config_path(); + if !path.exists() { + bail!("TEESimulator config does not exist at {}", path.display()); + } + let content = + std::fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?; + parse_teesim_config(&content, &path) +} + +pub fn ensure_profile_selected() -> Result<()> { + if Engine::detect() == Engine::TeeSimulatorV4 { + let config = read_teesim_config()?; + selected_profile_name(&config)?; + } + Ok(()) +} + +pub fn profile_status(configured: &str) -> Result { + if Engine::detect() != Engine::TeeSimulatorV4 { + return Ok(ProfileStatus { + available: false, + profiles: Vec::new(), + selected: None, + automatic: false, + }); + } + profile_status_for(&read_teesim_config()?, configured) +} + +pub fn validate_profile_choice(configured: &str) -> Result<()> { + if configured.is_empty() || Engine::detect() != Engine::TeeSimulatorV4 { + return Ok(()); + } + let config = read_teesim_config()?; + if !profiles(&config).is_some_and(|profiles| profiles.contains_key(configured)) { + bail!("TEESimulator profile does not exist: {configured}"); + } + Ok(()) +} + +fn profile_status_for(config: &Value, configured: &str) -> Result { + validate_teesim_config(config)?; + let profiles = profiles(config).context("TEESimulator profiles must be an object")?; + let names: Vec = profiles.keys().cloned().collect(); + let automatic = names.len() == 1; + let selected = if automatic { + names.first().cloned() + } else if profiles.contains_key(configured) { + Some(configured.to_owned()) + } else { + None + }; + Ok(ProfileStatus { + available: true, + profiles: names, + selected, + automatic, + }) +} + +fn parse_teesim_config(content: &[u8], path: &Path) -> Result { + let config: Value = serde_json::from_slice(content) + .with_context(|| format!("invalid TEESimulator config at {}", path.display()))?; + validate_teesim_config(&config)?; + Ok(config) +} + +fn validate_teesim_config(config: &Value) -> Result<()> { + config + .as_object() + .context("TEESimulator config root must be an object")?; + if config.get("version").and_then(Value::as_u64) != Some(1) { + bail!("unsupported TEESimulator config schema (expected version 1)"); + } + let profiles = profiles(config).context("TEESimulator profiles must be an object")?; + if profiles.is_empty() { + bail!("TEESimulator profiles must not be empty"); + } + let mut claimed_apps: HashMap = HashMap::new(); + let mut auto_include_profiles = 0; + for (name, profile) in profiles { + if name.is_empty() + || name.len() > 32 + || !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + bail!("TEESimulator profile name is invalid: {name}"); + } + let profile = profile + .as_object() + .with_context(|| format!("TEESimulator profile {name} must be an object"))?; + let keybox = profile + .get("keybox") + .and_then(Value::as_str) + .with_context(|| format!("TEESimulator profile {name} has no keybox"))?; + if !valid_keybox_name(keybox) { + bail!("TEESimulator profile {name} keybox must be an XML filename"); + } + let empty_apps = Vec::new(); + let apps = match profile.get("apps") { + Some(value) => value + .as_array() + .with_context(|| format!("TEESimulator profile {name} apps must be an array"))?, + None => &empty_apps, + }; + let auto_include = profile + .get("autoIncludeNewApps") + .and_then(Value::as_bool) + .unwrap_or(false); + if profile.contains_key("autoIncludeNewApps") && !profile["autoIncludeNewApps"].is_boolean() + { + bail!("TEESimulator profile {name} autoIncludeNewApps must be a boolean"); + } + if let Some(mode) = profile.get("mode") { + if !matches!(mode.as_str(), Some("patch" | "generation")) { + bail!("TEESimulator profile {name} mode must be patch or generation"); + } + } + if let Some(patch) = profile.get("patchLevel") { + let patch = patch.as_object().with_context(|| { + format!("TEESimulator profile {name} patchLevel must be an object") + })?; + for field in ["system", "vendor", "boot"] { + if let Some(value) = patch.get(field) { + let value = value.as_str().with_context(|| { + format!("TEESimulator profile {name} patchLevel.{field} must be a string") + })?; + if !value.is_empty() && !valid_patch_value(value) { + bail!( + "TEESimulator profile {name} has invalid patchLevel.{field}: {value}" + ); + } + } + } + } + if auto_include { + auto_include_profiles += 1; + if auto_include_profiles > 1 { + bail!("only one TEESimulator profile may auto-include new apps"); + } + } else if apps.is_empty() { + bail!("TEESimulator profile {name} has no apps"); + } + for app in apps { + let app = app.as_str().with_context(|| { + format!("TEESimulator profile {name} apps must contain strings") + })?; + if !valid_app_entry(app) { + bail!("TEESimulator profile {name} has invalid app entry: {app}"); + } + if let Some(owner) = claimed_apps.insert(app.to_owned(), name) { + if owner != name { + bail!("TEESimulator app entry appears in profiles {owner} and {name}: {app}"); + } + } + } + } + validate_effective_uid_ownership(profiles)?; + Ok(()) +} + +fn package_uids() -> HashMap { + crate::platform::packages::list_with_uids().unwrap_or_default() +} + +fn effective_uid(entry: &str, packages: &HashMap) -> Option { + entry + .strip_prefix("uid:") + .and_then(|uid| uid.parse().ok()) + .or_else(|| packages.get(entry).copied()) +} + +fn validate_effective_uid_ownership(profiles: &Map) -> Result<()> { + let packages = package_uids(); + let mut owners: HashMap = HashMap::new(); + for (name, profile) in profiles { + for app in profile + .get("apps") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + { + let Some(uid) = effective_uid(app, &packages) else { + continue; + }; + if let Some(owner) = owners.insert(uid, name) { + if owner != name { + bail!("TEESimulator UID {uid} is claimed by profiles {owner} and {name}"); + } + } + } + } + Ok(()) +} + +fn valid_keybox_name(value: &str) -> bool { + value.ends_with(".xml") + && value.len() > 4 + && value + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) +} + +fn valid_app_entry(value: &str) -> bool { + value + .strip_prefix("uid:") + .is_some_and(|uid| uid.parse::().is_ok_and(|uid| uid >= 0)) + || (!value.is_empty() + && value + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_'))) +} + +fn profiles(config: &Value) -> Option<&Map> { + config.get("profiles").and_then(Value::as_object) +} + +fn selected_profile_name(config: &Value) -> Result<&str> { + let profiles = profiles(config).context("TEESimulator profiles must be an object")?; + if profiles.len() == 1 { + return profiles + .keys() + .next() + .map(String::as_str) + .context("TEESimulator profiles must not be empty"); + } + let configured = crate::config::Config::load(None)?.general.teesim_profile; + if configured.is_empty() { + bail!( + "TEESimulator has multiple profiles; select one in the addon WebUI or run: ta-enhanced automation select-profile " + ); + } + profiles + .get_key_value(&configured) + .map(|(name, _)| name.as_str()) + .with_context(|| format!("configured TEESimulator profile does not exist: {configured}")) +} + +fn selected_profile(config: &Value) -> Result<&Map> { + let name = selected_profile_name(config)?; + profiles(config) + .and_then(|profiles| profiles.get(name)) + .and_then(Value::as_object) + .context("selected TEESimulator profile must be an object") +} + +fn selected_profile_mut(config: &mut Value) -> Result<&mut Map> { + let selected = selected_profile_name(config)?.to_owned(); + let profiles = config + .get_mut("profiles") + .and_then(Value::as_object_mut) + .context("TEESimulator profiles must be an object")?; + profiles + .get_mut(&selected) + .and_then(Value::as_object_mut) + .context("TEESimulator profile must be an object") +} + +fn update_profile_apps(config: &mut Value, entries: &[String]) -> Result> { + let selected = selected_profile_name(config)?.to_owned(); + update_profile_apps_for(config, entries, &selected) +} + +fn update_profile_apps_for( + config: &mut Value, + entries: &[String], + selected: &str, +) -> Result> { + let occupied: HashSet = profiles(config) + .into_iter() + .flat_map(|profiles| profiles.iter()) + .filter(|(name, _)| name.as_str() != selected) + .flat_map(|(_, profile)| { + profile + .get("apps") + .and_then(Value::as_array) + .into_iter() + .flatten() + }) + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(); + let packages = package_uids(); + let occupied_uids: HashSet = occupied + .iter() + .filter_map(|entry| effective_uid(entry, &packages)) + .collect(); + let effective: Vec = entries + .iter() + .filter(|entry| { + !occupied.contains(entry.as_str()) + && effective_uid(entry, &packages).is_none_or(|uid| !occupied_uids.contains(&uid)) + }) + .cloned() + .collect(); + if effective.len() != entries.len() { + let rejected: Vec<&str> = entries + .iter() + .filter(|entry| { + occupied.contains(entry.as_str()) + || effective_uid(entry, &packages) + .is_some_and(|uid| occupied_uids.contains(&uid)) + }) + .map(String::as_str) + .collect(); + bail!( + "apps already assigned to another TEESimulator profile: {}", + rejected.join(", ") + ); + } + let apps = effective.iter().cloned().map(Value::String).collect(); + profiles(config) + .and_then(|profiles| profiles.get(selected)) + .context("selected TEESimulator profile does not exist")?; + config["profiles"][selected]["apps"] = Value::Array(apps); + Ok(effective) +} + +fn mutate_teesim_config(mutation: F) -> Result +where + F: FnOnce(&mut Value) -> Result, +{ + let config_path = teesim_config_path(); + let mut config = read_teesim_config()?; + let result = mutation(&mut config)?; + validate_teesim_config(&config)?; + let mut data = serde_json::to_vec_pretty(&config)?; + data.push(b'\n'); + atomic_write(&config_path, &data)?; + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn updates_only_selected_profile_and_preserves_unknown_fields() { + let mut config = json!({ + "version": 1, + "futureField": true, + "profiles": { + "default": { "keybox": "keybox.xml", "apps": ["old.app"], "autoIncludeNewApps": true }, + "banking": { "keybox": "bank.xml", "apps": ["reserved.app"] } + } + }); + let effective = + update_profile_apps_for(&mut config, &["new.app".into()], "default").unwrap(); + + assert_eq!(config["futureField"], true); + assert_eq!(config["profiles"]["default"]["autoIncludeNewApps"], true); + assert_eq!(config["profiles"]["default"]["apps"], json!(["new.app"])); + assert_eq!( + config["profiles"]["banking"]["apps"], + json!(["reserved.app"]) + ); + assert_eq!(effective, vec!["new.app"]); + } + + #[test] + fn rejects_apps_owned_by_another_profile() { + let mut config = json!({ + "version": 1, + "profiles": { + "default": { "keybox": "keybox.xml", "apps": ["old.app"], "autoIncludeNewApps": true }, + "banking": { "keybox": "bank.xml", "apps": ["reserved.app"] } + } + }); + + let error = update_profile_apps_for( + &mut config, + &["new.app".into(), "reserved.app".into()], + "default", + ) + .unwrap_err(); + + assert!(error.to_string().contains("reserved.app")); + assert_eq!(config["profiles"]["default"]["apps"], json!(["old.app"])); + } + + #[test] + fn strips_trickystore_suffixes_for_teesim() { + assert_eq!( + normalize_targets(&["one.app!".into(), "two.app?".into(), "one.app".into()]), + vec!["one.app", "two.app"] + ); + } + + #[test] + fn validates_uid_tokens_at_kotlin_int_boundary() { + assert!(valid_app_entry("uid:2147483647")); + assert!(!valid_app_entry("uid:2147483648")); + assert!(!valid_app_entry("uid:999999999999999999999")); + assert!(!valid_app_entry("uid:-1")); + } + + #[test] + fn normalizes_legacy_patch_values() { + assert_eq!(normalize_patch_value("20250105").unwrap(), "2025-01-05"); + assert_eq!(normalize_patch_value("202501").unwrap(), "2025-01"); + assert_eq!(normalize_patch_value("prop").unwrap(), "system_property"); + assert_eq!(normalize_patch_value("YYYY-MM-05").unwrap(), "YYYY-MM-05"); + assert_eq!(normalize_patch_value("YYYY-MM-DD").unwrap(), "YYYY-MM-DD"); + assert_eq!(normalize_patch_value("YYYY-08").unwrap(), "YYYY-08"); + assert_eq!(normalize_patch_value("2026-MM").unwrap(), "2026-MM"); + assert_eq!(normalize_patch_value("2026-08-DD").unwrap(), "2026-08-DD"); + assert_eq!(normalize_patch_value("YYYY-08-DD").unwrap(), "YYYY-08-DD"); + assert!(normalize_patch_value("2025-13-01").is_err()); + assert!(normalize_patch_value("YYYY-MM-32").is_err()); + } + + #[test] + fn rejects_malformed_or_profileless_configs() { + assert!(validate_teesim_config(&json!({ "version": 1, "profiles": [] })).is_err()); + assert!(validate_teesim_config(&json!({ + "version": 1, + "profiles": { "other": { "keybox": "keybox.xml", "apps": ["com.example"] } } + })) + .is_ok()); + assert!(validate_teesim_config(&json!({ + "version": 1, + "profiles": { "default": { "keybox": "../keybox.xml", "apps": [] } } + })) + .is_err()); + assert!(validate_teesim_config(&json!({ + "version": 1, + "profiles": { "default": { + "keybox": "keybox.xml", "apps": ["com.example"], "mode": "invalid" + } } + })) + .is_err()); + assert!(validate_teesim_config(&json!({ + "version": 1, + "profiles": { "default": { + "keybox": "keybox.xml", "apps": ["com.example"], + "autoIncludeNewApps": "true" + } } + })) + .is_err()); + } + + #[test] + fn updates_explicit_profile_when_default_is_absent() { + let mut config = json!({ + "version": 1, + "profiles": { + "pixel": { "keybox": "pixel.xml", "apps": ["old.app"] }, + "banking": { "keybox": "bank.xml", "apps": ["reserved.app"] } + } + }); + + update_profile_apps_for(&mut config, &["new.app".into()], "pixel").unwrap(); + + assert_eq!(config["profiles"]["pixel"]["apps"], json!(["new.app"])); + assert_eq!( + config["profiles"]["banking"]["apps"], + json!(["reserved.app"]) + ); + } + + #[test] + fn reports_single_profile_as_automatic() { + let status = profile_status_for( + &json!({ + "version": 1, + "profiles": { + "default": { "keybox": "keybox.xml", "apps": ["com.example"] } + } + }), + "stale", + ) + .unwrap(); + + assert_eq!(status.profiles, vec!["default"]); + assert_eq!(status.selected.as_deref(), Some("default")); + assert!(status.automatic); + } + + #[test] + fn reports_multi_profile_selection_and_pending_states() { + let config = json!({ + "version": 1, + "profiles": { + "default": { "keybox": "keybox.xml", "apps": ["com.example"] }, + "banking": { "keybox": "bank.xml", "apps": ["com.bank"] } + } + }); + + let selected = profile_status_for(&config, "banking").unwrap(); + assert_eq!(selected.selected.as_deref(), Some("banking")); + assert!(!selected.automatic); + + let pending = profile_status_for(&config, "missing").unwrap(); + assert_eq!(pending.selected, None); + } + +} diff --git a/rust/src/health/mod.rs b/rust/src/health/mod.rs index 4c24a05..ebe036f 100644 --- a/rust/src/health/mod.rs +++ b/rust/src/health/mod.rs @@ -79,6 +79,16 @@ pub fn handle_health(action: HealthAction, cfg: &Config) -> anyhow::Result<()> { } pub fn detect_engine() -> String { + if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + if let Some(module) = crate::engine::Engine::TeeSimulatorV4.module_dir() { + if let Ok(content) = std::fs::read_to_string(module.join("module.prop")) { + if let Some(name) = content.lines().find_map(|line| line.strip_prefix("name=")) { + return name.trim().to_string(); + } + } + } + return "TEESimulator v4".to_string(); + } if let Ok(entries) = std::fs::read_dir("/data/adb/modules") { for entry in entries.flatten() { if let Ok(content) = std::fs::read_to_string(entry.path().join("module.prop")) { @@ -106,16 +116,14 @@ pub fn detect_engine() -> String { } pub fn is_engine_enabled() -> bool { - for dir in [TS_MODULE, TS_MODULE_HIDDEN] { - let p = Path::new(dir); - if p.is_dir() { - return !p.join("disable").exists(); - } - } - false + crate::engine::Engine::detect().is_enabled() } fn detect_nice_name() -> Option { + if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + // v4's service loop supervises its daemon; avoid competing with it. + return None; + } for dir in [TS_MODULE, TS_MODULE_HIDDEN] { let service_sh = Path::new(dir).join("service.sh"); if let Ok(content) = std::fs::read_to_string(&service_sh) { @@ -158,7 +166,7 @@ pub fn tee_status() -> anyhow::Result { } pub fn check_once(state: &mut HealthState, cfg: &Config) -> anyhow::Result { - if !Path::new(TS_MODULE).is_dir() && !Path::new(TS_MODULE_HIDDEN).is_dir() { + if crate::engine::Engine::detect().module_dir().is_none() { return Ok(true); } diff --git a/rust/src/keybox/generate.rs b/rust/src/keybox/generate.rs index 47c750a..10235f0 100644 --- a/rust/src/keybox/generate.rs +++ b/rust/src/keybox/generate.rs @@ -1,30 +1,33 @@ use anyhow::{Context, Result}; use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, PKCS_ECDSA_P256_SHA256}; -use rsa::RsaPrivateKey; use rsa::pkcs8::EncodePrivateKey; +use rsa::RsaPrivateKey; -use std::path::Path; use tracing::info; use crate::platform::fs::atomic_write; -const TARGET_KEYBOX: &str = "/data/adb/tricky_store/keybox.xml"; -const BACKUP_KEYBOX: &str = "/data/adb/tricky_store/keybox.xml.bak"; - pub fn generate_and_install() -> Result<()> { let xml = generate()?; + let engine = crate::engine::Engine::detect(); + let target = engine.keybox_path()?; + let backup = target.with_extension("xml.bak"); - if Path::new(TARGET_KEYBOX).exists() { - std::fs::copy(TARGET_KEYBOX, BACKUP_KEYBOX) - .context("failed to backup existing keybox")?; + if target.exists() { + std::fs::copy(&target, &backup).context("failed to backup existing keybox")?; } - atomic_write(Path::new(TARGET_KEYBOX), xml.as_bytes())?; + atomic_write(&target, xml.as_bytes())?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(TARGET_KEYBOX, std::fs::Permissions::from_mode(0o644)); + let mode = if engine == crate::engine::Engine::TrickyStore { + 0o644 + } else { + 0o600 + }; + let _ = std::fs::set_permissions(&target, std::fs::Permissions::from_mode(mode)); } info!("device keybox generated and installed"); @@ -35,18 +38,18 @@ fn generate() -> Result { let mut params = CertificateParams::default(); params.alg = &PKCS_ECDSA_P256_SHA256; params.distinguished_name = DistinguishedName::new(); - params.distinguished_name.push(DnType::CommonName, "Android Keybox"); + params + .distinguished_name + .push(DnType::CommonName, "Android Keybox"); - let cert = Certificate::from_params(params) - .context("EC cert generation failed")?; + let cert = Certificate::from_params(params).context("EC cert generation failed")?; let ec_pem = cert.serialize_private_key_pem(); - let cert_pem = cert.serialize_pem() - .context("cert serialization failed")?; + let cert_pem = cert.serialize_pem().context("cert serialization failed")?; let mut rng = rand::rngs::OsRng; - let rsa_key = RsaPrivateKey::new(&mut rng, 2048) - .context("RSA 2048 keygen failed")?; - let rsa_pem = rsa_key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF) + let rsa_key = RsaPrivateKey::new(&mut rng, 2048).context("RSA 2048 keygen failed")?; + let rsa_pem = rsa_key + .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF) .context("RSA PEM encoding failed")?; Ok(build_xml(&ec_pem, &cert_pem, rsa_pem.as_ref())) diff --git a/rust/src/keybox/mod.rs b/rust/src/keybox/mod.rs index 7c2eb5f..7675dd0 100644 --- a/rust/src/keybox/mod.rs +++ b/rust/src/keybox/mod.rs @@ -1,22 +1,19 @@ +pub mod generate; pub mod sources; pub mod validate; -pub mod generate; use std::fmt; use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::{Context, Result, bail}; -use serde::{Serialize, Deserialize}; -use tracing::{info, warn, error}; +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use tracing::{error, info, warn}; use crate::cli::KeyboxAction; use crate::config::Config; use crate::platform::fs::atomic_write; -const TARGET_KEYBOX: &str = "/data/adb/tricky_store/keybox.xml"; -const BACKUP_KEYBOX: &str = "/data/adb/tricky_store/keybox.xml.bak"; - #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum KeyboxSource { @@ -68,15 +65,18 @@ pub fn handle_keybox(action: KeyboxAction, cfg: &Config) -> Result<()> { } KeyboxAction::Validate { path } => { use std::io::Write; - let target = path.as_deref().unwrap_or(TARGET_KEYBOX); - let report = validate::validate_file_full(Path::new(target))?; + let target = match path { + Some(path) => PathBuf::from(path), + None => crate::engine::Engine::detect().keybox_path()?, + }; + let report = validate::validate_file_full(&target)?; let mut stdout = std::io::stdout().lock(); writeln!(stdout, "{}", serde_json::to_string_pretty(&report)?)?; stdout.flush()?; if report.ok { Ok(()) } else { - bail!("keybox validation failed: {target}") + bail!("keybox validation failed: {}", target.display()) } } KeyboxAction::SetCustom { path } => { @@ -105,14 +105,15 @@ pub fn handle_keybox(action: KeyboxAction, cfg: &Config) -> Result<()> { pub fn fetch(config: &Config) -> Result { let preferred = KeyboxSource::from_str(&config.keybox.source).unwrap_or_else(|e| { - warn!("keybox source {:?} invalid ({e}); defaulting to yurikey", config.keybox.source); + warn!( + "keybox source {:?} invalid ({e}); defaulting to yurikey", + config.keybox.source + ); KeyboxSource::default() }); let custom_url = &config.keybox.custom_url; let order = build_source_order(preferred); - let existing_hash = current_keybox_hash(); - for source in &order { let result = match source { KeyboxSource::Yurikey => sources::fetch_yurikey(), @@ -151,13 +152,19 @@ pub fn fetch(config: &Config) -> Result { .map(|k| k.root_type.as_snake_case()) .unwrap_or("unknown"); let new_hash = sources::compute_sha256(&data); - if !new_hash.is_empty() && Some(&new_hash) == existing_hash.as_ref() { - info!("keybox from {} (root={root_type}) identical to installed, skipping", source); - return Ok(FetchResult { source: source_label(source) }); + if !install_data(&data, &new_hash)? { + info!( + "keybox from {} (root={root_type}) identical to installed, skipping", + source + ); + return Ok(FetchResult { + source: source_label(source), + }); } - install_data(&data)?; info!("keybox installed from {} (root={root_type})", source); - return Ok(FetchResult { source: source_label(source) }); + return Ok(FetchResult { + source: source_label(source), + }); } Err(e) => { warn!("keybox source {} failed: {e}", source); @@ -174,19 +181,21 @@ pub fn fetch(config: &Config) -> Result { } pub fn backup() -> Result<()> { - let target = Path::new(TARGET_KEYBOX); + let target = crate::engine::Engine::detect().keybox_path()?; + backup_target(&target) +} + +fn backup_target(target: &Path) -> Result<()> { if !target.exists() { - bail!("no keybox to backup at {}", TARGET_KEYBOX); + bail!("no keybox to backup at {}", target.display()); } - let bak = Path::new(BACKUP_KEYBOX); + let bak = target.with_extension("xml.bak"); if bak.exists() { - let bak1 = PathBuf::from(format!("{}.1", BACKUP_KEYBOX)); - std::fs::rename(bak, &bak1) - .context("failed to rotate backup")?; + let bak1 = PathBuf::from(format!("{}.1", bak.display())); + std::fs::rename(&bak, &bak1).context("failed to rotate backup")?; } - std::fs::copy(target, bak) - .context("failed to create keybox backup")?; - info!("keybox backed up to {}", BACKUP_KEYBOX); + std::fs::copy(&target, &bak).context("failed to create keybox backup")?; + info!("keybox backed up to {}", bak.display()); Ok(()) } @@ -195,37 +204,60 @@ pub fn set_custom(path: &Path) -> Result<()> { bail!("custom keybox not found: {}", path.display()); } let data = std::fs::read(path)?; - let report = validate::validate_full(&data) - .with_context(|| format!("validating {}", path.display()))?; + let report = + validate::validate_full(&data).with_context(|| format!("validating {}", path.display()))?; if !report.ok { let summary = report .keys .iter() .filter(|k| !k.ok) - .map(|k| format!("Keybox#{}/Key#{} ({}): {}", k.keybox_index, k.key_index, k.algorithm, k.errors.join("; "))) + .map(|k| { + format!( + "Keybox#{}/Key#{} ({}): {}", + k.keybox_index, + k.key_index, + k.algorithm, + k.errors.join("; ") + ) + }) .collect::>() .join(" | "); bail!("custom keybox failed validation: {summary}"); } - install_data(&data)?; + install_data(&data, &sources::compute_sha256(&data))?; let root_type = report .keys .first() .map(|k| k.root_type.as_snake_case()) .unwrap_or("unknown"); - info!("custom keybox installed from {} (root={root_type})", path.display()); + info!( + "custom keybox installed from {} (root={root_type})", + path.display() + ); Ok(()) } pub fn get_sources(config: &Config) -> Vec { let active = KeyboxSource::from_str(&config.keybox.source).unwrap_or_else(|e| { - warn!("keybox source {:?} invalid ({e}); defaulting to yurikey", config.keybox.source); + warn!( + "keybox source {:?} invalid ({e}); defaulting to yurikey", + config.keybox.source + ); KeyboxSource::default() }); vec![ - SourceInfo { name: "yurikey".into(), active: active == KeyboxSource::Yurikey }, - SourceInfo { name: "upstream".into(), active: active == KeyboxSource::Upstream }, - SourceInfo { name: "custom".into(), active: active == KeyboxSource::Custom }, + SourceInfo { + name: "yurikey".into(), + active: active == KeyboxSource::Yurikey, + }, + SourceInfo { + name: "upstream".into(), + active: active == KeyboxSource::Upstream, + }, + SourceInfo { + name: "custom".into(), + active: active == KeyboxSource::Custom, + }, ] } @@ -244,35 +276,41 @@ fn build_source_order(preferred: KeyboxSource) -> Vec { order } -fn install_data(data: &[u8]) -> Result<()> { - if Path::new(TARGET_KEYBOX).exists() { - if let Err(e) = backup() { +fn install_data(data: &[u8], new_hash: &str) -> Result { + let target = crate::engine::Engine::detect().keybox_path()?; + if !new_hash.is_empty() && current_keybox_hash(&target).as_deref() == Some(new_hash) { + return Ok(false); + } + if target.exists() { + if let Err(e) = backup_target(&target) { error!("backup failed before install: {e}"); bail!("aborting keybox install: backup failed"); } } - atomic_write(Path::new(TARGET_KEYBOX), data) - .context("failed to write keybox")?; + atomic_write(&target, data).context("failed to write keybox")?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions( - TARGET_KEYBOX, - std::fs::Permissions::from_mode(0o600), - ); + let _ = std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)); } - Ok(()) + Ok(true) } -fn current_keybox_hash() -> Option { - let data = std::fs::read(TARGET_KEYBOX).ok()?; +fn current_keybox_hash(target: &Path) -> Option { + let data = std::fs::read(target).ok()?; let hash = sources::compute_sha256(&data); - if hash.is_empty() { None } else { Some(hash) } + if hash.is_empty() { + None + } else { + Some(hash) + } } fn has_valid_existing_keybox() -> bool { - Path::new(TARGET_KEYBOX).exists() - && validate::validate_file(Path::new(TARGET_KEYBOX)).is_ok() + let Ok(target) = crate::engine::Engine::detect().keybox_path() else { + return false; + }; + target.exists() && validate::validate_file(&target).is_ok() } fn source_label(s: &KeyboxSource) -> &'static str { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fe72f60..b6a6b98 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,5 +1,6 @@ pub mod cli; pub mod core; +pub mod engine; pub mod platform; pub mod logging; pub mod config; diff --git a/rust/src/platform/packages.rs b/rust/src/platform/packages.rs index 29bba6e..aaa2066 100644 --- a/rust/src/platform/packages.rs +++ b/rust/src/platform/packages.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::Path; const PACKAGES_LIST: &str = "/data/system/packages.list"; @@ -20,14 +20,21 @@ pub fn list_third_party() -> anyhow::Result> { } pub fn list_all() -> anyhow::Result> { + Ok(list_with_uids()?.into_keys().collect()) +} + +pub fn list_with_uids() -> anyhow::Result> { let content = std::fs::read_to_string(PACKAGES_LIST)?; Ok(parse_packages_list(&content)) } -fn parse_packages_list(content: &str) -> HashSet { +fn parse_packages_list(content: &str) -> HashMap { content .lines() - .filter_map(|line| line.split_whitespace().next().map(str::to_string)) + .filter_map(|line| { + let mut fields = line.split_whitespace(); + Some((fields.next()?.to_owned(), fields.next()?.parse().ok()?)) + }) .collect() } diff --git a/rust/src/security_patch/bulletin.rs b/rust/src/security_patch/bulletin.rs index eb24933..4910968 100644 --- a/rust/src/security_patch/bulletin.rs +++ b/rust/src/security_patch/bulletin.rs @@ -1,14 +1,17 @@ -use anyhow::{Result, Context}; +use anyhow::{Context, Result}; use tracing::{info, warn}; use crate::platform::network; -const BULLETIN_URL: &str = - "https://source.android.com/docs/security/bulletin/pixel"; +const BULLETIN_URL: &str = "https://source.android.com/docs/security/bulletin/pixel"; const FALLBACK_PATCHES: &[&str] = &[ - "2026-03-01", "2026-02-01", "2026-01-01", - "2025-12-01", "2025-11-01", "2025-10-01", + "2026-03-01", + "2026-02-01", + "2026-01-01", + "2025-12-01", + "2025-11-01", + "2025-10-01", ]; pub fn fetch_latest_patch() -> Result { @@ -25,8 +28,8 @@ pub fn fetch_latest_patch() -> Result { } fn fetch_from_bulletin() -> Result { - let html = network::download_text(BULLETIN_URL) - .context("failed to download security bulletin")?; + let html = + network::download_text(BULLETIN_URL).context("failed to download security bulletin")?; parse_patch_date(&html) } @@ -49,10 +52,7 @@ fn extract_date_from_td(line: &str) -> Option { if !trimmed.starts_with("") { return None; } - let content = trimmed - .strip_prefix("")? - .strip_suffix("")? - .trim(); + let content = trimmed.strip_prefix("")?.strip_suffix("")?.trim(); if is_valid_patch_date(content) { Some(content.to_string()) } else { diff --git a/rust/src/security_patch/mod.rs b/rust/src/security_patch/mod.rs index 3fad10a..d0f8189 100644 --- a/rust/src/security_patch/mod.rs +++ b/rust/src/security_patch/mod.rs @@ -2,7 +2,7 @@ pub mod bulletin; use std::path::Path; -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use serde::Serialize; use tracing::{info, warn}; @@ -18,6 +18,7 @@ const TS_MODULE_PROP: &str = "/data/adb/modules/tricky_store/module.prop"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum TrickyStoreVariant { + TeeSimulatorV4, James, Standard, Legacy, @@ -26,6 +27,7 @@ pub enum TrickyStoreVariant { impl std::fmt::Display for TrickyStoreVariant { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::TeeSimulatorV4 => write!(f, "TEESimulator v4"), Self::James => write!(f, "James"), Self::Standard => write!(f, "Standard"), Self::Legacy => write!(f, "Legacy"), @@ -61,11 +63,33 @@ pub fn handle_security_patch(action: SecurityPatchAction, cfg: &Config) -> Resul println!("{output}"); Ok(()) } - SecurityPatchAction::SetCustom { system, boot, vendor } => { + SecurityPatchAction::SetCustom { + system, + boot, + vendor, + } => { set_custom(&system, &boot, &vendor)?; println!("custom security patch dates applied"); Ok(()) } + SecurityPatchAction::ImportLegacy => { + if crate::engine::Engine::detect() != crate::engine::Engine::TeeSimulatorV4 { + bail!("legacy patch import is only available for TEESimulator v4"); + } + crate::engine::import_legacy_patch()?; + println!("legacy security patch imported"); + Ok(()) + } + SecurityPatchAction::ExportLegacy => { + if crate::engine::Engine::detect() != crate::engine::Engine::TeeSimulatorV4 { + return Ok(()); + } + let (system, boot, vendor) = crate::engine::read_patch_dates()?; + let content = format!("system={system}\nboot={boot}\nvendor={vendor}\n"); + atomic_write(Path::new(SECURITY_PATCH_FILE), content.as_bytes())?; + println!("TEESimulator security patch exported"); + Ok(()) + } } } @@ -88,11 +112,13 @@ pub fn get_boot_patch_date() -> Option { } pub fn get_vendor_patch_date() -> Option { - getprop("ro.vendor.build.security_patch") - .or_else(|| getprop("ro.build.version.security_patch")) + getprop("ro.vendor.build.security_patch").or_else(|| getprop("ro.build.version.security_patch")) } pub fn detect_variant() -> TrickyStoreVariant { + if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + return TrickyStoreVariant::TeeSimulatorV4; + } let prop_content = std::fs::read_to_string(TS_MODULE_PROP).unwrap_or_default(); if prop_content.contains("James") && !prop_content.contains("beakthoven") { @@ -137,14 +163,28 @@ pub fn set(config: &Config) -> Result<()> { fn patch_file_already_matches(variant: &TrickyStoreVariant, dates: &PatchDates) -> bool { match variant { + TrickyStoreVariant::TeeSimulatorV4 => crate::engine::read_patch_dates() + .map(|current| { + current + == ( + dates.system.clone(), + dates.boot.clone(), + dates.vendor.clone(), + ) + }) + .unwrap_or(false), TrickyStoreVariant::Standard => { - let Ok(content) = std::fs::read_to_string(SECURITY_PATCH_FILE) else { return false; }; + let Ok(content) = std::fs::read_to_string(SECURITY_PATCH_FILE) else { + return false; + }; content.contains(&format!("system={}", dates.system)) && content.contains(&format!("boot={}", dates.boot)) && content.contains(&format!("vendor={}", dates.vendor)) } TrickyStoreVariant::James => { - let Ok(content) = std::fs::read_to_string(DEVCONFIG_TOML) else { return false; }; + let Ok(content) = std::fs::read_to_string(DEVCONFIG_TOML) else { + return false; + }; content.contains(&format!("securityPatch = \"{}\"", dates.system)) } TrickyStoreVariant::Legacy => false, @@ -154,9 +194,21 @@ fn patch_file_already_matches(variant: &TrickyStoreVariant, dates: &PatchDates) pub fn set_custom(system: &str, boot: &str, vendor: &str) -> Result<()> { let device = read_device_dates(); let dates = PatchDates { - system: if system == "prop" { device.system } else { system.to_string() }, - boot: if boot == "prop" { device.boot } else { boot.to_string() }, - vendor: if vendor == "prop" { device.vendor } else { vendor.to_string() }, + system: if system == "prop" { + device.system + } else { + system.to_string() + }, + boot: if boot == "prop" { + device.boot + } else { + boot.to_string() + }, + vendor: if vendor == "prop" { + device.vendor + } else { + vendor.to_string() + }, }; let variant = detect_variant(); @@ -169,7 +221,10 @@ pub fn update(config: &Config) -> Result<()> { return Ok(()); } if !config.security_patch.custom_date.is_empty() { - info!("enforcing user custom patch: {}", config.security_patch.custom_date); + info!( + "enforcing user custom patch: {}", + config.security_patch.custom_date + ); return set(config); } update_force() @@ -184,7 +239,10 @@ pub fn update_force() -> Result<()> { }; let variant = detect_variant(); - info!("updating security patch to {} (variant: {variant})", dates.system); + info!( + "updating security patch to {} (variant: {variant})", + dates.system + ); write_patch_dates(&variant, &dates) } @@ -199,6 +257,14 @@ pub fn show_current() -> Result { output.push_str(&format!("vendor: {}\n", dates.vendor)); match variant { + TrickyStoreVariant::TeeSimulatorV4 => { + if let Ok((system, boot, vendor)) = crate::engine::read_patch_dates() { + output.push_str(&format!( + "config_content:\nsystem={system}\nboot={boot}\nvendor={vendor}\n" + )); + } + output.push_str("config_file: /data/adb/teesim/config.json\n"); + } TrickyStoreVariant::James => { let path = Path::new(DEVCONFIG_TOML); if path.exists() { @@ -226,11 +292,18 @@ pub fn show_current() -> Result { } fn write_patch_dates(variant: &TrickyStoreVariant, dates: &PatchDates) -> Result<()> { - crate::platform::fs::ensure_dir(Path::new(TS_DIR))?; - match variant { - TrickyStoreVariant::James => write_james(dates), - TrickyStoreVariant::Standard => write_standard(dates), + TrickyStoreVariant::TeeSimulatorV4 => { + crate::engine::write_patch_dates(&dates.system, &dates.boot, &dates.vendor) + } + TrickyStoreVariant::James => { + crate::platform::fs::ensure_dir(Path::new(TS_DIR))?; + write_james(dates) + } + TrickyStoreVariant::Standard => { + crate::platform::fs::ensure_dir(Path::new(TS_DIR))?; + write_standard(dates) + } TrickyStoreVariant::Legacy => write_legacy(dates), } } @@ -246,7 +319,8 @@ fn write_james(dates: &PatchDates) -> Result<()> { let new_line = format!("securityPatch = \"{}\"", dates.system); let updated = if content.contains("securityPatch") { - content.lines() + content + .lines() .map(|line| { if line.trim_start().starts_with("securityPatch") { new_line.as_str() @@ -257,7 +331,8 @@ fn write_james(dates: &PatchDates) -> Result<()> { .collect::>() .join("\n") } else if content.contains("[deviceProps]") { - content.lines() + content + .lines() .flat_map(|line| { if line.trim() == "[deviceProps]" { vec![line, &new_line as &str] @@ -273,8 +348,7 @@ fn write_james(dates: &PatchDates) -> Result<()> { format!("{content}\n{new_line}") }; - atomic_write(path, updated.as_bytes()) - .context("failed to write devconfig.toml")?; + atomic_write(path, updated.as_bytes()).context("failed to write devconfig.toml")?; info!("wrote security patch to devconfig.toml: {}", dates.system); Ok(()) } @@ -290,10 +364,8 @@ fn write_standard(dates: &PatchDates) -> Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions( - SECURITY_PATCH_FILE, - std::fs::Permissions::from_mode(0o644), - ); + let _ = + std::fs::set_permissions(SECURITY_PATCH_FILE, std::fs::Permissions::from_mode(0o644)); } info!("wrote security patch to security_patch.txt"); @@ -314,7 +386,8 @@ fn write_legacy(dates: &PatchDates) -> Result<()> { } fn extract_version_code(prop_content: &str) -> Option { - prop_content.lines() + prop_content + .lines() .find(|l| l.starts_with("versionCode=")) .and_then(|l| l.strip_prefix("versionCode=")) .and_then(|v| v.trim().parse().ok()) diff --git a/rust/src/status/mod.rs b/rust/src/status/mod.rs index 3e863b8..8c30b99 100644 --- a/rust/src/status/mod.rs +++ b/rust/src/status/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use std::path::Path; use std::process::Command; @@ -11,7 +11,6 @@ use crate::platform::packages; const TS_MODULE_PROP: &str = "/data/adb/modules/tricky_store/module.prop"; const TS_MODULE_PROP_HIDDEN: &str = "/data/adb/modules/.tricky_store/module.prop"; const ORIGINAL_DESC_FILE: &str = "/data/adb/tricky_store/ta-enhanced/description.bak"; -const TARGET_FILE: &str = "/data/adb/tricky_store/target.txt"; const BOOT_HASH_FILE: &str = "/data/adb/boot_hash"; const SECURITY_PATCH_FILE: &str = "/data/adb/tricky_store/security_patch.txt"; @@ -87,27 +86,30 @@ pub fn build_description(cfg: &Config) -> String { } pub fn count_active_apps() -> u32 { - let targets = match std::fs::read_to_string(TARGET_FILE) { - Ok(c) => c, + let targets = match crate::engine::read_targets() { + Ok(targets) => targets, Err(_) => return 0, }; - - let target_pkgs: HashSet<&str> = targets - .lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty() && !l.starts_with('#') && !l.starts_with('!')) - .collect(); - - if target_pkgs.is_empty() { - return 0; - } - - let installed = match packages::list_all() { - Ok(set) => set, + let installed = match packages::list_with_uids() { + Ok(installed) => installed, Err(_) => return 0, }; + count_installed_targets(&targets, &installed) +} - target_pkgs.iter().filter(|pkg| installed.contains(**pkg)).count() as u32 +fn count_installed_targets(targets: &[String], installed: &HashMap) -> u32 { + targets + .iter() + .filter(|target| { + target + .strip_prefix("uid:") + .and_then(|uid| uid.parse::().ok()) + .map_or_else( + || installed.contains_key(target.as_str()), + |uid| installed.values().any(|installed_uid| *installed_uid == uid), + ) + }) + .count() as u32 } pub fn get_keybox_label(cfg: &Config) -> &'static str { @@ -120,6 +122,12 @@ pub fn get_keybox_label(cfg: &Config) -> &'static str { } pub fn get_patch_level() -> String { + if let Ok((system, boot, _)) = crate::engine::read_patch_dates() { + let value = if boot.is_empty() { system } else { boot }; + if !value.is_empty() { + return value; + } + } if let Ok(content) = std::fs::read_to_string(SECURITY_PATCH_FILE) { for line in content.lines() { if let Some(val) = line.strip_prefix("boot=") { @@ -144,7 +152,7 @@ pub fn get_vbhash_active() -> bool { } pub fn save_original_description() -> Result<()> { - let desc_path = Path::new(ORIGINAL_DESC_FILE); + let desc_path = original_description_path(); if desc_path.exists() { return Ok(()); } @@ -153,23 +161,31 @@ pub fn save_original_description() -> Result<()> { if let Some(parent) = desc_path.parent() { std::fs::create_dir_all(parent)?; } - std::fs::write(desc_path, desc.as_bytes()) + std::fs::write(&desc_path, desc.as_bytes()) .context("failed to save original description")?; } Ok(()) } pub fn restore_original_description() -> Result<()> { - let desc_path = Path::new(ORIGINAL_DESC_FILE); + let desc_path = original_description_path(); if !desc_path.exists() { return Ok(()); } - let original = std::fs::read_to_string(desc_path) + let original = std::fs::read_to_string(&desc_path) .context("failed to read original description")?; update_prop_description(&original)?; Ok(()) } +fn original_description_path() -> std::path::PathBuf { + if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + std::path::PathBuf::from("/data/adb/tricky_store/ta-enhanced/addon-description.bak") + } else { + std::path::PathBuf::from(ORIGINAL_DESC_FILE) + } +} + pub fn update_prop_description(desc: &str) -> Result<()> { let prop_path = find_module_prop() .ok_or_else(|| anyhow::anyhow!("module.prop not found"))?; @@ -215,14 +231,17 @@ fn push_live_description(desc: &str) { .unwrap_or("ksud"); // ksud requires --internal ; the KSU_MODULE env var is ignored. - // Target tricky_store: that is the visible module the user sees once - // TA_enhanced's module.prop is removed in service.sh. + let module_id = if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + "TA_enhanced" + } else { + "tricky_store" + }; let _ = Command::new(ksud) .args([ "module", "config", "--internal", - "tricky_store", + module_id, "set", "override.description", desc, @@ -251,7 +270,15 @@ pub fn scan_xposed() -> Result> { } fn find_module_prop() -> Option { - [TS_MODULE_PROP, TS_MODULE_PROP_HIDDEN] + let candidates: &[&str] = if crate::engine::Engine::detect() == crate::engine::Engine::TeeSimulatorV4 { + &[ + "/data/adb/modules/.TA_enhanced/module.prop", + "/data/adb/modules/TA_enhanced/module.prop", + ] + } else { + &[TS_MODULE_PROP, TS_MODULE_PROP_HIDDEN] + }; + candidates .iter() .find(|p| Path::new(p).exists()) .map(|p| p.to_string()) @@ -265,3 +292,23 @@ fn read_module_prop_desc() -> Option { .find(|l| l.starts_with("description=")) .map(|l| l.trim_start_matches("description=").to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_package_and_uid_targets() { + let targets = vec![ + "com.example.one".to_owned(), + "uid:10002".to_owned(), + "uid:10003".to_owned(), + ]; + let installed = HashMap::from([ + ("com.example.one".to_owned(), 10001), + ("com.example.two".to_owned(), 10002), + ]); + + assert_eq!(count_installed_targets(&targets, &installed), 2); + } +} diff --git a/service.sh b/service.sh index a0b2557..9c7fd38 100644 --- a/service.sh +++ b/service.sh @@ -35,6 +35,9 @@ add_denylist_to_target() { done mv "$tmp_file" "$target_file" + if [ "$ENGINE" = "teesim" ] && "$BIN" automation profile-ready >/dev/null 2>&1; then + "$BIN" automation sync-target >/dev/null 2>&1 + fi } # Security patch is handled by the daemon's SecurityPatchTask (with retries + bulletin fetch). @@ -57,7 +60,22 @@ fi # Dot-prefix hides from Magisk's module list scan (stable since Magisk v24+). # service.sh re-copies on every boot so the hidden copy is always fresh. if [ "$MANAGER" = "MAGISK" ]; then - if [ -f "$MODPATH/action.sh" ] && [ "$MODPATH" != "$HIDE_DIR" ]; then + if [ "$ENGINE" = "teesim" ] && [ "$MODPATH" = "$HIDE_DIR" ]; then + VISIBLE_DIR="/data/adb/modules/TA_enhanced" + rm -rf "$VISIBLE_DIR" + mkdir -p "$VISIBLE_DIR" + if cp -af "$HIDE_DIR/." "$VISIBLE_DIR/" \ + && [ -x "$VISIBLE_DIR/bin/${ABI}/ta-enhanced" ] \ + && [ -f "$VISIBLE_DIR/webui/index.html" ]; then + MODPATH="$VISIBLE_DIR" + MODDIR="$VISIBLE_DIR" + BIN="$VISIBLE_DIR/bin/${ABI}/ta-enhanced" + else + _log "ERROR" "Failed to migrate hidden addon for TEESimulator; keeping current path" + rm -rf "$VISIBLE_DIR" + fi + fi + if [ "$ENGINE" = "tricky_store" ] && [ -f "$MODPATH/action.sh" ] && [ "$MODPATH" != "$HIDE_DIR" ]; then _log "INFO" "Module hiding (Magisk)" rm -rf "$HIDE_DIR" mkdir -p "$HIDE_DIR" @@ -71,6 +89,9 @@ if [ "$MANAGER" = "MAGISK" ]; then BIN="$MODPATH/bin/${ABI}/ta-enhanced" fi fi + if [ "$ENGINE" = "teesim" ] && [ -d "$HIDE_DIR" ] && [ "$MODPATH" != "$HIDE_DIR" ]; then + rm -rf "$HIDE_DIR" + fi [ -f "$TS_DIR/target_from_denylist" ] && add_denylist_to_target else _log "INFO" "Denylist merge skipped: $MANAGER has no flat denylist API" @@ -92,7 +113,9 @@ fi # in flight, so defer rm until our installer parent exits (PPID polling # matches upstream Tricky-Addon-Update-Target-List). cp -f "$MODPATH/module.prop" "/data/adb/tricky_store/ta-enhanced/module.prop" 2>/dev/null || true -if [ "$(getprop sys.boot_completed)" = "1" ]; then +if [ "$ENGINE" = "teesim" ]; then + : # Keep this addon's module metadata and WebUI separate from TEESimulator. +elif [ "$(getprop sys.boot_completed)" = "1" ]; then nohup sh -c "while kill -0 $PPID 2>/dev/null; do sleep 1; done; rm -f '$MODPATH/module.prop'" >/dev/null 2>&1 & else rm -f "$MODPATH/module.prop" @@ -104,18 +127,28 @@ mkdir -p /data/adb/tricky_store/ta-enhanced/bin cp -f "$MODPATH/bin/$ABI/resetprop-rs" /data/adb/tricky_store/ta-enhanced/bin/resetprop-rs chmod 755 /data/adb/tricky_store/ta-enhanced/bin/resetprop-rs -# Symlink Management -if [ -f "$MODPATH/action.sh" ] && [ ! -e "$TS/action.sh" ]; then - ln -s "$MODPATH/action.sh" "$TS/action.sh" 2>/dev/null || true -fi -if [ ! -e "$TS/webroot" ]; then - ln -s "$MODPATH/webui" "$TS/webroot" 2>/dev/null || true -fi -if [ ! -e "$TS/banner.png" ] && [ -f "$MODPATH/banner.png" ]; then - ln -s "$MODPATH/banner.png" "$TS/banner.png" 2>/dev/null || true +# TrickyStore can host this addon's UI. TEESimulator v4 already has its own +# webroot/action/banner, so leave all of those files untouched. +if [ "$ENGINE" = "tricky_store" ]; then + if [ -f "$MODPATH/action.sh" ] && [ ! -e "$TS/action.sh" ]; then + ln -s "$MODPATH/action.sh" "$TS/action.sh" 2>/dev/null || true + fi + if [ ! -e "$TS/webroot" ]; then + ln -s "$MODPATH/webui" "$TS/webroot" 2>/dev/null || true + fi + if [ ! -e "$TS/banner.png" ] && [ -f "$MODPATH/banner.png" ]; then + ln -s "$MODPATH/banner.png" "$TS/banner.png" 2>/dev/null || true + fi + if [ -f "$TS/module.prop" ] && ! grep -q "^banner=" "$TS/module.prop"; then + sed -i '$ a\banner=banner.png' "$TS/module.prop" 2>/dev/null || true + fi +elif [ ! -e "$MODPATH/webroot" ]; then + # Publish the addon's WebUI under its own module ID. + ln -s "$MODPATH/webui" "$MODPATH/webroot" 2>/dev/null || true fi -if [ -f "$TS/module.prop" ] && ! grep -q "^banner=" "$TS/module.prop"; then - sed -i '$ a\banner=banner.png' "$TS/module.prop" 2>/dev/null || true + +if [ "$ENGINE" != "teesim" ] || "$BIN" automation profile-ready >/dev/null 2>&1; then + "$BIN" automation export-target >/dev/null 2>&1 || true fi # Heavy support work waits for boot completion in a background subshell — @@ -150,8 +183,11 @@ mkdir -p "$MODPATH/common/tmp" # Xposed Detection (background) "$BIN" status xposed-scan >> "$LOG_BASE_DIR/main.log" 2>&1 & -# Magisk: clean up unhidden module dir -[ -f "$MODPATH/action.sh" ] && rm -rf "/data/adb/modules/TA_enhanced" +# Magisk/TrickyStore legacy hosting cleans the visible addon copy. With +# TEESimulator this addon remains separate and keeps its own module WebUI. +if [ "$ENGINE" = "tricky_store" ] && [ -f "$MODPATH/action.sh" ]; then + rm -rf "/data/adb/modules/TA_enhanced" +fi # Launch Daemon _log "INFO" "Starting ta-enhanced daemon" @@ -160,11 +196,18 @@ _log "INFO" "Daemon launched" # Keybox boot-time retry burst (background): exponential backoff, daemon # takes over on the configured schedule once these attempts exhaust. -if [ ! -f "/data/adb/tricky_store/keybox.xml" ]; then +if [ "$ENGINE" = "teesim" ] && ! "$BIN" automation profile-ready >/dev/null 2>&1; then + : # Profile-pending TEESimulator installs remain read-only. +elif { [ "$ENGINE" = "tricky_store" ] && [ ! -f "/data/adb/tricky_store/keybox.xml" ]; } \ + || { [ "$ENGINE" = "teesim" ] && ! "$BIN" keybox validate >/dev/null 2>&1; }; then ( for _delay in 30 60 120 240; do sleep "$_delay" - [ -f "/data/adb/tricky_store/keybox.xml" ] && exit 0 + if [ "$ENGINE" = "tricky_store" ]; then + [ -f "/data/adb/tricky_store/keybox.xml" ] && exit 0 + else + "$BIN" keybox validate >/dev/null 2>&1 && exit 0 + fi timeout 10 "$BIN" keybox fetch 2>/dev/null && exit 0 done ) & diff --git a/uninstall.sh b/uninstall.sh index 54988fa..82f2f35 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,5 +1,6 @@ MODPATH=${0%/*} TS="/data/adb/modules/tricky_store" +. "$MODPATH/common/detect_engine.sh" SCRIPT_DIR="/data/adb/tricky_store" AUTOMATION_DIR="$SCRIPT_DIR/.automation" TA_DIR="$SCRIPT_DIR/ta-enhanced" @@ -69,17 +70,19 @@ fi # Remove module residue rm -rf "/data/adb/modules/.TA_enhanced" rm -f "/data/adb/boot_hash" -rm -f "$SCRIPT_DIR/security_patch_auto_config" -rm -f "$SCRIPT_DIR/target_from_denylist" -rm -f "$SCRIPT_DIR/system_app" -rm -f "$SCRIPT_DIR/enhanced.conf" -rm -f "$SCRIPT_DIR/.verbose" -rm -f "$SCRIPT_DIR/devconfig.toml" +if [ "$ENGINE" = "tricky_store" ]; then + rm -f "$SCRIPT_DIR/security_patch_auto_config" + rm -f "$SCRIPT_DIR/target_from_denylist" + rm -f "$SCRIPT_DIR/system_app" + rm -f "$SCRIPT_DIR/enhanced.conf" + rm -f "$SCRIPT_DIR/.verbose" + rm -f "$SCRIPT_DIR/devconfig.toml" +fi rm -rf "/data/adb/modules/TA_enhanced" # Restore TrickyStore description DESC_BAK="$TA_DIR/description.bak" -if [ -f "$DESC_BAK" ] && [ -f "$TS/module.prop" ]; then +if [ "$ENGINE" = "tricky_store" ] && [ -f "$DESC_BAK" ] && [ -f "$TS/module.prop" ]; then orig=$(cat "$DESC_BAK" 2>/dev/null) if [ -n "$orig" ]; then sed -i "s|^description=.*|description=${orig}|" "$TS/module.prop" 2>/dev/null @@ -87,7 +90,7 @@ if [ -f "$DESC_BAK" ] && [ -f "$TS/module.prop" ]; then fi fi -if [ -d "$TS" ]; then +if [ "$ENGINE" = "tricky_store" ] && [ -d "$TS" ]; then [ -L "$TS/webroot" ] && rm -f "$TS/webroot" [ -L "$TS/action.sh" ] && rm -f "$TS/action.sh" [ -L "$TS/banner.png" ] && rm -f "$TS/banner.png" diff --git a/webui/assets/index-migrated.min.js b/webui/assets/index-migrated.min.js index a09950d..5b04e18 100644 --- a/webui/assets/index-migrated.min.js +++ b/webui/assets/index-migrated.min.js @@ -515,7 +515,7 @@ Please report this to https://github.com/markedjs/marked.`,o){let r="

An error rm -f /data/adb/tricky_store/security_patch.txt || true rm -f /data/adb/tricky_store/devconfig.toml || true ${BP} config set security_patch.custom_date "" || true - `).then(({errno:t})=>(y("security_patch_value_empty"),t===0));else if(o==="manual"){re.security_patch_auto=!1,Rn("security_patch_auto",!1),syncPatchToggle();const t=Ue?"/data/adb/tricky_store/devconfig.toml":"/data/adb/tricky_store/security_patch.txt",r=e.replace(/[`$"\\]/g,""),cd=r.match(/securityPatch\s*=\s*"(\d{4}-\d{2}-\d{2})"/)?.[1]||r.match(/(?:^|\n)system=(\d{4}-\d{2}-\d{2})/)?.[1]||r.match(/(?:^|\n)all=(\d{8})/)?.[1]?.replace(/^(\d{4})(\d{2})(\d{2})$/,"$1-$2-$3")||"";const fw=()=>x(` + `).then(({errno:t})=>(y(t===0?"security_patch_value_empty":"security_patch_save_failed",t===0),t===0));else if(o==="manual"){re.security_patch_auto=!1,Rn("security_patch_auto",!1),syncPatchToggle();const t=Ue?"/data/adb/tricky_store/devconfig.toml":"/data/adb/tricky_store/security_patch.txt",r=e.replace(/[`$"\\]/g,""),cd=r.match(/securityPatch\s*=\s*"(\d{4}-\d{2}-\d{2})"/)?.[1]||r.match(/(?:^|\n)system=(\d{4}-\d{2}-\d{2})/)?.[1]||r.match(/(?:^|\n)all=(\d{8})/)?.[1]?.replace(/^(\d{4})(\d{2})(\d{2})$/,"$1-$2-$3")||"";const fw=()=>x(` echo "${r}" > ${t} chmod 644 ${t} `).then(({errno:i})=>{const n=i===0;return y(n?"security_patch_save_success":"security_patch_save_failed",n),n});return cd?x(`${BP} config set security_patch.custom_date "${cd}"`).then(fw):fw()}}async function Cr(){let o,e,t,r;try{if(Ue){const{stdout:i}=await x("cat /data/adb/tricky_store/devconfig.toml");if(i.trim()!==""){const n=i.split(` diff --git a/webui/index.html b/webui/index.html index 0ca1118..61f4017 100644 --- a/webui/index.html +++ b/webui/index.html @@ -119,11 +119,9 @@ - -

+
Security Patch Auto-Update
@@ -558,6 +568,120 @@ try { ksu.exec(cmd, '{}', cb); } catch (e) { delete window[cb]; resolve({ errno: 1, stdout: '', stderr: String(e) }); } }); + const addonBinaryCommand = (args) => `p=TA_enhanced; [ -d /data/adb/modules/.TA_enhanced ] && p=.TA_enhanced; case $(uname -m) in aarch64) arch=arm64-v8a;; armv7*|armv8l) arch=armeabi-v7a;; x86_64) arch=x86_64;; i?86) arch=x86;; *) exit 1;; esac; /data/adb/modules/$p/bin/$arch/ta-enhanced ${args}`; + const teeCheck = "{ test -d /data/adb/modules/teesim && test ! -f /data/adb/modules/teesim/remove && grep -q '^id=teesim$' /data/adb/modules/teesim/module.prop; } || { test -d /data/adb/modules_update/teesim && test ! -f /data/adb/modules_update/teesim/remove && grep -q '^id=teesim$' /data/adb/modules_update/teesim/module.prop; }"; + const teeActiveCheck = "{ test -d /data/adb/modules/teesim && test ! -f /data/adb/modules/teesim/remove && test ! -f /data/adb/modules/teesim/disable && grep -q '^id=teesim$' /data/adb/modules/teesim/module.prop; } || { test -d /data/adb/modules_update/teesim && test ! -f /data/adb/modules_update/teesim/remove && test ! -f /data/adb/modules_update/teesim/disable && grep -q '^id=teesim$' /data/adb/modules_update/teesim/module.prop; }"; + if (typeof ksu !== 'undefined' && ksu.exec && !ksu.__taEnhancedWrapped) { + const originalExec = ksu.exec.bind(ksu); + let hookSequence = 0; + const runOriginal = (command, callback) => { + const callbackName = `__taHook_${Date.now()}_${hookSequence++}`; + window[callbackName] = (errno, stdout, stderr) => { + delete window[callbackName]; + callback(Number(errno), stdout || '', stderr || ''); + }; + originalExec(command, '{}', callbackName); + }; + ksu.exec = (command, options, callbackName) => { + const deliver = (errno, stdout, stderr) => { + const callback = window[callbackName]; + if (typeof callback === 'function') callback(errno, stdout, stderr); + }; + const isTargetRead = /^\s*cat \/data\/adb\/tricky_store\/target\.txt/.test(command); + const isPatchRead = /^\s*cat \/data\/adb\/tricky_store\/security_patch\.txt/.test(command); + const isHealthEngineCheck = /modules(?:\/\.?)tricky_store.*disable/.test(command) || (command.includes('/data/adb/modules/tricky_store') && command.includes('disable')); + const isEngineNameCheck = command.includes("grep -h '^name=TEESimulator'"); + const isRestartCountCheck = command.includes('^restarts=') && command.includes('.health_state'); + const followupFor = () => { + if (command.includes("cat << 'KB_EOF' > /data/adb/tricky_store/keybox.xml")) { + return addonBinaryCommand('keybox set-custom /data/adb/tricky_store/keybox.xml'); + } + if (/mv\s+\/data\/adb\/tricky_store\/target\.txt\.tmp\s+\/data\/adb\/tricky_store\/target\.txt(?:\s|$)/.test(command)) { + return addonBinaryCommand('automation sync-target'); + } + if (command.includes('/data/adb/tricky_store/security_patch.txt') && !isPatchRead) { + return addonBinaryCommand('security-patch import-legacy'); + } + return ''; + }; + const execute = () => { + const proxyName = `__taProxy_${Date.now()}_${hookSequence++}`; + window[proxyName] = (errno, stdout, stderr) => { + delete window[proxyName]; + const code = Number(errno); + const followup = code === 0 ? followupFor() : ''; + if (!followup) { + deliver(code, stdout || '', stderr || ''); + return; + } + runOriginal(teeCheck, (teeCode) => { + if (teeCode !== 0) { + deliver(code, stdout || '', stderr || ''); + return; + } + runOriginal(followup, (followupCode, followupStdout, followupStderr) => { + deliver(followupCode, stdout || followupStdout, followupStderr || stderr || ''); + }); + }); + }; + originalExec(command, options, proxyName); + }; + if (isHealthEngineCheck) { + runOriginal(teeCheck, (teeCode) => { + if (teeCode !== 0) { + execute(); + return; + } + runOriginal(teeActiveCheck, (activeCode, stdout, stderr) => { + deliver(activeCode, stdout, stderr); + }); + }); + return; + } + if (isEngineNameCheck) { + runOriginal(teeCheck, (teeCode) => { + if (teeCode !== 0) { + execute(); + return; + } + const teeNameCmd = "grep -h '^name=' /data/adb/modules/teesim/module.prop /data/adb/modules_update/teesim/module.prop 2>/dev/null | head -1 | cut -d= -f2"; + runOriginal(teeNameCmd, (nameCode, stdout, stderr) => { + if (nameCode === 0 && stdout && stdout.trim()) { + deliver(0, stdout, stderr); + } else { + execute(); + } + }); + }); + return; + } + if (isRestartCountCheck) { + const restartCmd = `grep -o '"restarts"[[:space:]]*:[[:space:]]*[0-9]*' /data/adb/tricky_store/.health_state 2>/dev/null | grep -o '[0-9]*$' || ${command}`; + runOriginal(restartCmd, (resCode, stdout, stderr) => { + deliver(resCode, stdout, stderr); + }); + return; + } + if (!isTargetRead && !isPatchRead) { + execute(); + return; + } + runOriginal(teeCheck, (teeCode) => { + if (teeCode !== 0) { + execute(); + return; + } + const exportCommand = isTargetRead + ? addonBinaryCommand('automation export-target') + : addonBinaryCommand('security-patch export-legacy'); + runOriginal(exportCommand, (exportCode, stdout, stderr) => { + if (exportCode === 0) execute(); + else deliver(exportCode, stdout, stderr); + }); + }); + }; + ksu.__taEnhancedWrapped = true; + } window.y = (key, isError = false, duration = 1200) => { const c = document.querySelector('.prompt-container'); const p = document.getElementById('prompt'); @@ -584,6 +708,69 @@ const track = toggle.querySelector('.toggle-track'); return !!(track && track.classList.contains('active')); }; + const refreshProfiles = async () => { + const section = document.getElementById('teesim-profile-section'); + const select = document.getElementById('teesim-profile-select'); + const apply = document.getElementById('teesim-profile-apply'); + const hint = document.getElementById('teesim-profile-hint'); + if (!section || !select || !apply || !hint) return; + const ariaLabel = (document.getElementById('i18n-teesim-profile-select')?.textContent.trim()) + || select.getAttribute('label') + || 'TEESimulator profile'; + select.setAttribute('aria-label', ariaLabel); + const result = await ksuExec(addonBinaryCommand('automation profiles')); + if (result.errno !== 0) { + section.hidden = false; + select.replaceChildren(); + select.disabled = true; + apply.hidden = true; + hint.textContent = `TEESimulator config error: ${(result.stderr || result.stdout).trim() || 'unknown error'}`; + return; + } + let status; + try { status = JSON.parse(result.stdout); } + catch { section.hidden = true; return; } + if (!status.available) { + section.hidden = true; + return; + } + section.hidden = false; + select.replaceChildren(); + if (!status.automatic) { + const pending = document.createElement('option'); + pending.value = ''; + pending.textContent = 'No profile selected'; + select.appendChild(pending); + } + for (const name of status.profiles) { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + select.appendChild(option); + } + select.value = status.selected || ''; + select.disabled = status.automatic; + apply.hidden = status.automatic; + hint.textContent = status.automatic + ? `Using the only profile: ${status.selected}` + : status.selected + ? `Managed profile: ${status.selected}` + : 'Select the profile this addon may manage.'; + }; + const applyProfile = async () => { + const select = document.getElementById('teesim-profile-select'); + const apply = document.getElementById('teesim-profile-apply'); + if (!select || !apply) return; + apply.disabled = true; + const profile = select.value; + const set = await ksuExec(addonBinaryCommand(`automation select-profile "${profile}"`)); + if (set.errno !== 0) { + apply.disabled = false; + if (typeof window.y === 'function') window.y('Failed to set profile', true, 5000); + return; + } + location.reload(); + }; const refreshState = async () => { const toggles = document.querySelectorAll('.custom-toggle[data-flag]'); for (const t of toggles) { @@ -604,67 +791,6 @@ setOn(toggle, !currentlyOn); if (typeof window.y === 'function') window.y('prompt_reboot_required', false, 5000); }; - const TARGET_TXT = '/data/adb/tricky_store/target.txt'; - let saveScheduled = false; - const flushTargetTxt = async () => { - saveScheduled = false; - const cards = Array.from(document.querySelectorAll('.card[data-package]')); - const visible = new Set(); - const checkedVisible = new Map(); - for (const c of cards) { - const pkg = c.dataset.package; - if (!/^[a-zA-Z0-9._]+$/.test(pkg)) continue; - visible.add(pkg); - const cb = c.querySelector('md-checkbox'); - if (cb && cb.checked) { - const mode = c.dataset.mode === 'generate' ? '!' - : c.dataset.mode === 'hack' ? '?' : ''; - checkedVisible.set(pkg, mode); - } - } - const cur = await ksuExec(`cat ${TARGET_TXT} 2>/dev/null`); - const preserved = (cur.stdout || '').split('\n') - .filter(line => { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) return true; - const pkg = trimmed.replace(/[!?]$/, ''); - return !visible.has(pkg); - }); - const visibleLines = []; - for (const [pkg, mode] of checkedVisible) visibleLines.push(`${pkg}${mode}`); - const out = [...preserved, ...visibleLines] - .filter(Boolean) - .filter((v, i, a) => a.indexOf(v) === i) - .join('\n'); - const heredoc = `cat > ${TARGET_TXT} <<'TGT_EOF'\n${out}\nTGT_EOF`; - const r = await ksuExec(heredoc); - if (r.errno === 0) { - if (typeof window.y === 'function') window.y('prompt_saved_target'); - } else if (typeof window.y === 'function') { - window.y('prompt_save_error', true); - } - }; - const scheduleFlush = () => { - if (saveScheduled) return; - saveScheduled = true; - queueMicrotask(flushTargetTxt); - }; - window.scheduleFlush = scheduleFlush; - document.addEventListener('change', (ev) => { - if (ev.target && ev.target.matches && ev.target.matches('md-checkbox.checkbox')) { - scheduleFlush(); - } - }, true); - document.addEventListener('click', (ev) => { - if (ev.target && ev.target.closest && ev.target.closest('.card[data-package]')) { - scheduleFlush(); - } - }, true); - ['select-all', 'deselect-all', 'select-denylist', 'deselect-unnecessary'] - .forEach(id => { - const el = document.getElementById(id); - if (el) el.addEventListener('click', () => setTimeout(scheduleFlush, 0)); - }); const renderKeyboxBadge = () => { const badge = document.getElementById('keybox-badge'); if (!badge) return false; @@ -709,10 +835,17 @@ t.addEventListener('click', onToggleClick); }); refreshState(); + refreshProfiles(); + document.getElementById('teesim-profile-apply')?.addEventListener('click', applyProfile); watchKeyboxBadge(); const dlg = document.getElementById('automation-settings-dialog'); if (dlg) { - new MutationObserver(() => { if (dlg.hasAttribute('open')) refreshState(); }) + new MutationObserver(() => { + if (dlg.hasAttribute('open')) { + refreshState(); + refreshProfiles(); + } + }) .observe(dlg, { attributes: true, attributeFilter: ['open'] }); } }; diff --git a/webui/locales/strings/ar.xml b/webui/locales/strings/ar.xml index eea2596..caa4728 100644 --- a/webui/locales/strings/ar.xml +++ b/webui/locales/strings/ar.xml @@ -147,6 +147,9 @@ تعيين إجراءات يدوية جلب المفتاح + ملف تعريف TEESimulator + ملف تعريف TEESimulator + تعيين ملف التعريف تحديث تلقائي لتصحيح الأمان تعيين التصحيح خصائص المنطقة diff --git a/webui/locales/strings/az.xml b/webui/locales/strings/az.xml index d339e7d..eca391a 100644 --- a/webui/locales/strings/az.xml +++ b/webui/locales/strings/az.xml @@ -142,6 +142,9 @@ Təyin et Əl ilə əməliyyatlar Açar yüklə + TEESimulator profili + TEESimulator profili + Profili təyin et Təhlükəsizlik yaması avtomatik yeniləmə Yamanı təyin et Region Xüsusiyyətləri diff --git a/webui/locales/strings/bn.xml b/webui/locales/strings/bn.xml index e949d57..b48e9e5 100644 --- a/webui/locales/strings/bn.xml +++ b/webui/locales/strings/bn.xml @@ -143,6 +143,9 @@ সেট করুন ম্যানুয়াল কার্যক্রম কী সংগ্রহ করুন + TEESimulator প্রোফাইল + TEESimulator প্রোফাইল + প্রোফাইল সেট করুন নিরাপত্তা প্যাচ স্বয়ংক্রিয় আপডেট প্যাচ সেট করুন রিজিয়ন প্রপস diff --git a/webui/locales/strings/de.xml b/webui/locales/strings/de.xml index 7344bb6..5c0de3b 100644 --- a/webui/locales/strings/de.xml +++ b/webui/locales/strings/de.xml @@ -143,6 +143,9 @@ Festlegen Manuelle Aktionen Schlüssel abrufen + TEESimulator-Profil + TEESimulator-Profil + Profil anwenden Sicherheitspatch automatisch aktualisieren Patch festlegen Region-Props diff --git a/webui/locales/strings/el.xml b/webui/locales/strings/el.xml index 958ce80..90f4100 100644 --- a/webui/locales/strings/el.xml +++ b/webui/locales/strings/el.xml @@ -142,6 +142,9 @@ Ορισμός Χειροκίνητες ενέργειες Λήψη κλειδιού + Προφίλ TEESimulator + Προφίλ TEESimulator + Ορισμός προφίλ Αυτόματη ενημέρωση ενημέρωσης ασφαλείας Ορισμός ενημέρωσης Region Props diff --git a/webui/locales/strings/en.xml b/webui/locales/strings/en.xml index 586b37f..914702b 100644 --- a/webui/locales/strings/en.xml +++ b/webui/locales/strings/en.xml @@ -140,6 +140,9 @@ Set Manual Actions Fetch Key + TEESimulator Profile + TEESimulator Profile + Set Profile Security Patch Auto-Update Set Patch Region Props diff --git a/webui/locales/strings/es-ES.xml b/webui/locales/strings/es-ES.xml index e9122e8..9aabbab 100644 --- a/webui/locales/strings/es-ES.xml +++ b/webui/locales/strings/es-ES.xml @@ -147,6 +147,9 @@ Establecer Acciones manuales Obtener clave + Perfil de TEESimulator + Perfil de TEESimulator + Aplicar perfil Actualización automática del parche de seguridad Establecer parche Props de región diff --git a/webui/locales/strings/fa.xml b/webui/locales/strings/fa.xml index ab03c63..ebaa952 100644 --- a/webui/locales/strings/fa.xml +++ b/webui/locales/strings/fa.xml @@ -143,6 +143,9 @@ تنظیم عملیات دستی دریافت کلید + نمایه TEESimulator + نمایه TEESimulator + تنظیم نمایه به‌روزرسانی خودکار وصله امنیتی تنظیم وصله ویژگی‌های منطقه diff --git a/webui/locales/strings/fr.xml b/webui/locales/strings/fr.xml index 7a8e013..5adb391 100644 --- a/webui/locales/strings/fr.xml +++ b/webui/locales/strings/fr.xml @@ -147,6 +147,9 @@ Définir Actions manuelles Récupérer la clé + Profil TEESimulator + Profil TEESimulator + Définir le profil Mise à jour auto du correctif de sécurité Définir le correctif Props de région diff --git a/webui/locales/strings/id.xml b/webui/locales/strings/id.xml index a8ce14a..c3e6de0 100644 --- a/webui/locales/strings/id.xml +++ b/webui/locales/strings/id.xml @@ -142,6 +142,9 @@ Atur Tindakan Manual Ambil Kunci + Profil TEESimulator + Profil TEESimulator + Terapkan profil Pembaruan Otomatis Patch Keamanan Atur Patch Region Props diff --git a/webui/locales/strings/it.xml b/webui/locales/strings/it.xml index 9bb9231..157a756 100644 --- a/webui/locales/strings/it.xml +++ b/webui/locales/strings/it.xml @@ -143,6 +143,9 @@ Imposta Azioni manuali Recupera chiave + Profilo TEESimulator + Profilo TEESimulator + Imposta profilo Aggiornamento automatico patch di sicurezza Imposta patch Props regionali diff --git a/webui/locales/strings/ja.xml b/webui/locales/strings/ja.xml index 6d2cb3c..a330e2f 100644 --- a/webui/locales/strings/ja.xml +++ b/webui/locales/strings/ja.xml @@ -143,6 +143,9 @@ 設定 手動操作 キー取得 + TEESimulator プロファイル + TEESimulator プロファイル + プロファイルを設定 セキュリティパッチ自動更新 パッチ設定 リージョンプロパティ diff --git a/webui/locales/strings/ko.xml b/webui/locales/strings/ko.xml index 834c7dc..941bb82 100644 --- a/webui/locales/strings/ko.xml +++ b/webui/locales/strings/ko.xml @@ -142,6 +142,9 @@ 설정 수동 작업 키 가져오기 + TEESimulator 프로필 + TEESimulator 프로필 + 프로필 설정 보안 패치 자동 업데이트 패치 설정 리전 속성 diff --git a/webui/locales/strings/pl.xml b/webui/locales/strings/pl.xml index 36fe86a..fad0f51 100644 --- a/webui/locales/strings/pl.xml +++ b/webui/locales/strings/pl.xml @@ -147,6 +147,9 @@ Ustaw Ręczne akcje Pobierz klucz + Profil TEESimulator + Profil TEESimulator + Ustaw profil Automatyczna aktualizacja łatki bezpieczeństwa Ustaw łatkę Props regionu diff --git a/webui/locales/strings/pt-BR.xml b/webui/locales/strings/pt-BR.xml index 5f05fbf..21ef13d 100644 --- a/webui/locales/strings/pt-BR.xml +++ b/webui/locales/strings/pt-BR.xml @@ -146,6 +146,9 @@ Definir Ações manuais Buscar chave + Perfil do TEESimulator + Perfil do TEESimulator + Definir perfil Atualização automática do patch de segurança Definir patch Props de região diff --git a/webui/locales/strings/ru.xml b/webui/locales/strings/ru.xml index 201c30e..513a79d 100644 --- a/webui/locales/strings/ru.xml +++ b/webui/locales/strings/ru.xml @@ -145,6 +145,9 @@ Установить Ручные действия Загрузить ключ + Профиль TEESimulator + Профиль TEESimulator + Применить профиль Автообновление патча безопасности Установить патч Региональные свойства diff --git a/webui/locales/strings/th.xml b/webui/locales/strings/th.xml index c6d26a1..c7dc612 100644 --- a/webui/locales/strings/th.xml +++ b/webui/locales/strings/th.xml @@ -146,6 +146,9 @@ ตั้งค่า การดำเนินการด้วยตนเอง ดึงคีย์ + โปรไฟล์ TEESimulator + โปรไฟล์ TEESimulator + ตั้งค่าโปรไฟล์ อัปเดตแพตช์ความปลอดภัยอัตโนมัติ ตั้งค่าแพตช์ Region Props diff --git a/webui/locales/strings/tl.xml b/webui/locales/strings/tl.xml index a331716..60da678 100644 --- a/webui/locales/strings/tl.xml +++ b/webui/locales/strings/tl.xml @@ -142,6 +142,9 @@ I-set Manu-manong Aksyon Kunin ang Key + Profile ng TEESimulator + Profile ng TEESimulator + Itakda ang profile Auto-Update ng Security Patch I-set ang Patch Region Props diff --git a/webui/locales/strings/tr.xml b/webui/locales/strings/tr.xml index fbca450..3527eeb 100644 --- a/webui/locales/strings/tr.xml +++ b/webui/locales/strings/tr.xml @@ -142,6 +142,9 @@ Ayarla Manuel İşlemler Anahtar Getir + TEESimulator Profili + TEESimulator Profili + Profili Ayarla Güvenlik Yaması Otomatik Güncelleme Yama Ayarla Bölge Özellikleri diff --git a/webui/locales/strings/uk.xml b/webui/locales/strings/uk.xml index c7e4969..aad38c0 100644 --- a/webui/locales/strings/uk.xml +++ b/webui/locales/strings/uk.xml @@ -147,6 +147,9 @@ Встановити Ручні дії Завантажити ключ + Профіль TEESimulator + Профіль TEESimulator + Застосувати профіль Автооновлення патча безпеки Встановити патч Регіональні властивості diff --git a/webui/locales/strings/vi.xml b/webui/locales/strings/vi.xml index d6bb613..7112c0a 100644 --- a/webui/locales/strings/vi.xml +++ b/webui/locales/strings/vi.xml @@ -147,6 +147,9 @@ Đặt Thao tác thủ công Lấy khóa + Cấu hình TEESimulator + Cấu hình TEESimulator + Đặt cấu hình Tự động cập nhật bản vá bảo mật Đặt bản vá Props vùng diff --git a/webui/locales/strings/zh-CN.xml b/webui/locales/strings/zh-CN.xml index 2f89b0b..1753528 100644 --- a/webui/locales/strings/zh-CN.xml +++ b/webui/locales/strings/zh-CN.xml @@ -147,6 +147,9 @@ 设置 手动操作 获取密钥 + TEESimulator 配置文件 + TEESimulator 配置文件 + 应用配置文件 安全补丁自动更新 设置补丁 区域属性 diff --git a/webui/locales/strings/zh-TW.xml b/webui/locales/strings/zh-TW.xml index 110a34c..ff6810b 100644 --- a/webui/locales/strings/zh-TW.xml +++ b/webui/locales/strings/zh-TW.xml @@ -147,6 +147,9 @@ 設定 手動操作 取得金鑰 + TEESimulator 設定檔 + TEESimulator 設定檔 + 套用設定檔 安全修補程式自動更新 設定修補程式 區域屬性 diff --git a/webui/locales/template.xml b/webui/locales/template.xml index 0300750..74e7a13 100644 --- a/webui/locales/template.xml +++ b/webui/locales/template.xml @@ -142,6 +142,9 @@ Set Manual Actions Fetch Key + TEESimulator Profile + TEESimulator Profile + Set Profile Security Patch Auto-Update Set Patch Appearance