StringFogPro 是一款自动加密 APK / AAR 中 Java·Kotlin 字符串字面量的 Gradle 字符串加密插件:构建期把明文替换为「密文 + 运行时解密调用」,反编译只看到乱码,让硬编码的接口地址、密钥、Token 不再裸奔。它在经典 StringFog(Apache-2.0)基础上做现代化超集升级——拥抱 AGP 8.7 / 9.2 现代插桩 API(AsmClassVisitorFactory + onVariants),内置 AES(原版 v3.0.0 已删除),提供 XOR / AES / 自定义算法三选一,新增 每串随机密钥、bytes 免 Base64 模式、明文→密文映射文件,支持按包 include/exclude + 最小串长阈值配置。运行时是零依赖纯 Java JAR,一次 ./gradlew assembleRelease 全自动完成、零侵入业务代码。34/34 测试通过 + 真实 APK/AAR 反编译证据 + AGP 8.7/9.2 双档实测。 适用于 Android APK/AAR/库的字符串混淆、密钥保护、反逆向加固。
- 核心特性
- 快速接入
- 配置项参考
- 相对原版 StringFog 的升级
- 真实反编译证据
- 算法自主扩展
- 适配测试矩阵
- 版本支持表
- 工程结构
- 设计说明与已知边界
- Roadmap(诚实标注)
- 更新日志
- 致谢与 License
| 特性 | 说明 |
|---|---|
| Java / Kotlin 双支持 | 工作在 ASM 字节码层,与源语言无关,两者被同样加密(附真实反编译证据) |
| 拼接字面量加密(v2.2.0) | 支持 invokedynamic makeConcatWithConstants(Java 9+/Kotlin 的 "文本"+变量/字符串模板)——去糖为等价 StringBuilder 链,加密 recipe/bootstrap 常量里的明文(旧版仅 LDC 够不着) |
| 字段 ConstantValue 加密(v2.3.0) | 根治 const val(Kotlin)/ static final String(Java)盲点——剥离字段明文 ConstantValue 属性,在 <clinit> 注入「密文+decrypt+PUTSTATIC」运行期还原,使明文不再以字段常量形态残留于 .class/AAR/dex 常量池 |
| APK + AAR/JAR 加密 | Application 变体与 Library 变体走同一套 ASM 变换核心 |
| 三种算法 | 内置 XOR+Base64(默认) 与 AES-128/CBC,可注册任意自定义 IStringFog |
| 每串随机密钥 | 每个字符串独立随机密钥,相同明文各处密文不同,抗统计/模式分析 |
| bytes 免 Base64 | 密文以原始 byte[] 字面量发射(d8 优化为 fill-array-data),抹除 Base64 静态特征 |
| 映射文件 | 可选输出「类#方法 · 算法 · 明文→密文」映射,便于审计与排查 |
| 细粒度配置 | 总开关 + 按包白/黑名单 + minLength 阈值 + 配置校验/冲突告警 |
| 完全自动化 | onVariants 自动注册,仅 apply 插件;仅 release 插桩,debug 保持明文 |
| 零依赖运行时 | 纯 Java JAR,仅依赖 JDK,POM 零依赖,不引入 kotlin-stdlib |
| 向后兼容 | 零配置默认路径与 v1.1.0 逐字节等价,历史工程零成本迁移 |
StringFogPro 是标准 java-gradle-plugin,发布时自动生成插件 marker(com.va.stringfog:com.va.stringfog.gradle.plugin),
消费端即可 plugins { id("com.va.stringfog") version "2.2.0" } 一行接入(免 buildscript classpath),并获得 stringfog { } 类型安全 DSL。
pluginManagement {
repositories {
mavenLocal() // 本地 publishToMavenLocal 联调时命中 2.2.0(含 marker)
maven { url = uri("https://jitpack.io") } // JitPack 源码分发(见下方「JitPack 说明」)
google(); mavenCentral(); gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven { url = uri("https://jitpack.io") }
google(); mavenCentral()
}
}在要加密的模块 build.gradle.kts:
plugins {
id("com.android.application") // 或 com.android.library
id("com.va.stringfog") version "2.2.0" // ← 一行接入;自动提供 stringfog {} DSL
}
dependencies {
// 运行时解密器(零依赖纯 Java JAR)
implementation("com.github.Pangu-Immortal.StringFogPro:stringfog:2.2.0")
}
// 可选配置;不写则零配置走默认 XOR(向后兼容 v1.1.0)
stringfog {
enabled.set(true)
algorithm.set("xor") // "xor" | "aes" | 自定义 IStringFog 全限定类名
// key.set("your-release-key") // 不设则 xor 用默认密钥、aes 用内置默认密钥
minLength.set(1)
// bytesMode.set(true) // 密文以 byte[] 发射,免 Base64
// randomKeyPerString.set(true) // 每串独立随机密钥
// mappingEnabled.set(true) // 输出 build/outputs/stringfog/ 映射文件
// fogPackages.add("com.your.app") // 只加密这些包(留空=全部)
// excludePackages.add("com.your.app.model") // 排除这些包(优先级更高)
}搞定。 执行 ./gradlew assembleRelease,release 产物里的字符串即被自动加密;debug 不受影响。
全局临时关闭:
./gradlew assembleRelease -Pva.stringfog.enabled=false
version的取值:plugins { id }里的版本须与已发布的 marker 版本一致—— 本地publishToMavenLocal(默认2.2.0)→ 用version "2.2.0";Gradle Plugin Portal(未来)→ 亦为2.2.0。
JitPack 服务 module 坐标(com.github.Pangu-Immortal.StringFogPro:*),但不服务 Gradle 插件 marker
(marker 组名是 com.va.stringfog)。因此若从 JitPack 取插件,plugins { id } 需二选一:
- 仍走一行式:在
pluginManagement加一次resolutionStrategy把 id 映射到 module 坐标(用 JitPack 的v前缀 tag):pluginManagement { resolutionStrategy { eachPlugin { if (requested.id.id == "com.va.stringfog") { useModule("com.github.Pangu-Immortal.StringFogPro:stringfog-gradle-plugin:${requested.version}") } } } } // 之后:plugins { id("com.va.stringfog") version "v2.2.0" } - fallback · buildscript classpath(老方式,不再主推):
buildscript { repositories { maven { url = uri("https://jitpack.io") }; google(); mavenCentral() } dependencies { classpath("com.github.Pangu-Immortal.StringFogPro:stringfog-gradle-plugin:v2.2.0") } } // 子模块:apply(plugin = "com.va.stringfog") + configure<...StringFogExtension> { ... }
一句话:mavenLocal / Gradle Plugin Portal 下
plugins { id }开箱一行接入;JitPack 下加一次resolutionStrategy映射即可保持一行式,classpath 仅作最后 fallback。
stringfog { }(com.vapro.vae.stringfog.plugin.StringFogExtension):
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
Boolean |
true |
总开关;亦可 -Pva.stringfog.enabled=false 全局关 |
algorithm |
String |
"xor" |
xor / aes / 自定义 IStringFog 全限定类名 |
key |
String |
内置默认 | 密钥;未设时 xor 走 DEFAULT_KEY(兼容 v1.1.0),aes 走内置默认密钥 |
fogPackages |
List<String> |
[] |
仅加密这些包前缀下的类(为空=全部) |
excludePackages |
List<String> |
[] |
排除这些包前缀下的类(优先级高于 fogPackages) |
minLength |
Int |
1 |
最小加密串长阈值,短于此长度保持明文 |
bytesMode |
Boolean |
false |
密文以原始 byte[] 字面量发射(免 Base64) |
randomKeyPerString |
Boolean |
false |
每个字符串独立随机密钥(抗统计分析) |
randomKeyLength |
Int |
16 |
每串随机密钥长度(字符数,仅 randomKeyPerString 生效) |
mappingEnabled |
Boolean |
false |
输出映射到 build/outputs/stringfog/stringfog-mapping-<variant>.txt |
配置健壮性:minLength 为负 / algorithm 空白 / randomKeyLength 非正 → 抛错早失败;fogPackages ∩ excludePackages 重叠、aes/自定义算法未设 key → 告警不阻塞。
| 维度 | 原版 StringFog(5.x) | StringFogPro v2.2.0 | 实现 |
|---|---|---|---|
| AGP 基线 | AGP 8.x / Gradle 8 | AGP 8.7 与 9.2 双档实测 | ✅ |
| AES 算法 | v3.0.0 起已删除 | 内置回归(AES-128/CBC/PKCS5,随机 IV) | ✅ |
| 排除包 exclude | v1.4.0 起已移除 | 重新提供 excludePackages |
✅ |
| 最小串长阈值 | 无 | 新增 minLength |
✅ |
| 每串随机密钥 | ✅ | RandomKeyGenerator 已实现 |
✅ |
| bytes 免 Base64 | ✅ | byte[] 字面量 / fill-array-data 已实现 |
✅ |
| mapping 映射文件 | ✅ | 构建期明文→密文映射已实现 | ✅ |
| 配置形态 | Groovy DSL 为主 | 类型化 Kotlin DSL + 配置校验 | ✅ |
| 运行时依赖 | 需引算法库 | 零依赖纯 Java JAR | ✅ |
诚实声明:插件 marker 已由
java-gradle-plugin自动生成并随publishToMavenLocal发布,故 mavenLocal / Gradle Plugin Portal 下plugins { id("com.va.stringfog") version "2.2.0" }已可一行接入。唯一未完成项是「发布到官方 Gradle Plugin Portal」(需外部发布账号,本项目暂无);在此之前,JitPack 分发下用一次resolutionStrategy映射即可保持一行式,或退回 buildscript classpath(见快速接入)。bytes 模式「省体积」诚实说明:本仓 6 条短串样例整包 DEX 差异仅 +8 字节(可忽略),故 bytes 模式首要价值是抹除 Base64 文本特征 + 免运行时 Base64 解码,而非对短串省体积。
以下均来自本仓 sample/ 模块 v2.1.0 真实构建产物的 dexdump / javap 反编译(build-tools 36.0.0),非虚构。debug 不插桩(release-only),故 debug DEX 即「加密前」,release DEX 即「加密后」。
① Java 加密后(release DEX,默认 XOR,单参路径)
# JavaSecrets.apiToken() —— release DEX
const-string v0, "CVJjayoOcl0bDRMnFCUtay4UfxNYbAZGfHlrXjsTC1cfXw=="
invoke-static {v0}, Lcom/vapro/vae/stringfog/StringFogRuntime;.decrypt:(Ljava/lang/String;)Ljava/lang/String;
# 明文 "sk-JAVA-PLAINTEXT-..." 在 release DEX 计数=0(消失);debug=1② AES 模式(-Psf.algo=aes -Psf.key=my-release-key-2026,三参路径)
const-string v0, "my-release-key-2026"
const-string v1, "aes"
const-string v2, "DkySB3hkRyzW6kHRx6VGwjISWgTY+L+oaRDZ1qGGh7e/R0V9IJoX9Ny/xEOoRt5ik7jOjFJWwwOhz8hcEr+FJQ=="
invoke-static {v2, v0, v1}, L…/StringFogRuntime;.decrypt:(3×String)String③ 每串随机密钥(-Psf.randomKey=true) —— 每串一个随机 16 位密钥(6 串实测 6 个互异密钥):
const-string v0, "xni1fX647FTX3nn9" # 该串专属随机密钥
const-string v1, "xor"
const-string v2, "CwVEeycOdxlnChURfTorYSxDWANVbAMCAH5taFIMDV0dCA=="
invoke-static {v2, v0, v1}, L…/StringFogRuntime;.decrypt:(3×String)String④ bytes 模式(-Psf.bytes=true) —— 无 Base64 文本,密文以 fill-array-data 存储:
new-array v0, v0, [B
fill-array-data v0, 0000000c
invoke-static {v0}, L…/StringFogRuntime;.decrypt:([B)Ljava/lang/String;⑤ AAR 库端到端(sample/lib → classes.jar → javap -c) —— 库字符串同样被加密:
# LibJavaSecrets.libApiToken() —— AAR classes.jar
0: ldc #35 // String CVJjbSIaHjoKFxNDCj0pejRtC3k/dVIROWwzDDkVDVVLC30VXm4=
2: invokestatic #33 // Method .../StringFogRuntime.decrypt:(String)String
# classes.jar 内 4 条库明文 grep 计数均=0
复现:根
./gradlew publishToMavenLocal→./gradlew -p sample :app:assembleDebug :app:assembleRelease :lib:assembleRelease(各模式加-Psf.*)→dexdump -d(APK)/javap -c(AAR)对比即得。
实现 IStringFog(构建期加密与运行时解密的唯一契约),构建期用 DSL 指定全限定类名、运行时以同名 id 注册。
// 1) 实现算法(放在 buildscript classpath 与 App 都能看到的位置)
package com.your.app.crypto;
import com.vapro.vae.stringfog.IStringFog;
public final class MyFog implements IStringFog {
@Override public byte[] encrypt(byte[] data, byte[] key) { return transform(data, key); }
@Override public byte[] decrypt(byte[] data, byte[] key) { return transform(data, key); }
private byte[] transform(byte[] d, byte[] k) { /* ... */ return d; }
}// 2) 构建期指定(需 public 无参构造,构建期反射构造)
stringfog { algorithm.set("com.your.app.crypto.MyFog") }// 3) 运行时注册(App 早期,如 Application.onCreate)
StringFogRuntime.register("com.your.app.crypto.MyFog", new com.your.app.crypto.MyFog());内置 xor / aes 已默认注册,无需手动 register。
全部为可复现的真实证据:
./gradlew test(24 单测/集成)、真实 APK/AAR 反编译、AGP 8.7 与 9.2 双档真实构建。声明=测试,未测的诚实标注。
./gradlew test 汇总:24 / 24 通过(0 失败 0 错误) —— stringfog 11 + stringfog-gradle-plugin 13。
| 维度 | 结果 | 证据 |
|---|---|---|
| AGP 9.2.1 + Gradle 9.6 + JDK 21 + compileSdk 36 | ✅ 实测 | sample/ 真实 APK+AAR 反编译 |
| AGP 8.7.3 + Gradle 8.13 + JDK 21 + compileSdk 35 | ✅ 实测 | sample-agp8/ 真实 APK:release 明文=0,debug=1 |
| Java @ APK / Kotlin @ APK | ✅ | sample/app release DEX 明文计数=0 |
| Java @ AAR / Kotlin @ AAR | ✅ | sample/lib classes.jar javap 反编译,明文=0 |
| XOR / AES / 每串随机密钥 / bytes 往返 | ✅ | StringFogRuntimeTest + StringFogTransformTest |
| v1.1.0 逐字节向后兼容 | ✅ | backwardCompatWithV110 |
| AGP 7.2 ~ 8.6 | API 自 AGP 7.2 稳定,理论兼容 | |
| JDK 17(下限) | 产物 target=17,本环境以 JDK 21 构建 |
| 组件 | 版本 | 说明 |
|---|---|---|
| AGP | 8.7.3 与 9.2.1(双档实测) | AsmClassVisitorFactory(AGP 7.2+ 提供);7.2~8.6 理论兼容未逐版实测 |
| Gradle | 8.13 与 9.6.0 | 主构建锁 9.6.0;sample-agp8 锁 8.13 |
| Kotlin | 随 Gradle kotlin-dsl 内嵌 |
插件源码 100% Kotlin |
| JDK | 构建/测试 JDK 21;产物字节码 Java 17(下限) | 低版本字节码向上兼容 |
| ASM | 9.9 | 由 AGP 内置提供 |
| minSdk | 26+ | 运行时用 java.util.Base64(API 26 起);sample 取 28 |
StringFogPro/
├── stringfog/ # 运行时 + 算法库(纯 Java,零依赖,JitPack 发布)
│ └── .../stringfog/
│ ├── IStringFog.java # 加解密统一契约
│ ├── IKeyGenerator.java # 每串密钥生成器契约
│ ├── RandomKeyGenerator.java# 随机每串密钥实现
│ ├── XorFog.java # 默认 XOR 循环密钥(兼容 v1.1.0)
│ ├── AesFog.java # AES-128/CBC/PKCS5(随机 IV 前置)
│ └── StringFogRuntime.java # 运行时解密调度器(四入口 + 注册表)
├── stringfog-gradle-plugin/ # AGP 插桩插件(100% Kotlin,JitPack 发布)
│ └── .../plugin/
│ ├── StringFogPlugin.kt # onVariants release 注册 + 配置校验
│ ├── StringFogExtension.kt # stringfog { } DSL
│ ├── StringFogFactory.kt # AsmClassVisitorFactory
│ └── core/ # 纯 ASM 变换核心(可独立单测)
│ ├── FogConfigResolver.kt
│ ├── PackageFilter.kt
│ ├── MappingWriter.kt
│ └── StringFogMethodVisitor.kt
├── sample/ # 演示多模块(:app APK + :lib AAR)
└── sample-agp8/ # 多 AGP 适配第二档(AGP 8.7.3 + Gradle 8.13)
- 运行时为何是 Java 而非 Kotlin? 运行时 JAR 会被打进每个消费方 APK,目标是零依赖极小。用 Kotlin 会强制引入
kotlin-stdlib(约 1.5MB+),破坏零依赖定位。加密在 ASM 字节码层,Kotlin 业务类同样被加密。 - 密钥并非密码学机密。 密钥(含每串随机密钥)随字节码嵌入 APK。目标是抵御
strings/grep 明文扫描、抬高逆向成本,而非提供不可破解的加密。 - 编译期常量字段加密(v2.3.0 起)。
const val(Kotlin)/static final String(Java)编译后明文以字段的ConstantValue属性形态落进常量池(非方法体LDC)——v2.3.0 前是覆盖不到的盲点。现对「static+String类型 + 带ConstantValue属性、且达minLength门槛」的常量字段,剥离其明文ConstantValue(字段变无初值static [final]),并在类<clinit>注入「密文+decrypt+PUTSTATIC」运行期还原(PUTSTATIC对final static字段仅在本类<clinit>合法,注入位置正满足);无<clinit>则合成一个,有则前插既有<clinit>(密文序列在方法体两类改写之后插入,不被二次加密)。取值经字段读取运行期解密,逐字节等于原文。短于minLength的常量字段保留原ConstantValue(不误伤)。 - 拼接字面量加密(v2.2.0 起)。 除
LDCString 常量外,Java 9+/Kotlin 的invokedynamic makeConcatWithConstants("文本"+变量、字符串模板)也被处理:去糖为等价StringBuilder链,把 recipe 字面量片段与�bootstrap 常量里 ≥minLength的明文改为运行期decrypt,动态参数按精确类型append,拼接结果逐字节不变。无可加密字面量的拼接(纯变量 /makeConcat)原样保留、零改动。此模式下插件对被插桩类请求COMPUTE_FRAMES重算帧。 - 超长串跳过。 超阈值(Base64 45000 / bytes 8000 UTF-8 字节)保持明文,避免常量池 65535 上限,远超真实密钥/URL 长度。
- 作用域
PROJECT。 仅加密本模块源码,不加密依赖(避免二次加密 / 加密 AndroidX)。
- 插件 marker 自动生成 ——
java-gradle-plugin已生成com.va.stringfog:com.va.stringfog.gradle.plugin,publishToMavenLocal一并发布;mavenLocal / 未来 Portal 下plugins { id("com.va.stringfog") version "2.2.0" }一行接入已实测跑通(DXApp :webkitsdk)。 - 发布到官方 Gradle Plugin Portal —— 需外部发布账号,本项目暂无;在此之前 JitPack 分发用一次
resolutionStrategy映射保持一行式,或退回 buildscript classpath。 - AGP 7.2 ~ 8.6 逐版实测 —— API 层理论兼容,已实测 8.7.3 与 9.2.1 两档。
- JDK 17 下限实测 —— 产物面向 Java 17,本环境以 JDK 21 构建/测试。
- R8/混淆共存实测 —— sample 关闭 R8 聚焦插桩;插桩发生在 R8 前,理论无冲突。
v2.1.0 已补齐的原 Roadmap 项:每串随机密钥、bytes 免 Base64、mapping 映射文件、AAR 库端到端反编译证据——均附实现位与测试/反编译证据。
- 新增字段
ConstantValue加密:const val(Kotlin)/static final String(Java)编译后,明文以字段的ConstantValue属性形态落进 class 常量池——旧版只改方法体LDC/invokedynamic,覆盖不到字段属性,导致明文残留.class/AAR/dex(这正是上轮AdxIdentityContext.VIA_PACKAGE被迫从const val改普通val绕过的盲点)。本版在ClassVisitor层对达门槛的static+String常量字段剥离明文ConstantValue,并在类<clinit>注入「密文+decrypt+PUTSTATIC」——无<clinit>则合成、有则前插既有<clinit>(密文序列在两类改写之后插入,杜绝二次加密)。字段读取运行期解密,逐字节等于原文。 - 发射器抽出:
LDC加密 / 拼接去糖块 / 字段<clinit>注入三处共用同一FogInsnEmitter(四形态与StringFogRuntime四条decrypt入口严格一致)。 - invokedynamic 复核结论:v2.2.0 的拼接去糖已覆盖 recipe/bootstrap 常量里的明文(含内联进 recipe 的
const)——新增constInlinedIntoConcatRecipe用例复现「ADXWebView 日志形态」并断言mark.via.gp清零,证实此前「recipe 覆盖不到」为误判;真正盲点仅是字段ConstantValue,本版根治。 - 测试:40/40 通过(旧 34 + 新 6 字段常量);新增用例覆盖 Java 无
<clinit>(合成)/ 有<clinit>(前插)、ASM 复刻 Kotlinobject(const val+INSTANCE,生产xor+每串密钥配置)、短于minLength不误伤、多常量字段一并注入、const内联进拼接 recipe 清零——均以「字段ConstantValue剥离 +.class明文消失 + 反射字段值逐字节等于原文」断言。
- 新增
makeConcatWithConstants加密:Java 9+/Kotlin 的"文本"+变量/字符串模板编译为invokedynamic,其字面量藏在 recipe 与 bootstrap 常量里(旧版 LDC 路径够不着)。本版把该invokedynamic去糖为等价StringBuilder链,加密达门槛字面量、动态参数按精确类型append,拼接结果逐字节等价。 - 树 API 重构:核心访问器改用
MethodNode,依方法真实maxLocals安全分配新局部(去糖需按逆序暂存栈上参数);LDC 加密与拼接块加密共用单一加密序列构造器。 - 帧模式:被插桩类请求
COMPUTE_FRAMES_FOR_INSTRUMENTED_CLASSES(去糖改变指令数并新增局部,帧/maxs 须重算)。 - 测试:34/34 通过(旧 24 + 新 10 invokedynamic);新增用例覆盖字面量+变量、多段长短混合、全基本类型参数、`` 常量折叠、bytes/AES/每串密钥、纯变量/
makeConcat不去糖、真实 `javac` 产物、含分支(StackMapTable)方法 `COMPUTE_FRAMES` 去糖——均以「对真实 `StringConcatFactory` 参考结果逐字节往返一致 + 明文消失」断言。
- 补齐能力:每串随机密钥、bytes 免 Base64、明文→密文映射文件、AAR 库端到端反编译证据。
- 运行时四入口:Base64/bytes × 单参/三参 的
decrypt重载。 - 代码精修:
shouldFog接口默认实现;ASM 边界防护;AesFog静态SecureRandom;PackageFilter可单测;线程安全审计。 - 配置健壮性:
minLength/algorithm/randomKeyLength校验早失败;重叠告警。 - 适配矩阵:24/24 通过;AGP 8.7.3+Gradle 8.13 与 AGP 9.2.1+Gradle 9.6 双档;Java/Kotlin × APK/AAR 真实反编译。
- 多模块化;统一
IStringFog契约;内置 AES-128/CBC;算法自主扩展;细粒度配置。
- 单模块纯 Java 运行时库:
StringFogRuntime.decrypt(String)(XOR + Base64)。
本项目基于 MegatronKing/StringFog(Apache-2.0)思想改写并做现代化超集升级,向原作者致谢。
本项目采用 Apache-2.0 协议,允许商用。详见 LICENSE。
关于作者
主业大模型算法 / AI / 端侧方向(Agentic · LangGraph · A2A · MCP · ADK · GraphRAG · 端侧离线多模态 · 车载 · 世界模型);ROM / 逆向是我长期钻研的技术兴趣。
欢迎交流合作 · 📮 yugu88@126.com · GitHub @Pangu-Immortal