Administrator
发布于 2026-09-02 / 1,107 阅读
0
0

鸿蒙安全加固:签名、混淆、反调试的实战清单

系列文章《鸿蒙开发从入门到专家》第 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 模式兼顾了加密和完整性。

六、安全开发工程化

上面的内容零散,实际落地要靠工程化让团队都能用上:

做法

工具/位置

静态扫描

鸿蒙 Studio 自带 lint + 自定义规则

依赖检测

定期 npm audit / 检查供应链

密钥管理

服务端动态下发 + Keystore 双备份

上线前扫描

自研脚本扫描硬编码 IP、密钥

安全 review

大版本前由安全工程师专项 review

七、案例:金融钱包应用的安全清单

一个支付类应用的常见加固方案:

  1. 启动校验:包签名指纹 + so 哈希。

  2. 网络:强制 TLS 1.3 + Pinning + 重放保护。

  3. 本地:所有金额/token 走 Keystore,不裸存。

  4. 页面:涉及金额的页面开启 setWindowPrivacyMode(true)。

  5. 调试:检测异常连接自动锁定并上报。

  6. 后端:所有关键操作带 server-signed timestamp 防重放。

  7. 升级:核心 SDK 升级必须用官方签名校验。

这套方案在中等规模的金融团队里都是够的。如果应用有更严格的合规要求(如 PCI-DSS),还要在服务端配合硬件加密机、TEE 服务端做更多事。

小结

安全加固是"成本高、回报慢"的工作,但一次没做就足以让公司上榜事故。核心做法:

  • 签名:保管好密钥、V2 方案、上架自检。

  • 混淆:覆盖率最大化、字符串轻加密、资源混淆。

  • 密钥:动态下发 + Keystore + 拆分存储。

  • 运行时:防调试、防二次打包、运行时校验。

  • 传输:强制 TLS、Pinning。

  • 存储:敏感数据加密落盘。

下一篇要解决的是结构问题——组件化和插件化。每个 app 写到第二个版本都会面对"模块边界模糊、循环依赖、改一行牵全身"的噩梦,组件化是怎么解决这个问题的。

参考阅读

  • 鸿蒙开发者文档《应用安全设计指南》

  • 鸿蒙开发者文档《代码混淆工具使用》

  • OWASP MASVS(移动应用安全验证标准)

  • 《Android 移动安全实战》(横向借鉴)


评论