weg2022/strguard

GitHub: weg2022/strguard

一个 Kotlin Gradle 插件,通过 ASM 重写字节码并借助 Rust JNI 库对 JVM/Android 应用中的字符串字面量进行加密混淆,以抵御静态提取和逆向分析。

Stars: 2 | Forks: 1

# StrGuard [English](README.md) | [简体中文](README.zh-CN.md) StrGuard 是一个使用 ASM 重写 JVM 类文件的 Kotlin Gradle 插件。符合条件的字符串字面量会转换为特定于构建的功能调用,这些调用指向一个由 ChaCha20-Poly1305 vault 支持的生成的 Rust JNI 库。 StrGuard 提供了经过认证的混淆功能,旨在提高静态提取的成本,而非分发后的密码学保密性。每个构建产物不可避免地包含解码其字符串所需的运行时材料,并且控制或检测该过程的代码可以在其成为 JVM `String` 后观察到该值。请勿在应用代码中发送密码、签名密钥或长期有效的凭据。 ## 适用范围 StrGuard 支持: * Java、Java Library、Application 和 Kotlin/JVM 模块 * 带有 Java 或 Kotlin 类的 Android Application 和 Android Library 变体 * Kotlin Multiplatform JVM 和 Android 目标;JS、Native 和 Wasm 将直接透传 * Windows x64/arm64、Linux glibc x64/arm64 和 macOS x64/arm64 * Android `armeabi-v7a`、`arm64-v8a`、`x86` 和 `x86_64` * 经过 ProGuard/R8 验证的构建产物,以及 Compose Desktop 的 release ProGuard 集成 * `LDC`、static final 字符串、Java 9+ `StringConcatFactory`、Kotlin 字符串模板,以及当这些相同的字面量被用于数组、集合、switch、lambda、反射调用或其他普通 bytecode 时 * 可选的 Kotlin metadata 注解移除 支持的 plugin ID 包括 `java`、`java-library`、`application`、`org.jetbrains.kotlin.jvm`、`org.jetbrains.kotlin.android`、`com.android.application`、`com.android.library` 和 `org.jetbrains.kotlin.multiplatform`。Kotlin Android 必须与 Android Application 或 Library 插件配合使用。 ## 环境要求 * 需要 JDK 17 或 21 来运行 Gradle;受保护的输出要求 Java 11 或原始 bytecode 版本(以较新者为准) * ASM 9.10.1 接受并保留 Java 11 到 Java 27 的类文件(major 55-71),包括 Java 27 的预览类;不支持此范围之外的输入 * 已发布的插件会将 ASM 打包并重定位到一个私有命名空间中,因此由 Gradle、`buildSrc` 或其他插件加载的旧版 ASM 无法降级对类文件的支持 * Gradle 8.14.4 * Rust/Cargo 1.94.1 以及所选的 target 和 linker * Kotlin Gradle Plugin 2.1.21 * Android Gradle Plugin 8.13.2,Android SDK 34 和 NDK 27.2.12479018 `STRGUARD_CARGO_EXECUTABLE` 可用于指定特定的 Cargo 二进制文件。其配套的 `rustc` 以及所选的 linker/archiver 会包含在工具链指纹中。 ## 安装 对于每一个包含已编译类的 JVM 或 Android 模块,从 Gradle Plugin Portal 应用 StrGuard: ``` plugins { id("io.github.weg2022.strguard") version "2.0.1" } ``` 不要仅将其应用于聚合器(aggregator)根项目。 ## 发布 seed 每次启用的构建都需要一个 256 位的 seed,编码为恰好 64 个十六进制字符。从 CI 中注入它,且切勿打印或提交它: ``` STRGUARD_RELEASE_SEED_HEX=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ``` 相同的 seed、模块标识、target、输入和工具链会产生相同的转换类和 Native 输入。在不同的信任域中请使用不同的 seed。 ## 配置 ``` strGuard { enabled.set(true) releaseSeedHex.set(providers.environmentVariable("STRGUARD_RELEASE_SEED_HEX")) // Desktop JVM/KMP only. Defaults to the current host. targetTriple.set("x86_64-pc-windows-msvc") // Android only. Defaults to all four official ABIs. androidAbis.set(setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) stringGuardPackages.set(listOf("com.example.app")) keepStringPackages.set(listOf("com.example.app.generated")) java9StringConcatEnabled.set(true) strictStringCoverage.set(true) consoleOutput.set(false) removeMetadata.set(false) removeMetadataPackages.set(listOf("com.example.app")) keepMetadataPackages.set(listOf("com.example.app.reflective")) } ``` | 属性 | 默认值 | 效果 | | --- | --- | --- | | `enabled` | `true` | 为 false 时,禁用重写、seed/target/API/ABI 验证以及 Native 相关工作。 | | `releaseSeedHex` | `STRGUARD_RELEASE_SEED_HEX` | 启用时所需的 64 位十六进制 seed。 | | `targetTriple` | 环境变量或当前主机 | Desktop JVM/KMP Rust target;每个 JAR 对应一个 runtime。 | | `androidAbis` | 四个官方 ABI | 非空的 Android ABI 集合;AGP 过滤器和最终的 splits 必须是其子集。 | | `java9StringConcatEnabled` | `true` | 保护受支持的 Java 9+ concat 配方常量。 | | `strictStringCoverage` | `false` | 写入完整的聚合覆盖率,并在扫描后,如果某个选定的类存在不受支持的非空字符串位置或未知的自定义 attribute,则构建失败。 | | `consoleOutput` | `false` | 打印基于 schema 的摘要信息,不包含字面量或关键材料。 | | `removeMetadata` | `false` | 移除符合条件的 Kotlin metadata 注解。 | | `stringGuardPackages` | 空 | 包含的前缀;为空表示所有非 StrGuard 的应用程序类。 | | `keepStringPackages` | 空 | 字符串保护排除的前缀。 | | `removeMetadataPackages` | 空 | Metadata 移除的包含前缀。 | | `keepMetadataPackages` | 空 | Metadata 移除的排除前缀。 | 包名条目接受点号或斜杠分隔的名称,并包含其后代。Keep 列表具有优先权。 ## 字符串覆盖率 在选定的应用程序类文件中,StrGuard 会保护普通的字符串常量,无论生成它们的源码语法是什么。这包括方法字面量、static final 字段和 Kotlin `const val`、数组/集合元素、Java `switch` 和 Kotlin `when` 分支、lambda 体、反射参数、Java 9+ concat 配面片段以及 Kotlin 模板。值会保留其精确的 UTF-16 码元,包括 NUL、控制字符、换行符、非 BMP 字符和不完整的代理对(surrogates)。一个受保护的值最多可包含 30,000 个 UTF-16 码元(60,000 个 UTF-16LE 字节)。 StrGuard 无法透明地保护与应用程序相关的所有字符串: * 注解值和默认值是类文件和框架契约的一部分; * 诸如 XML、JSON、properties、manifests、assets 和 Android 资源等资源不在 JVM bytecode 转换的范围内; * 依赖项 JAR、生成的输出或 `stringGuardPackages` 之外的包不会被作为输入,除非拥有它们的模块应用了 StrGuard; * 在运行时生成、从服务接收、通过 JNI 加载或已经内联到另一个未受保护模块中的字符串,无法作为源码字面量被恢复; * 任意的 `ConstantDynamic`、不受支持的 `invokedynamic`、空字符串以及超过大小限制的值将保持不变; * `keepStringPackages` 和 `@KeepString` 是有意设置的排除项。 `strictStringCoverage` 会在应用了这些明确边界之后,审核每一个选定的类。它会在不记录字面量或位置的情况下汇总原因,写入 `summary.txt`,然后当仍然存在非空的不受支持的位置或未知的自定义 attribute 时,在提交当前转换的类或 Native 输入之前使构建失败。它无法为从未进入类转换器的资源、依赖项、排除的包或运行时生成的数据声明覆盖率。 受支持的 Desktop target: ``` x86_64-pc-windows-msvc aarch64-pc-windows-msvc x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin ``` Android 使用与 ABI 无关的已转换类和 vault 数据,然后构建配置好的 Rust target。Native runtime 的最低版本要求为 API 21。当 `minSdk < 21` 或 AGP ABI 过滤器/最终 split 不在 `androidAbis` 中时,构建会失败。AAR marker 会声明确切的 ABI 集合和 minSdk。 ## 排除类 在 Java 和 Kotlin 编译期间可以使用 `KeepString` 和 `KeepMetadata`: ``` import io.github.weg2022.strguard.annotation.KeepString @KeepString class DiagnosticStrings { fun value() = "This literal stays in the class file" } ``` ## 输出与代码压缩工具 ``` build/strguard/classes/main build/strguard/native-input/main build/strguard/native-resources/main build/reports/strguard/main ``` KMP JVM 输出包含 target 名称。Android 输出使用变体名称。JVM JAR 包含一个随机化的 bridge、模块唯一的 loader、一个 target Native library、强制的 shrinker 规则以及一个带版本的 marker。Android APK 使用 `lib/`,而 AAR 使用 `jni/`。 Desktop shrinker 必须按以下顺序运行: ``` compile -> StrGuard protected JAR -> ProGuard/R8 -> StrGuard verifier -> publication/distribution ``` ``` val artifact = strGuardArtifacts.jvm("main") val shrink = tasks.register("proguardMain") { dependsOn(artifact.protectedJar) injars(artifact.protectedJar.get().asFile) configuration(artifact.requiredShrinkerRules.get().asFile) } val verifiedJar = artifact.verifyShrunkJar( shrink.map { layout.buildDirectory.file("shrinker/raw.jar").get() }, "proguard:7.9.1", ) ``` 仅发布/分发已验证的输出。应用程序仍需自行管理有关公共 API、反射、序列化、`ServiceLoader`、主类、Kotlin metadata 和框架的规则。Compose Desktop 的 release ProGuard 会自动连接,无需更改用户的 shrinker 设置。 ## Gradle Build Cache StrGuard 的 transform、Desktop/Android Native 构建、Android 合并和 shrinker 验证任务都是可缓存的。在输入不变的情况下,第二次构建将显示为 `UP-TO-DATE`。在执行 `clean` 之后、在新的 worktree 中或在与另一个兼容的 CI worker 上,如果启用了 Build Cache,Gradle 可以将缺失的输出恢复为 `FROM-CACHE`: ``` # gradle.properties org.gradle.caching=true ``` 或者,传入 `--build-cache`。缓存键包含输入类的内容和相对路径、seed 指纹(而非原始 seed)、模块标识、保护选项、target/ABI/minSdk、捆绑的 runtime 实现、Rust 工具链选择以及 Rust/NDK linker/archiver/library 的指纹。Native 构建使用任务私有的 `CARGO_HOME` 和 `HOME`。更改这些输入中的任何一个都会导致重新执行,而不是命中过期的缓存。如果项目或其祖先目录提供了 `.cargo/config` 或 `.cargo/config.toml`,StrGuard 会保守地禁用 Native 任务的 up-to-date 检查和 Build Cache 复用,因为任意的 Cargo 键可能会选择额外的未跟踪工具;但 transform 缓存仍保持启用状态。 原始的 release seed 既不是任务输入也不是输出,并且不会存储在缓存条目中。然而,transform 条目不可避免地会包含由 seed 派生的 Native 输入、已转换的类和 vault 数据。请将每个本地或远程的 Build Cache 视为受保护构建产物信任边界的一部分。切勿使用公共或不受信任的远程缓存;要求使用经过身份验证的 TLS,隔离信任域,并尽可能只允许受信任的 CI 构建进行推送,而开发者在实际操作中只进行只读加载。有能力替换缓存条目的方同样能够替换可执行的构建输出。 ## 安全性与兼容性 * StrGuard 可以抵御直接的常量池扫描、`strings` 命令以及通用的静态提取。它无法在运行时被访问后对值提供保护。 * 已转换的类、vault 和 Native library 是一个特定于构建的单元,必须一起分发。 * 确定性任务支持带有完整规范化输入的 Gradle up-to-date 检查和 Build Cache 恢复。如上所述,远程缓存访问是一个安全边界。 * ChaCha20-Poly1305 认证失败是致命的。JNI 返回内部驻留的字符串(interned strings)以保持 Java/Kotlin 字面量的同一性。 * Cargo 接收的是一个不含原始 seed 的白名单环境。输出是有边界的,超时/中断会终止子孙进程。 * 派生的字节数组会被清除。而 JVM 的 `String` 值无法从托管内存中被可靠地擦除。 * 输出会作为一个具备回滚能力的集合进行暂存和提交。 * Desktop 加载过程会枚举所有匹配的 classpath 资源,仅在同一个容器内将 Native 二进制文件和 marker 进行配对,验证构建产物/bridge/loader 的 metadata,并校验为执行 `System.load` 而复制的确切字节的 SHA-256。 * 必须存在且仅存在一个有效的 Native 容器。除非 `java.io.tmpdir/strguard` 和每个 `sg-*` 解压文件归当前用户所有,并受到仅限所有者的 POSIX 权限或仅限所有者的 ACL 保护,否则 loader 将关闭并失败。 * Hash 校验和 `System.load` 会在清理前在生成的 loader 内部运行。启动时绝不会扫描或删除另一个进程的解压文件;被锁定的文件会被安排在 JVM 退出时删除。 * 操作系统临时父目录仍然是一个信任边界:可写的 POSIX 父目录必须设置粘滞位(sticky bit),而 ACL 文件系统必须公开一个归当前用户所有的临时目录。同账户和特权本地主体不在威胁模型范围内。 * Marker 摘要可在 marker 受信任时检测到不匹配和二进制篡改;它不是签名或 MAC。如果攻击者能够在 classpath 上同时替换 marker 和二进制文件,他就已经控制了应用程序代码,这仍然超出了本威胁模型的范围。 * Static final 字符串和 Kotlin `const val` 会失去 `ConstantValue` 并在 `` 中初始化。请勿将受保护的值用作跨模块的编译时常量或注解参数。 * 注解字符串、任意的 `ConstantDynamic`、不受支持的 `invokedynamic`、空字符串以及超过 30,000 个 UTF-16 码元的字符串将保持不变。启用严格覆盖率可使非空且不受支持的位置导致构建失败。 * 移除 Metadata 可能会破坏反射、序列化和编译器工具。 请通过 GitHub 私密漏洞报告来报告漏洞。请勿在公开 issue发布 release seed、密钥分片、受保护的 vault、凭据、漏洞利用细节或私密的应用程序代码。 ## 报告与迁移 每次 transform 都会将 `build/reports/strguard//summary.txt` 作为带有 `schemaVersion=1` 的 Java properties 写入。稳定的字段包括 `enabled`、`strictStringCoverage`、`runtimeTarget`、类选择计数、`stringCandidates`、`protectedStrings`、`skippedStrings`、`strictViolations`、`coverageUnknowns`、按原因分类的 `skipped*` 计数、`removedMetadata` 以及未匹配的 keep 选择器。`stringCandidates = protectedStrings + skippedStrings`;`strictViolations` 也包含 `coverageUnknowns`。报告绝不包含字面量、seed、密钥分片、调用点身份或解码后的值。消费者必须忽略未知的可选 schema 1 字段。 `2.0.0` 版本更改了 vault、Native runtime、task graph 和 shrinker 契约。请清理每个受保护的模块。Vault v2 会被拒绝。在更改 seed、target、插件或 Native 工具链时,请删除模块的 `build/` 目录。请勿将 1.x 或预发布版(pre-GA)2.0 的类与另一个生成的 Native library 混合使用。 ## 开发 [`samples`](samples/README.md) 目录涵盖了所有受支持的项目类型。 ``` .\gradlew.bat :strguard-plugin:check :strguard-plugin:validatePlugins cargo fmt --manifest-path native/strguard-runtime/Cargo.toml -- --check cargo clippy --manifest-path native/strguard-runtime/Cargo.toml --all-targets --locked -- -D warnings cargo test --manifest-path native/strguard-runtime/Cargo.toml --locked ``` 设置 `STRGUARD_ANDROID_NATIVE_TEST=true` 和 `ANDROID_NDK_VERSION=27.2.12479018` 以在功能测试中编译所有四个 Android runtime。本地测试不会启动模拟器。 ## CI `ci.yml` 涵盖了 JDK 17/21、Java 11、六个 Desktop 运行器、四 ABI Android/R8、Rust audit/deny/coverage/performance/RSS、可重复性、依赖验证、分发和 samples。 ## 许可证 Apache License 2.0。参见 [LICENSE](LICENSE)。
标签:Gradle插件, JVM, Kotlin, Rust, SOC Prime, 代码混淆, 可视化界面, 后台面板检测, 开发工具, 网络流量审计