系列文章《鸿蒙开发从入门到专家》第 33 篇 · 中级
安全加固这件事,最大的认知误区是"鸿蒙应用包只有我自己能装,没人会动它"。
现实情况是:HAP 一旦发布,攻击者就能拿到压缩包;能拿到压缩包,意味着能看到你的代码、资源、API 调用。剩下的只是时间和意愿问题。越是涉及账号、支付、敏感数据的应用,越要把基础防线打齐。这篇从代码视角把鸿蒙应用常见加固手段过一遍,按"先签名 → 再混淆 → 后运行时"的层次讲。
一、签名与发布:别被替换包
鸿蒙安装包必须经过签名才能在系统上安装。签名本身不是用来"加密"的,它的真实作用是:确认包没被篡改,且发布者就是你。
很多团队忽略的细节:
1. 签名文件保管
签名用 .p12 + .p7b + 密码组合,务必存到安全位置。一旦泄密,攻击者可以用同签名发布"姊妹版"应用,覆盖用户安装。建议:
签名密钥不在开发机明文存档。
CI 上传时使用安全变量,不要写死在
build-profile.json5。团队内按需分配签名权限。
2. 启用 V2 签名方案
// build-profile.json5
{
"app": {
"signingConfigs": {
"release": {
"material": {
"storeFile": "sign/release.p12",
"storePassword": "***",
"keyAlias": "release",
"keyPassword": "***"
},
"signAlgorithm": "SHA256withECDSA",
"profile": "sign/release.p7b"
}
},
"products": [
{
"name": "default",
"signingConfig": "release"
}
]
}
}
签名算法优先选 SHA256withECDSA 或 SHA384withECDSA,避免老旧的 SHA1withRSA。
3. 上架前自检
发布前用 hdc shell bm install -p xxx.hap 试试能否重装、被覆盖。如果一个 HAP 能被未经签名的同名包覆盖,签名流程就有问题。
二、代码混淆:提升逆向门槛
混淆不是"加密",但能让攻击者从源码读到 “lambda$0” 这种毫无意义的名字。要把它用扎实,规则文件得配合代码仔细调。
1. 启用混淆
// obfuscation-rules.txt
-keep public class com.huawei.myapp.** { *; }
-keepclassmembers class com.huawei.myapp.entry.** { *; }
-keepattributes Signature, *Annotation*
# 不混淆 JSON 序列化字段
-keepclassmembers class com.huawei.myapp.data.** { <fields>; }
# 不混淆 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}
-keep 的规则要克制,能减少的就减少。覆盖率越高,混淆效果越差。一般经验:
入口 Ability、ServiceAbility 不混淆(系统调用)。
与系统框架交互的接口(如
IAbilityContinuation)保留。反射用到的字段、类保留。
其他都可以混淆。
2. 字符串加密
代码里的 API 地址、密钥、错误信息都可以做轻度加密:
// utils/strcrypt.ts
const KEY = [0x53, 0x7a, 0x21, 0x10];
export function decrypt(buf: Uint8Array): string {
const out: number[] = [];
for (let i = 0; i < buf.length; i++) {
out.push(buf[i] ^ KEY[i % KEY.length]);
}
return String.fromCharCode(...out);
}
调用端:
import { decrypt } from '../utils/strcrypt';
const url = decrypt(new Uint8Array([0x35, 0x12, 0x68, 0x37]));
不指望它真防得住逆向,但至少拖慢静态分析。
3. 资源混淆
上一篇包体积优化已经讲过,资源混淆会改资源路径,把文件名变成 assets/r/_a1.png。视觉上不直观,逆向工具也跟着麻烦。
三、密钥保护:把硬编码的密钥搬家
代码里硬编码 AK/SK、AES key、JWT secret 是反模式。运行时需要的密钥建议三选一:
1. 服务端动态下发
最简单。客户端启动时调用 /api/secret/get?appKey=xxx&sign=xxx,服务端验证包签名后下发密钥。优点是不进包;缺点是要保证调用本身是安全的(防中间人和重放)。
2. Keystore 存储
鸿蒙提供 keystoreManager,支持生物特征绑定:
import { keystoreManager } from '@kit.SecurityKit';
const key = keystoreManager.generateKey({
alias: 'user_pay_token',
purpose: keystoreManager.KeyPurpose.ENCRYPT | keystoreManager.KeyPurpose.DECRYPT,
securityLevel: keystoreManager.SecurityLevel.S4,
});
密钥不出用户态,TEE/SE 保护。需要配合生物认证才能解锁。
3. 拆分存储
把密钥拆成三块:
一块二进制文件里(混淆)。
一块写到系统设置里。
一块从网络获取。
启动时拼起来,少一块都不行。绕过成本会显著上升。
4. 防截屏
金融类应用常做"涉及金额/二维码的页面禁止截屏":
import { window } from '@kit.ArkUI';
window.getLastWindow(getContext()).then(win => {
win.setWindowPrivacyMode(true);
});
setWindowPrivacyMode(true) 后,截屏会得到黑屏。截屏后系统也不会自动出现在某些敏感页面里。
四、调试与运行时防护
只做静态加固还不够,运行时也要做几件事。
1. 防调试
import { appRecovery } from '@kit.AppRecoveryKit';
// 启动时检查是否被调试
if (process.versions) {
// Node 风格的环境,某些逆向工具会注入 process 对象
}
最朴素也最有效的方式是检测 hdc 连接并自我退出:
// 仅 debug 包开启,生产包剔除这段代码
if (process.env['NODE_ENV'] !== 'production') {
// 开发模式正常
} else {
// 生产模式:检测到 hdbg / Frida 等行为则立刻退出
}
更"硬"的方式是验证应用的完整性签名——但这要求 HAP 在签名时携带一段静态校验信息,目前业内做法是比对包内一个空 hash 文件,启动时校验。
2. 防止二次打包
核心思路:客户端启动时把自己的签名指纹上报到服务端,服务端校验是否与登记一致。
import { bundleManager } from '@kit.AppKit';
const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO;
bundleManager.getBundleInfoForSelf(bundleFlags).then((info) => {
const fp = info.signatureInfo.appSignature;
fetch('/api/security/check', { body: JSON.stringify({ fp }) });
});
服务端校验不通过则拒绝服务。这是反"伪正版"应用的有效手段。
3. 运行时完整性校验
鸿蒙目前没有像 Android 的 SafetyNet 那样的强校验 API,但可以通过以下方式组合:
网络层 TLS pinning,防中间人。
启动时验证关键 so 文件的 hash。
检测 root/越狱(鸿蒙暂不涉及,但分布式设备场景复杂)。
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
const sha = cryptoFramework.createHash('SHA256');
sha.update(await readSoContent());
const expected = 'expectedHashHere...';
if (sha.digest() !== expected) {
// 异常上报并退出
}
五、传输与存储安全
1. HTTPS + Pinning
所有网络请求强制 HTTPS,不要给 http 留口子。Pinning 是把服务器证书的公钥或证书指纹塞在客户端里。
import { http } from '@kit.NetworkKit';
const req = http.createHttp();
req.on('sslVerify', (event) => {
const pinned = 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
if (event.cert !== pinned) {
return false;
}
return true;
});
注意:不要 pin 整个证书(Pinning 顶级域名),换成 pin 公钥,留有证书轮换空间。
2. 本地存储加密
Preferences、KV 存储里的敏感数据要加密:
import { preferences } from '@kit.DataPreferencesKit';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
const KEY_ALIAS = 'user_token_key';
async function storeToken(token: string) {
const aes = cryptoFramework.createSymKeyGenerator('AES256');
const symKey = aes.generateSymKey();
const cipher = cryptoFramework.createCipher('AES256|GCM|PKCS7');
cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey);
const enc = await cipher.doFinal({ data: new TextEncoder().encode(token) });
const pref = await preferences.getPreferences(getContext(), 'secure');
await pref.put('enc_token', enc.data);
}
AES256|GCM 模式兼顾了加密和完整性。
六、安全开发工程化
上面的内容零散,实际落地要靠工程化让团队都能用上:
七、案例:金融钱包应用的安全清单
一个支付类应用的常见加固方案:
启动校验:包签名指纹 + so 哈希。
网络:强制 TLS 1.3 + Pinning + 重放保护。
本地:所有金额/token 走 Keystore,不裸存。
页面:涉及金额的页面开启
setWindowPrivacyMode(true)。调试:检测异常连接自动锁定并上报。
后端:所有关键操作带 server-signed timestamp 防重放。
升级:核心 SDK 升级必须用官方签名校验。
这套方案在中等规模的金融团队里都是够的。如果应用有更严格的合规要求(如 PCI-DSS),还要在服务端配合硬件加密机、TEE 服务端做更多事。
小结
安全加固是"成本高、回报慢"的工作,但一次没做就足以让公司上榜事故。核心做法:
签名:保管好密钥、V2 方案、上架自检。
混淆:覆盖率最大化、字符串轻加密、资源混淆。
密钥:动态下发 + Keystore + 拆分存储。
运行时:防调试、防二次打包、运行时校验。
传输:强制 TLS、Pinning。
存储:敏感数据加密落盘。
下一篇要解决的是结构问题——组件化和插件化。每个 app 写到第二个版本都会面对"模块边界模糊、循环依赖、改一行牵全身"的噩梦,组件化是怎么解决这个问题的。
参考阅读
鸿蒙开发者文档《应用安全设计指南》
鸿蒙开发者文档《代码混淆工具使用》
OWASP MASVS(移动应用安全验证标准)
《Android 移动安全实战》(横向借鉴)