diff --git a/.github/release-notes/versions/1.8.0.md b/.github/release-notes/versions/1.8.0.md new file mode 100644 index 0000000..f693d91 --- /dev/null +++ b/.github/release-notes/versions/1.8.0.md @@ -0,0 +1,15 @@ +### 离线项目资料与方法级保护 + +- 加固时按包名与运行证书自动生成、复用项目专属保护资料,每次构建派生不同材料;设置页支持查看身份、导出与恢复备份。默认完全离线,手机安装即用。 +- APK/AAB 支持必保方法清单与逐产物报告,区分选择、转换、产物核验和未覆盖原因;签名后绑定最终文件摘要。未转换的方法不会计入 VMP 覆盖,必保项失败会阻断交付。 +- 扩展 PVM2 参数类型和异常路径验证,修复包装对象跨 JNI 传递的语义;加强并发调用结束后的镜像、解析副本和寄存器清理,以及运行时完整性核验。 +- Native VMP 源码 SDK 扩展整数、float/double、分支/循环、min/max 和漏斗移位,并增加对象、最终 ELF 及 APK/AAB 条目的构建证据核验;修复 CMake 接入与增量构建问题。 +- 从仓库源码重建 PVM2 打包器并核对依赖摘要;修复默认 Application 的自闭合 Manifest 标签属性写入,减少普通 APK 接入失败。 + +### 使用与验证范围 + +- Native VMP 需要业务源码或兼容 LLVM bitcode,通过函数标注接入;当前仍不支持指针、内存访问、通用调用、异常、原子和向量,不能自动虚拟化任意预编译 SO。推荐 LLVM 21、NDK r29 与 Ninja。 +- 111 个 Native 语义测试函数通过宿主检查及单台 Android 15 设备 ARM64/ARMv7 对照。授权业务样本验证了 19 个 DEX 方法及 1 个 SO 内的 1 个 Native 函数;这不是整包或全部业务覆盖。 +- 项目私有种子保存在本机,备份包含私有材料,应妥善保存。安装即用的 APK 仍有公开恢复路径;本版不承诺无法静态还原,也不宣称达到某个加固代数。 +- 新 SO 产物尚未完成正式整包性能与跨设备验收;无资源 APK 重打包及 AAB 业务全流程仍需按项目验证。升级后请重新加固并进行业务回归,已有 APK 不会自动获得新保护。 +- 方法保护需要 JDK 17。详细接入与边界见[使用指南](https://github.com/kairowan/Shellsmith/blob/v1.8.0/docs/usage.md)和[Native SDK](https://github.com/kairowan/Shellsmith/blob/v1.8.0/native-vmp/README.md)。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8390005..6097cda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,12 @@ jobs: - name: 检查 Rust 格式 run: cargo fmt --all --check + - name: 从源码重建并验证 PVM2 方法保护 + run: make test-pvm2 + + - name: 检查 PVM2 运行时并发与明文生命周期 + run: make test-pvm2-runtime + - name: 运行融合核心测试 run: make test-fusion diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcf1ba9..f3c23c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -173,6 +173,10 @@ jobs: shell: bash run: bash scripts/bump-version.sh "$VERSION" + - name: 从源码重建 PVM2 打包器 + shell: bash + run: python3 scripts/build_pvm2_packer.py + # 打包器 JAR 与运行时源码现在都在本仓库内(third_party/xopprotector), # 因此这里恢复为硬性闸门:两侧格式版本不一致就直接失败,不再放行。 - name: 校验 PVM2 打包器与运行时格式版本一致 diff --git a/Cargo.lock b/Cargo.lock index c4788d0..53d706b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2201,11 +2201,9 @@ dependencies = [ [[package]] name = "mocika-shield" -version = "1.7.0" +version = "1.8.0" dependencies = [ - "chacha20poly1305", "dunce", - "rand 0.9.4", "reqwest 0.12.28", "rusqlite", "semver", @@ -2231,7 +2229,7 @@ dependencies = [ [[package]] name = "mocikashield" -version = "1.7.0" +version = "1.8.0" dependencies = [ "cc", "chacha20poly1305", @@ -3637,7 +3635,7 @@ dependencies = [ [[package]] name = "shield-cli" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "clap", @@ -3655,7 +3653,7 @@ dependencies = [ [[package]] name = "shield-core" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "chacha20poly1305", @@ -3665,6 +3663,7 @@ dependencies = [ "hkdf", "log", "rand 0.9.4", + "rusqlite", "serde", "serde_json", "sha1", diff --git a/Makefile b/Makefile index ceba3cb..65c4fd8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ else CLEAN_CMD := rm -rf endif -.PHONY: build-cli build-stub audit-stub-dex build-gui build-all \ +.PHONY: build-pvm2 test-pvm2 test-pvm2-runtime build-cli build-stub audit-stub-dex build-gui build-all \ release release-linux release-windows \ release-macos release-macos-universal \ bump-version test test-fusion test-native-vmp check-aab clean help @@ -21,6 +21,9 @@ endif help: @echo "用法: make <目标>" @echo "" + @echo " build-pvm2 从仓库 Java 源码重建 PVM2 打包器(JDK 17+)" + @echo " test-pvm2 从源码构建并执行通用 DEX 方法验证" + @echo " test-pvm2-runtime 检查 Native 明文生命周期、并发与 JNI 执行" @echo " build-cli 编译 shield-cli(当前平台 release)" @echo " build-stub 构建 shield-stub(Android AAR + 资源包)" @echo " audit-stub-dex 输出 Stub DEX 指标并检查膨胀上限(需先 build-stub)" @@ -43,7 +46,17 @@ build-cli: cargo build --release -p shield-cli @echo "✅ 产物: $(CLI_BIN)" -build-stub: +build-pvm2: + $(PYTHON) scripts/build_pvm2_packer.py + +test-pvm2: + $(PYTHON) tests/scripts/check-pvm2-packer.py + +test-pvm2-runtime: + $(PYTHON) tests/scripts/check-pvm2-runtime.py + cargo test --manifest-path shield-stub/src/main/rust/Cargo.toml xop_pvm2 + +build-stub: build-pvm2 @echo "🤖 构建 shield-stub..." $(BUILD_STUB_CMD) diff --git a/apps/shield-cli/Cargo.toml b/apps/shield-cli/Cargo.toml index 2318122..ada8488 100644 --- a/apps/shield-cli/Cargo.toml +++ b/apps/shield-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shield-cli" -version = "1.7.0" +version = "1.8.0" edition = "2021" authors = ["MocikaDev"] license = "MIT OR Apache-2.0" @@ -24,5 +24,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" -[dev-dependencies] tempfile = "3.20" diff --git a/apps/shield-cli/src/args.rs b/apps/shield-cli/src/args.rs index 9485988..7dffb02 100644 --- a/apps/shield-cli/src/args.rs +++ b/apps/shield-cli/src/args.rs @@ -224,6 +224,12 @@ pub struct ProtectAabArgs { /// AAB 中要交给嵌入式 PVM2 的类描述符前缀,可重复传入。 #[arg(long = "xop-true-vmp-prefix")] pub xop_true_vmp_prefix: Vec, + /// 关键方法清单(UTF-8):每行 模块名完整 DEX 方法描述符。 + #[arg(long, value_name = "FILE")] + pub pvm2_required_methods: Option, + /// Native 证据清单(UTF-8):模块/lib/ABI/库名.soSDK schema 2 报告路径。 + #[arg(long, value_name = "FILE")] + pub native_vmp_evidence: Option, /// `--engine xop` 时使用的 Xop shell-files 目录。 #[arg(long, value_name = "DIR")] pub xop_shell_dir: Option, @@ -367,6 +373,12 @@ pub(crate) struct ProtectArgs { /// 要交给 Xop PVM2 的 DEX 类型描述符前缀,可重复传入(例如 Lcom/example/pay/)。 #[arg(long = "xop-true-vmp-prefix")] pub xop_true_vmp_prefix: Vec, + /// 关键方法清单(UTF-8):每行 模块名完整 DEX 方法描述符。 + #[arg(long, value_name = "FILE")] + pub pvm2_required_methods: Option, + /// Native 证据清单(UTF-8):模块/lib/ABI/库名.soSDK schema 2 报告路径。 + #[arg(long, value_name = "FILE")] + pub native_vmp_evidence: Option, /// 每行输出一个 JSON 事件,兼容原有 --json-progress 参数。 #[arg(long, alias = "json-progress")] pub json: bool, diff --git a/apps/shield-cli/src/commands.rs b/apps/shield-cli/src/commands.rs index e817bdf..c1c0a00 100644 --- a/apps/shield-cli/src/commands.rs +++ b/apps/shield-cli/src/commands.rs @@ -67,6 +67,8 @@ pub(crate) fn run_protect(args: ResolvedProtectArgs) -> Result<()> { }, xop_pvm2_packer_path: args.xop_pvm2_packer, xop_true_vmp_prefixes: args.xop_true_vmp_prefix, + pvm2_required_methods: args.pvm2_required_methods, + native_vmp_evidence: args.native_vmp_evidence, external_pvm2_dex_dir: None, external_pas2_assets_dir: None, deferred_pas2_asset_modules: Vec::new(), @@ -183,6 +185,7 @@ pub fn run_protect_aab_with_control( if !args.input.is_file() { anyhow::bail!("输入 AAB 不存在:{}", args.input.display()); } + shield_core::protection_report::checked_archive(&args.input)?; if args.input == args.output { anyhow::bail!("输入和输出 AAB 不能是同一路径"); } @@ -211,9 +214,11 @@ pub fn run_protect_aab_with_control( if !matches!(args.engine, ProtectionEngineArg::Mocika) { anyhow::bail!("AAB 多模块当前只支持 Shellsmith 单 Stub + 嵌入式 PVM2"); } - if args.xop_pvm2_packer.is_none() || args.xop_true_vmp_prefix.is_empty() { + if args.xop_pvm2_packer.is_none() + || (args.xop_true_vmp_prefix.is_empty() && args.pvm2_required_methods.is_none()) + { anyhow::bail!( - "AAB 多模块必须配置嵌入式 Xop PVM2 Packer 和业务类前缀,确保代码/资源模块共用单 Stub 密钥生命周期" + "AAB 多模块必须配置嵌入式 Xop PVM2 Packer 和业务类前缀或关键方法清单,确保代码/资源模块共用单 Stub 密钥生命周期" ); } } @@ -260,9 +265,13 @@ pub fn run_protect_aab_with_control( ); } if matches!(args.engine, ProtectionEngineArg::Xop) { - if args.xop_pvm2_packer.is_some() || !args.xop_true_vmp_prefix.is_empty() { + if args.xop_pvm2_packer.is_some() + || !args.xop_true_vmp_prefix.is_empty() + || args.pvm2_required_methods.is_some() + || args.native_vmp_evidence.is_some() + { anyhow::bail!( - "--xop-pvm2-packer/--xop-true-vmp-prefix 只用于 --engine mocika;--engine xop 使用独立 Xop 单壳" + "--xop-pvm2-packer/--xop-true-vmp-prefix/--pvm2-required-methods/--native-vmp-evidence 只用于 --engine mocika;--engine xop 使用独立 Xop 单壳" ); } let packer = args @@ -421,6 +430,8 @@ pub fn run_protect_aab_with_control( }, xop_pvm2_packer_path: args.xop_pvm2_packer.clone(), xop_true_vmp_prefixes: args.xop_true_vmp_prefix.clone(), + pvm2_required_methods: args.pvm2_required_methods.clone(), + native_vmp_evidence: args.native_vmp_evidence.clone(), external_pvm2_dex_dir: (!extra_module_dirs.is_empty()) .then_some(extra_modules_root.clone()), external_pas2_assets_dir: has_external_assets.then_some(extra_modules_root.clone()), @@ -516,9 +527,18 @@ pub fn run_protect_aab_with_control( )?; on_progress("SignAab", "使用所选上传证书签署 AAB"); ensure_aab_not_cancelled(&cancel)?; + let staged_aab = tempfile::Builder::new() + .prefix("shield-aab-") + .suffix(".aab") + .tempfile_in( + args.output + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(std::path::Path::new(".")), + )?; sign_aab_with_jarsigner( &unsigned_aab, - &args.output, + staged_aab.path(), &args.ks, &args.key_alias, &pass, @@ -527,7 +547,24 @@ pub fn run_protect_aab_with_control( )?; on_progress("ValidateBundle", "使用 bundletool 校验最终 AAB"); ensure_aab_not_cancelled(&cancel)?; - run_bundletool_validate(&args.bundletool, &args.output)?; + run_bundletool_validate(&args.bundletool, staged_aab.path())?; + if matches!(args.engine, ProtectionEngineArg::Mocika) + && (args.xop_pvm2_packer.is_some() || args.native_vmp_evidence.is_some()) + { + shield_core::protection_report::rebind_report( + &protected_apk, + staged_aab.path(), + &shield_core::protection_report::report_path(&args.output), + true, + Some(&args.input), + )?; + } else { + let obsolete = shield_core::protection_report::report_path(&args.output); + if obsolete.exists() { + std::fs::remove_file(obsolete)?; + } + } + staged_aab.persist(&args.output).map_err(|e| e.error)?; let mut device_install = "not-requested"; let mut smoke_test = "not-requested"; if let Some(apks_output) = args.apks_output.as_ref() { diff --git a/apps/shield-cli/src/config.rs b/apps/shield-cli/src/config.rs index a24be28..719087e 100644 --- a/apps/shield-cli/src/config.rs +++ b/apps/shield-cli/src/config.rs @@ -32,6 +32,8 @@ struct ProtectConfig { environment_policy: Option, profile: Option, ai_resistance: Option, + pvm2_required_methods: Option, + native_vmp_evidence: Option, } #[derive(Debug, Default, Deserialize)] @@ -102,6 +104,12 @@ impl CliConfig { .unwrap_or_default(), xop_pvm2_packer: args.xop_pvm2_packer, xop_true_vmp_prefix: args.xop_true_vmp_prefix, + pvm2_required_methods: args + .pvm2_required_methods + .or_else(|| self.protect.pvm2_required_methods.clone()), + native_vmp_evidence: args + .native_vmp_evidence + .or_else(|| self.protect.native_vmp_evidence.clone()), json: args.json, verbose: args.verbose, }) @@ -142,6 +150,8 @@ impl CliConfig { &mut self.protect.ui_packer, &mut self.protect.resources, &mut self.protect.apksigner, + &mut self.protect.pvm2_required_methods, + &mut self.protect.native_vmp_evidence, &mut self.sign.input, &mut self.sign.output, &mut self.sign.keystore, @@ -168,6 +178,8 @@ pub(crate) struct ResolvedProtectArgs { pub ai_resistance: AiResistanceArg, pub xop_pvm2_packer: Option, pub xop_true_vmp_prefix: Vec, + pub pvm2_required_methods: Option, + pub native_vmp_evidence: Option, pub json: bool, pub verbose: bool, } diff --git a/apps/shield-gui/package-lock.json b/apps/shield-gui/package-lock.json index bfc5e36..5eaae2e 100644 --- a/apps/shield-gui/package-lock.json +++ b/apps/shield-gui/package-lock.json @@ -1,12 +1,12 @@ { "name": "mocika-shield-gui", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mocika-shield-gui", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT OR Apache-2.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.19", diff --git a/apps/shield-gui/package.json b/apps/shield-gui/package.json index f14aed0..126622a 100644 --- a/apps/shield-gui/package.json +++ b/apps/shield-gui/package.json @@ -1,6 +1,6 @@ { "name": "mocika-shield-gui", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT OR Apache-2.0", "private": true, "type": "module", diff --git a/apps/shield-gui/src-tauri/Cargo.toml b/apps/shield-gui/src-tauri/Cargo.toml index cc6733d..9ee0b2f 100644 --- a/apps/shield-gui/src-tauri/Cargo.toml +++ b/apps/shield-gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mocika-shield" -version = "1.7.0" +version = "1.8.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false @@ -31,9 +31,7 @@ url = "2" toml = "0.8" rusqlite = { version = "0.37", features = ["bundled"] } uuid = { version = "1", features = ["v4"] } -chacha20poly1305 = { workspace = true } sha2 = { workspace = true } -rand = { workspace = true } [dev-dependencies] tempfile = "3" diff --git a/apps/shield-gui/src-tauri/src/aab_runner.rs b/apps/shield-gui/src-tauri/src/aab_runner.rs index 5231b1e..6d5475e 100644 --- a/apps/shield-gui/src-tauri/src/aab_runner.rs +++ b/apps/shield-gui/src-tauri/src/aab_runner.rs @@ -37,6 +37,8 @@ pub(crate) struct AabProtectRequest { pub(crate) ai_resistance: AiResistanceRequest, #[serde(default)] pub(crate) xop_true_vmp_prefixes: Vec, + pub(crate) pvm2_required_methods: Option, + pub(crate) native_vmp_evidence: Option, } pub(crate) fn check_aab(app: &tauri::AppHandle, path: String) -> Result { @@ -76,13 +78,14 @@ pub(crate) async fn execute_protect_aab( let apksigner = find_apksigner_path(&app) .ok_or_else(|| ExecutionFailure::from("当前安装包缺少 apksigner".to_string()))?; let ui_packer = find_xop_pvm2_packer_path(&app); - let pvm2 = if request.xop_true_vmp_prefixes.is_empty() { - None - } else { - Some(find_xop_pvm2_packer_path(&app).ok_or_else(|| { - ExecutionFailure::from("当前安装包缺少内置 Xop PVM2 Packer".to_string()) - })?) - }; + let pvm2 = + if request.xop_true_vmp_prefixes.is_empty() && request.pvm2_required_methods.is_none() { + None + } else { + Some(find_xop_pvm2_packer_path(&app).ok_or_else(|| { + ExecutionFailure::from("当前安装包缺少内置 Xop PVM2 Packer".to_string()) + })?) + }; let cancel = Arc::clone(&cancel_handle.0); let task_id = request.task_id.clone(); let task_window = window.clone(); @@ -132,6 +135,14 @@ pub(crate) async fn execute_protect_aab( xop_packer: None, xop_pvm2_packer: pvm2, xop_true_vmp_prefix: request.xop_true_vmp_prefixes, + pvm2_required_methods: request + .pvm2_required_methods + .map(PathBuf::from) + .map(strip_unc_prefix), + native_vmp_evidence: request + .native_vmp_evidence + .map(PathBuf::from) + .map(strip_unc_prefix), xop_shell_dir: None, xop_profile: XopProfileArg::Industry, xop_no_protect_so: false, diff --git a/apps/shield-gui/src-tauri/src/app_config.rs b/apps/shield-gui/src-tauri/src/app_config.rs index 4fb9d6e..de073ca 100644 --- a/apps/shield-gui/src-tauri/src/app_config.rs +++ b/apps/shield-gui/src-tauri/src/app_config.rs @@ -463,3 +463,28 @@ theme_mode = "system" assert_eq!(normalized.fixed_output_directory, "/tmp/output"); } } + +#[tauri::command] +pub(crate) fn list_protection_projects( +) -> Result, String> { + let directory = + shield_core::project_protection::default_directory().map_err(|e| e.to_string())?; + shield_core::project_protection::list(&directory).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub(crate) fn export_protection_project(id: String, path: String) -> Result<(), String> { + let directory = + shield_core::project_protection::default_directory().map_err(|e| e.to_string())?; + shield_core::project_protection::export(&directory, &id, Path::new(&path)) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub(crate) fn import_protection_project( + path: String, +) -> Result { + let directory = + shield_core::project_protection::default_directory().map_err(|e| e.to_string())?; + shield_core::project_protection::import(&directory, Path::new(&path)).map_err(|e| e.to_string()) +} diff --git a/apps/shield-gui/src-tauri/src/cert_store.rs b/apps/shield-gui/src-tauri/src/cert_store.rs index f732b88..3fef371 100644 --- a/apps/shield-gui/src-tauri/src/cert_store.rs +++ b/apps/shield-gui/src-tauri/src/cert_store.rs @@ -1,14 +1,8 @@ use crate::app_config::normalize_keystore_type; use crate::app_paths::strip_unc_prefix; -use chacha20poly1305::{ - aead::{Aead, KeyInit}, - ChaCha20Poly1305, Nonce, -}; -use rand::RngCore; use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::env; +use shield_core::local_secret::{decrypt_secret, derive_crypto_key, encrypt_secret}; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -20,7 +14,6 @@ const DB_FILE: &str = "shield.db"; #[path = "cert_unicode_tests.rs"] mod unicode_tests; const APP_STATE_DEFAULT_CERTIFICATE_ID: &str = "default_certificate_id"; -const SECRET_PREFIX: &str = "enc:v1:"; type VerifyState = (String, Option, Option); #[derive(Debug, Clone, Serialize, Deserialize)] @@ -566,95 +559,12 @@ fn now_timestamp() -> i64 { .as_secs() as i64 } -fn derive_crypto_key(db_path: &Path, keystore_dir: &Path) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(b"mocika-shield-gui-cert-store-v1"); - hasher.update(db_path.to_string_lossy().as_bytes()); - hasher.update(keystore_dir.to_string_lossy().as_bytes()); - if let Some(home) = env::var_os("HOME").or_else(|| env::var_os("USERPROFILE")) { - hasher.update(home.to_string_lossy().as_bytes()); - } - if let Some(user) = env::var_os("USER").or_else(|| env::var_os("USERNAME")) { - hasher.update(user.to_string_lossy().as_bytes()); - } - hasher.finalize().into() -} - -fn encrypt_secret(key: &[u8; 32], value: &str) -> Result { - let cipher = ChaCha20Poly1305::new_from_slice(key) - .map_err(|_| "初始化证书密码加密器失败".to_string())?; - let mut nonce = [0u8; 12]; - rand::rng().fill_bytes(&mut nonce); - let ciphertext = cipher - .encrypt(Nonce::from_slice(&nonce), value.as_bytes()) - .map_err(|_| "加密证书密码失败".to_string())?; - Ok(format!( - "{}{}:{}", - SECRET_PREFIX, - encode_hex(&nonce), - encode_hex(&ciphertext) - )) -} - -fn decrypt_secret(key: &[u8; 32], value: &str) -> Result { - let rest = value - .strip_prefix(SECRET_PREFIX) - .ok_or_else(|| "证书数据库包含未加密密码,请重新导入或创建证书".to_string())?; - let (nonce_hex, cipher_hex) = rest - .split_once(':') - .ok_or_else(|| "证书数据库密码字段格式无效".to_string())?; - let nonce = decode_hex(nonce_hex)?; - if nonce.len() != 12 { - return Err("证书数据库密码 nonce 长度无效".to_string()); - } - let ciphertext = decode_hex(cipher_hex)?; - let cipher = ChaCha20Poly1305::new_from_slice(key) - .map_err(|_| "初始化证书密码解密器失败".to_string())?; - let plaintext = cipher - .decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref()) - .map_err(|_| "解密证书密码失败,请重新导入或创建证书".to_string())?; - String::from_utf8(plaintext).map_err(|_| "证书密码不是合法 UTF-8".to_string()) -} - fn decrypt_record_secrets(key: &[u8; 32], item: &mut CertificateRecord) -> Result<(), String> { item.keystore_password = decrypt_secret(key, &item.keystore_password)?; item.key_password = decrypt_secret(key, &item.key_password)?; Ok(()) } -fn encode_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for &byte in bytes { - out.push(HEX[(byte >> 4) as usize] as char); - out.push(HEX[(byte & 0x0f) as usize] as char); - } - out -} - -fn decode_hex(value: &str) -> Result, String> { - if !value.len().is_multiple_of(2) { - return Err("证书数据库密码字段 hex 长度无效".to_string()); - } - let mut out = Vec::with_capacity(value.len() / 2); - let bytes = value.as_bytes(); - for pair in bytes.chunks_exact(2) { - let hi = decode_hex_nibble(pair[0])?; - let lo = decode_hex_nibble(pair[1])?; - out.push((hi << 4) | lo); - } - Ok(out) -} - -fn decode_hex_nibble(byte: u8) -> Result { - match byte { - b'0'..=b'9' => Ok(byte - b'0'), - b'a'..=b'f' => Ok(byte - b'a' + 10), - b'A'..=b'F' => Ok(byte - b'A' + 10), - _ => Err("证书数据库密码字段包含非法 hex 字符".to_string()), - } -} - #[cfg(unix)] fn tighten_dir_permissions(path: &Path) -> Result<(), String> { use std::os::unix::fs::PermissionsExt; diff --git a/apps/shield-gui/src-tauri/src/main.rs b/apps/shield-gui/src-tauri/src/main.rs index 361706c..2c6633b 100644 --- a/apps/shield-gui/src-tauri/src/main.rs +++ b/apps/shield-gui/src-tauri/src/main.rs @@ -82,6 +82,8 @@ struct ProtectRequest { xop_pvm2_packer_path: Option, #[serde(default)] xop_true_vmp_prefixes: Vec, + pvm2_required_methods: Option, + native_vmp_evidence: Option, certificate_id: Option, } @@ -220,6 +222,8 @@ async fn protect_apk( ai_resistance: request.ai_resistance, xop_pvm2_packer_path: request.xop_pvm2_packer_path, xop_true_vmp_prefixes: request.xop_true_vmp_prefixes, + pvm2_required_methods: request.pvm2_required_methods, + native_vmp_evidence: request.native_vmp_evidence, signing_certificate, signed_output: request.signed_output, }, @@ -574,6 +578,9 @@ fn main() { check_file_exists, delete_file, check_apk, + app_config::list_protection_projects, + app_config::export_protection_project, + app_config::import_protection_project, get_app_config, save_app_config, save_protect_defaults, diff --git a/apps/shield-gui/src-tauri/src/protect_runner.rs b/apps/shield-gui/src-tauri/src/protect_runner.rs index 5e07e87..4fdf8fd 100644 --- a/apps/shield-gui/src-tauri/src/protect_runner.rs +++ b/apps/shield-gui/src-tauri/src/protect_runner.rs @@ -74,13 +74,18 @@ fn unsupported_api19_abis(native_abis: &[String]) -> Vec { fn resolve_xop_pvm2_packer( custom: Option, prefixes: &[String], + required: bool, built_in: Option, ) -> Option { custom .filter(|path| !path.trim().is_empty()) .map(PathBuf::from) .map(strip_unc_prefix) - .or_else(|| (!prefixes.is_empty()).then_some(built_in).flatten()) + .or_else(|| { + (!prefixes.is_empty() || required) + .then_some(built_in) + .flatten() + }) } pub(crate) struct ProtectExecution { @@ -96,6 +101,8 @@ pub(crate) struct ProtectExecution { pub(crate) ai_resistance: AiResistanceRequest, pub(crate) xop_pvm2_packer_path: Option, pub(crate) xop_true_vmp_prefixes: Vec, + pub(crate) pvm2_required_methods: Option, + pub(crate) native_vmp_evidence: Option, pub(crate) signing_certificate: Option, pub(crate) signed_output: Option, } @@ -142,6 +149,7 @@ pub(crate) async fn execute_protect_apk( let resolved_xop_pvm2_packer = resolve_xop_pvm2_packer( request.xop_pvm2_packer_path.clone(), &request.xop_true_vmp_prefixes, + request.pvm2_required_methods.is_some(), find_xop_pvm2_packer_path(&app), ); @@ -194,6 +202,14 @@ pub(crate) async fn execute_protect_apk( }, xop_pvm2_packer_path: resolved_xop_pvm2_packer, xop_true_vmp_prefixes: request.xop_true_vmp_prefixes, + pvm2_required_methods: request + .pvm2_required_methods + .map(PathBuf::from) + .map(strip_unc_prefix), + native_vmp_evidence: request + .native_vmp_evidence + .map(PathBuf::from) + .map(strip_unc_prefix), external_pvm2_dex_dir: None, external_pas2_assets_dir: None, deferred_pas2_asset_modules: Vec::new(), @@ -255,6 +271,7 @@ pub(crate) async fn execute_protect_apk( task_manager.progress(&progress_window, &progress_task_id, step, message) }, )?; + let _ = std::fs::remove_file(shield_core::protection_report::report_path(&opts.output)); let _ = std::fs::remove_file(format!("{final_output}.idsig")); let _ = std::fs::remove_file(unsigned_output); task_manager.progress( @@ -290,11 +307,16 @@ mod pvm2_tests { fn built_in_packer_is_used_only_when_pvm2_prefixes_are_selected() { let built_in = PathBuf::from("built-in.jar"); assert_eq!( - resolve_xop_pvm2_packer(None, &["Lcom/example/".into()], Some(built_in.clone())), + resolve_xop_pvm2_packer( + None, + &["Lcom/example/".into()], + false, + Some(built_in.clone()) + ), Some(built_in) ); assert_eq!( - resolve_xop_pvm2_packer(None, &[], Some(PathBuf::from("built-in.jar"))), + resolve_xop_pvm2_packer(None, &[], false, Some(PathBuf::from("built-in.jar"))), None ); } @@ -305,6 +327,7 @@ mod pvm2_tests { resolve_xop_pvm2_packer( Some("custom.jar".into()), &["Lcom/example/".into()], + false, Some(PathBuf::from("built-in.jar")), ), Some(PathBuf::from("custom.jar")) diff --git a/apps/shield-gui/src-tauri/tauri.conf.json b/apps/shield-gui/src-tauri/tauri.conf.json index 49ed430..aad930d 100644 --- a/apps/shield-gui/src-tauri/tauri.conf.json +++ b/apps/shield-gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Shellsmith", - "version": "1.7.0", + "version": "1.8.0", "identifier": "dev.mocika.shield-gui", "build": { "beforeDevCommand": "npm run dev -- --host 127.0.0.1", diff --git a/apps/shield-gui/src/components/app/protect-configuration-panel.tsx b/apps/shield-gui/src/components/app/protect-configuration-panel.tsx index c5417f0..3b7eec6 100644 --- a/apps/shield-gui/src/components/app/protect-configuration-panel.tsx +++ b/apps/shield-gui/src/components/app/protect-configuration-panel.tsx @@ -1,5 +1,5 @@ import { RuntimeUiSettings } from "@/components/app/runtime-ui-settings"; -import type { RuntimeUiProtection } from "@/lib/tauri"; +import { openFileDialog, type RuntimeUiProtection } from "@/lib/tauri"; import { FolderKey, FolderOpen, Play, Settings2 } from "lucide-react"; import { AppButton, SelectInput, TextInput } from "@/components/app/common"; import { @@ -27,6 +27,8 @@ export function ProtectConfigurationPanel({ aiResistance, usingRecommendedProtection, xopPvm2PackerPath, + pvm2RequiredMethods, onPvm2RequiredMethodsChange, + nativeVmpEvidence, onNativeVmpEvidenceChange, xopTrueVmpPrefixesText, xopTrueVmpPrefixes, xopPvm2BuiltInAvailable, @@ -66,6 +68,10 @@ export function ProtectConfigurationPanel({ aiResistance: AiResistance; usingRecommendedProtection: boolean; xopPvm2PackerPath: string; + nativeVmpEvidence: string; + onNativeVmpEvidenceChange: (value: string) => void; + pvm2RequiredMethods: string; + onPvm2RequiredMethodsChange: (value: string) => void; xopTrueVmpPrefixesText: string; xopTrueVmpPrefixes: string[]; xopPvm2BuiltInAvailable: boolean; @@ -95,7 +101,7 @@ export function ProtectConfigurationPanel({ const invalidPvmPrefix = xopTrueVmpPrefixes.some((prefix) => prefix.length < 6 || !prefix.startsWith("L") || (!prefix.endsWith("/") && !prefix.endsWith(";")) || prefix.includes("..")); const customPvmPacker = Boolean(xopPvm2PackerPath.trim()); const pvmPackerAvailable = customPvmPacker || xopPvm2BuiltInAvailable; - const pvmReady = pvmPackerAvailable && xopTrueVmpPrefixes.length > 0 && !invalidPvmPrefix && xopPvm2JavaReady && runtimeMode === "standard"; + const pvmReady = pvmPackerAvailable && (xopTrueVmpPrefixes.length > 0 || Boolean(pvm2RequiredMethods.trim())) && !invalidPvmPrefix && xopPvm2JavaReady && runtimeMode === "standard"; return (