HarmonyOS ArkGuard 源码混淆:保护代码与配置选项

release 构建时开启混淆,变量名变成 a、b、c,方法名面目全非,代码挤成一行——这就是 ArkGuard 干的事。混淆有两个直接目的:保护代码逻辑增加逆向难度、减小包体积。但 ArkGuard 不是开了就万事大吉,配错白名单运行时崩、混淆后堆栈看不懂、依赖包冲突这些问题都很常见。这篇把 ArkGuard 的工作原理、配置选项、常见坑讲清楚。

ArkGuard 能做什么不能做什么

先明确能力边界。ArkGuard 的能力范围:

能做的:

  • 名称混淆(变量名、属性名、文件名等重命名)
  • 代码压缩(删空格换行)
  • 注释删除
  • 删除 console.* 语句

不能做的:

  • 控制流混淆(不改执行逻辑)
  • 数据混淆(不改数据结构)
  • C/C++、JSON、资源文件的混淆

ArkGuard 只处理 ArkTS/TS/JS 代码。和 Java 生态的 ProGuard 比,ArkGuard 是基础混淆工具,没有 ProGuard 那些高级混淆能力。对代码安全要求高的场景,除了 ArkGuard,还要考虑应用加密、安全加固这些措施。

工作原理

ArkGuard 在编译时读取模块的 build-profile.json5 配置,解析并合并当前模块与依赖模块的混淆规则,对中间文件做混淆处理,输出到 build 目录。

build-profile.json5
混淆开关 + 规则文件

ArkGuard

当前模块
obfuscation-rules.txt

依赖模块
consumer-rules.txt / obfuscation.txt

UI 转换后的
中间代码

混淆后中间代码
落盘到 build 目录

混淆发生在 UI 转换之后、字节码生成之前。混淆处理的是中间代码,不是源码也不是字节码。

语言的限制

ArkGuard 针对 JS/TS/ArkTS,这些语言和 Java 这种强类型语言不一样。JS 支持运行时动态修改对象和函数,而混淆是编译期的静态处理,这种差异可能导致混淆后的名称在运行时解析不到。

TS/ArkTS 虽然有静态类型系统,但用的是结构性类型机制——相同结构的不同命名类型视为等价。这导致无法精确追踪类型来源。

一个具体后果:ArkGuard 用全局生效的属性保留机制。配置保留属性 prop1,所有叫 prop1 的属性都会被保留,没法只保留类 A 的 prop1 而混淆类 B 的 prop1。这是语言特性决定的限制,不是工具的问题。

怎么开启

在模块的 build-profile.json5 里配置:

{
  "arkOptions": {
    "obfuscation": {
      "ruleOptions": {
        "enable": true,                          // 开启混淆
        "files": ["./obfuscation-rules.txt"]     // 本模块混淆规则文件
      },
      "consumerFiles": ["./consumer-rules.txt"]  // 被依赖时生效的规则
    }
  }
}

混淆只在 release 构建生效,debug 不混淆。开启后默认只混淆局部变量和参数名,要更强的混淆效果需要在 obfuscation-rules.txt 里配置选项。

DevEco Studio 5.0.3.600 及以后版本,新建工程默认关闭混淆。obfuscation-rules.txt 默认有这四项推荐配置:

-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation

混淆配置选项

在 obfuscation-rules.txt 里配置。用 # 加注释。

开关选项

选项作用起始 API
-disable-obfuscation关闭所有混淆10
-enable-property-obfuscation属性名称混淆10
-enable-string-property-obfuscation字符串属性名混淆(需配合上一项)10
-enable-toplevel-obfuscation顶层作用域名称混淆10
-enable-export-obfuscation导入导出名称混淆10
-enable-filename-obfuscation文件/文件夹名混淆10
-compact代码压缩到一行10
-remove-comments删声明文件 JSDoc 注释10
-remove-log删 console.* 调用10

看几个具体效果。

-enable-property-obfuscation:

// 混淆前
class TestA {
  static prop1: number = 0;
}
TestA.prop1;

// 混淆后
class TestA {
  static i: number = 0;
}
TestA.i;

-enable-toplevel-obfuscation:

// 混淆前
let count = 0;

// 混淆后
let s = 0;

-compact:

// 混淆前
class TestA {
  static prop1: number = 0;
}
TestA.prop1;

// 混淆后
class TestA { static prop1: number = 0; } TestA.prop1;

保留选项

光开混淆不开保留,几乎一定会出问题。保留选项指定哪些名称不能混淆。

选项作用
-keep-property-name保留指定属性名
-keep-global-name保留顶层作用域/导入导出名称
-keep-file-name保留文件/文件夹名
-keep-comments保留指定元素的 JSDoc 注释
-keep-dts保留 .d.ts 文件中的所有名称
-keep保留指定路径下所有名称

配置示例:

-keep-property-name
firstName
lastName
addNum

-keep-file-name
DynamicImportFile

通配符支持:? 匹配单个字符,* 匹配任意数量字符。

-keep-property-name
a*          # 保留所有 a 开头的属性名
?           # 保留所有单字符属性名
*           # 保留所有属性名(等于关了属性混淆)

哪些场景必须配白名单

这是实操中最关键的部分。以下场景的名称如果不配白名单,运行时大概率出问题。

1. so 库 API 名称

调用 native so 库的方法,方法名必须保留:

import testNapi from 'libentry.so';
testNapi.addNum(2, 3);  // addNum 必须保留

配置:

-keep-property-name
addNum

2. JSON 文件字段

import jsonData from './data.json';
let val = jsonData.jsonProperty;  // jsonProperty 必须保留

3. 数据库字段

const valueBucket: ValuesBucket = {
  ID1: 'ID1',      // ID1 必须保留
  NAME1: 'jack'    // NAME1 必须保留
}

4. 动态属性访问

字符串拼接或变量访问属性:

const obj = { staticName: 'value' };
const fieldName = 'static' + 'Name';
console.info(obj[fieldName]);  // staticName 必须保留

5. 网络请求字段

httpRequest.request('https://example.com/Login', {
  extraData: { usernameTest: 'test1', passwordTest: 'test2' }
  // usernameTest 和 passwordTest 必须保留
});

6. 动态 import 路径

const moduleName = './DynamicImportFile';
const modules = await import(moduleName);  // DynamicImportFile 必须保留

配置:

-keep-file-name
DynamicImportFile

混淆规则怎么合并

编译一个模块时,最终生效的规则是当前模块规则和依赖模块规则的合并结果。

当前模块
ruleOptions.files

合并后规则

依赖的本地 HSP
consumerFiles

依赖的本地 HAR
consumerFiles

依赖的远程 HAR/HSP
obfuscation.txt

合并逻辑:

  • 混淆选项:或运算。任意一个规则文件里开了某选项,最终就包含该选项。
  • 保留选项:并集。所有规则文件里的白名单合并。

API version 18 之后,默认只合并依赖模块的保留选项,不合并混淆选项。这避免了依赖包的混淆配置影响主模块。如果需要恢复旧行为,配置 -enable-lib-obfuscation-options。

三种配置文件的区别

文件谁能改影响本模块影响依赖方
obfuscation-rules.txt开发者是否
consumer-rules.txt开发者否是
obfuscation.txt自动生成否是

consumer-rules.txt 的规则在别的模块依赖本模块时生效。建议只在 consumer-rules.txt 里配保留选项,别配混淆选项,免得影响主模块的混淆效果。

混淆后的代码长什么样

源码:

export class UserService {
  private userName: string = '';
  private age: number = 0;
  
  setName(name: string): void {
    this.userName = name;
    console.info('set name:', name);
  }
  
  getName(): string {
    return this.userName;
  }
}

开启全部混淆 + compact + remove-log 后,大致变成:

export class a {
  private b: string = '';
  private c: number = 0;
  d(e: string): void { this.b = e; }
  f(): string { return this.b; }
}

(export 的类名 a 是因为开了 -enable-export-obfuscation,方法体内的 console.info 被 -remove-log 删掉了,代码挤成一行是 -compact 的效果。)

混淆与调试的关系

混淆后代码名称都变了,crash 堆栈里也是混淆后的名称,直接看不懂。还原需要两个文件:

  • nameCache.json:名称映射表,记录混淆前后的对应关系。构建后在 build/default/.../release/obfuscation/ 下。
  • sourceMaps.map:源码映射信息,记录压缩/转换后代码到原始源码的映射。

用 DevEco Studio Command Line Tools 里的 hstack 插件可以自动还原堆栈:

hstack <crash日志文件> -s <sourceMaps.map路径> -n <nameCache.json路径>

nameCache.json 必须备份。每次全量构建都会覆盖这个文件。线上版本 crash 排查时,如果没有对应版本的 nameCache.json,堆栈就还原不了。发版时把这个文件和 sourceMaps.map 一起存档。

常见混淆问题排查

问题一:运行时找不到方法

现象:release 包运行崩溃,报错类似 xxx is not a function。
原因:某个方法名被混淆,但运行时通过原名访问了。
排查:看报错的方法名,在 obfuscation-rules.txt 里加 -keep-property-name 或 -keep-global-name。

问题二:so 库调用失败

现象:调用 native 方法报错。
原因:so 库的方法名被混淆。
排查:把所有 so 库导出的方法名加进 -keep-property-name。

问题三:JSON 解析后字段 undefined

现象:读 JSON 数据字段返回 undefined。
原因:JSON 文件的字段名被混淆了。
排查:把用到的 JSON 字段名加进白名单。

问题四:动态 import 失败

现象:import(path) 报找不到模块。
原因:文件名被混淆,动态 import 的路径对不上。
排查:把动态 import 的路径名加进 -keep-file-name。

问题五:路由跳转失败

现象:页面跳转报错。
原因:路由配置里的页面路径被混淆。
排查:路由表里的 pageSourceFile 路径加进 -keep-file-name。API version 20 之后系统自动处理,老版本要手动配。

问题六:HAR 包被依赖时出问题

现象:依赖某个 HAR 后,主模块运行异常。
原因:HAR 的 consumer-rules.txt 配了混淆选项,影响了主模块。
排查:检查 HAR 的 consumer-rules.txt,只保留保留选项,删掉混淆选项。

实践中要注意的

逐项开启混淆:别一上来就开所有混淆选项。先开 -enable-toplevel-obfuscation,跑通了再开 -enable-property-obfuscation,最后开 -enable-export-obfuscation 和 -enable-filename-obfuscation。每开一项都跑一遍功能,出问题好定位。

release 和 debug 行为差异:debug 不混淆,release 混淆。只在 release 出现的问题,优先怀疑混淆。排查时先关掉混淆确认是不是混淆导致的。

-compact 影响堆栈定位:release 堆栈只有行号没有列号,开了 -compact 后所有代码挤一行,行号定位失效。如果希望某些路径保留换行方便看堆栈,用 -keep-uncompact 指定不压缩的路径。

-enable-string-property-obfuscation 要谨慎:字符串属性名混淆后,如果代码里有特殊字符的字符串属性(比如 "\n"、""),可能没法通过白名单保留。这种场景别开这个选项。

@KeepSymbol 注解标记白名单:API version 19 开始,可以在源码里用 // @KeepSymbol 注释标记不混淆的名称,比在配置文件里写白名单更直观:

// @KeepSymbol
class MyClass02 {
  prop01: string = "prop";  // MyClass02 和 prop01 都不会混淆
}

几条经验哦

  1. so 库 API 白名单第一时间配上:这是混淆最常见的坑,新项目开混淆前先把所有 so 库导出的方法名列进白名单。
  2. nameCache.json 每个版本都要存:发版时把 nameCache.json 和 sourceMaps.map 一起归档,线上 crash 还原全靠这两个文件。
  3. consumer-rules.txt 只放保留选项:别在 consumer-rules.txt 里配混淆选项,会影响依赖方的主模块。
  4. 用混淆助手排查:DevEco Studio 有混淆助手功能,能帮着识别需要配置的白名单,比手动排查快。
  5. 混淆不是安全银弹:ArkGuard 只做基础混淆,对有安全要求的场景,还要配合应用加密、加固这些措施。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐