Administrator
发布于 2026-09-03 / 9,941 阅读
0
0

鸿蒙应用安全审计与合规检查

鸿蒙百篇系列第 79 篇 · 专家

第 12 篇讲过安全机制与权限管理,第 33 篇讲过安全加固。这篇站在安全审计的角度,系统梳理鸿蒙应用在上线前需要做的安全检查和合规对照。

安全不是加一个混淆就完了的事。HarmonyOS NEXT 的星盾安全架构从系统层提供了能力,但开发者如果不知道该用哪些、怎么用,这些能力等于没有。这篇文章是一份审计清单,帮你逐项对照。

一、星盾安全架构:开发者需要知道的部分

HarmonyOS NEXT 的星盾安全架构包含四个层面:

层面

系统提供

开发者责任

生态纯净

上架前自动漏洞扫描 + 人工审核

确保提交版本通过安全检测

隐私可控

禁止 9 类不合理权限、单次授权机制

只申请必要权限,适配单次授权流程

数据高安

硬件级一文一密、端端加密

敏感数据加密存储,不在日志泄露

分布安全

跨设备数据分类分级、加密传输

跨设备数据传输使用系统能力,不自建加密

星盾架构中,系统层做的事已经很多了。但开发者的责任不变:你申请了什么权限、怎么存敏感数据、第三方 SDK 是否安全——这些系统不管,出了事是你的。

二、权限审计清单

HarmonyOS NEXT 禁止的 9 类权限

以下权限在 HarmonyOS NEXT 中被禁止,如果你的代码里还有调用,必须移除:

被禁权限

替代方案

读取已安装应用列表

无直接替代,通过系统分享/打开方式间接获取

访问短信

系统短信应用处理,应用无法直接读取

访问存储文件(全盘)

使用文件选择器(FilePicker)

读取联系人

通过系统选择器,用户逐次选择

读取通话记录

无直接替代

读取日历

通过系统日历 API 按需访问

读取通知

无直接替代

访问剪贴板

仅在用户粘贴时单次访问

精准位置(非必要场景)

使用大致位置替代

权限申请原则

import abilityAccessCtrl from '@ohos.abilityAccessCtrl'

class PermissionManager {
  private accessManager: abilityAccessCtrl.AccessManager =
    abilityAccessCtrl.createAccessManager()

  // 只在真正需要时申请,不要启动就批量申请
  async requestLocationForMap(): Promise<boolean> {
    const token = this.getContext().accessToken

    try {
      const result = await this.accessManager.requestPermissionsFromUser(
        token,
        ['ohos.permission.APPROXIMATELY_LOCATION']  // 优先用大致位置
      )

      if (result.authResults[0] === 0) {
        return true
      }
      return false
    } catch (e) {
      console.error('Location permission denied: ' + JSON.stringify(e))
      return false
    }
  }

  // 如果确实需要精准位置,二次申请
  async requestPreciseLocation(): Promise<boolean> {
    // 先检查是否已有大致位置权限
    const token = this.getContext().accessToken
    const hasApprox = await this.checkPermission(token, 'ohos.permission.APPROXIMATELY_LOCATION')

    if (!hasApprox) {
      return false  // 连大致位置都没有,不要直接申请精准
    }

    const result = await this.accessManager.requestPermissionsFromUser(
      token,
      ['ohos.permission.LOCATION']
    )
    return result.authResults[0] === 0
  }

  private async checkPermission(token: number, permission: string): Promise<boolean> {
    const status = await this.accessManager.checkAccessToken(token, permission)
    return status === 0  // PERMISSION_GRANTED
  }
}

权限审计要点

逐个模块检查以下事项:

  • [ ] 每个 requestPermissionsFromUser 调用,是否有明确的触发时机(用户操作而非启动时批量)

  • [ ] 是否存在请求了但从未使用的权限

  • [ ] 权限被拒绝后是否有降级方案(大致位置替代精准位置、选择器替代全盘文件访问)

  • [ ] targetSDKVersion 对应的权限级别是否匹配(normal / system_basic / system_core)

  • [ ] module.json5 中 requestPermissions 的 reason 字段是否填写了中文说明

三、数据存储加密审计

需要加密的数据

数据类型

示例

加密要求

用户凭据

密码、Token、SessionID

必须,用 Asset Store Kit

敏感个人信息

身份证号、银行卡号

必须,用 cryptoFramework 加密后存储

业务数据

订单数据、聊天记录

视业务需求,建议加密

普通偏好

主题、语言

不需要

Asset Store Kit:对标 iOS Keychain

HarmonyOS NEXT 提供了 Asset Store Kit,专门用于短敏感数据的本地安全存储,基于硬件 TEE 级别加密:

import asset from '@ohos.security.asset'

class SecureStorage {
  // 存储
  async storeToken(token: string): Promise<void> {
    const attributes: asset.AssetMap = {
      'ALIAS': { type: asset.AssetType.BYTES, value: this.stringToUint8Array('user_token') },
      'SECRET': { type: asset.AssetType.BYTES, value: this.stringToUint8Array(token) },
      'ACCESSIBILITY': {
        type: asset.AssetType.NUMBER,
        value: asset.Accessibility.DEVICE_FIRST_UNLOCKED  // 设备首次解锁后可访问
      },
      'SYNC_TYPE': {
        type: asset.AssetType.NUMBER,
        value: asset.SyncType.NONE  // 不跨设备同步
      }
    }

    try {
      await asset.add(attributes)
    } catch (e) {
      // 如果已存在,先删再加
      if ((e as BusinessError).code === 0x8016) {
        await asset.remove({ 'ALIAS': attributes.ALIAS })
        await asset.add(attributes)
      }
    }
  }

  // 读取
  async getToken(): Promise<string | null> {
    try {
      const query: asset.AssetMap = {
        'ALIAS': { type: asset.AssetType.BYTES, value: this.stringToUint8Array('user_token') }
      }
      const results = await asset.query(query)
      if (results.length > 0) {
        return this.uint8ArrayToString(results[0].SECRET.value)
      }
      return null
    } catch (e) {
      console.error('Get token failed: ' + JSON.stringify(e))
      return null
    }
  }

  // 删除
  async clearToken(): Promise<void> {
    await asset.remove({
      'ALIAS': { type: asset.AssetType.BYTES, value: this.stringToUint8Array('user_token') }
    })
  }

  private stringToUint8Array(str: string): Uint8Array {
    const encoder = new TextEncoder()
    return encoder.encode(str)
  }

  private uint8ArrayToString(bytes: Uint8Array): string {
    const decoder = new TextDecoder('utf-8')
    return decoder.decode(bytes)
  }
}

cryptoFramework:业务数据加密

对于不适合用 Asset Store 的较大数据(如聊天记录),用 cryptoFramework 做 AES 加密:

import cryptoFramework from '@ohos.security.cryptoFramework'

class DataEncryptor {
  private keyAlias: string = 'app_aes_key_v1'

  // 初始化密钥(首次生成,后续复用)
  async initKey(): Promise<void> {
    try {
      // 尝试获取已有密钥
      await this.getKey()
    } catch (e) {
      // 不存在则生成新密钥
      const keySpec: cryptoFramework.HKeySpec = {
        algorithm: 'AES256',
        keySize: 256,
        alias: this.keyAlias,
        purpose: cryptoFramework.CryptoPurpose.ENCRYPT_DECRYPT
      }
      await cryptoFramework.generateKey(keySpec)
    }
  }

  // 加密
  async encrypt(plaintext: string): Promise<string> {
    const encryptSpec: cryptoFramework.HEncryptSpec = {
      algorithm: 'AES256',
      alias: this.keyAlias,
      purpose: cryptoFramework.CryptoPurpose.ENCRYPT,
      data: this.encode(plaintext)
    }
    const encrypted = await cryptoFramework.encrypt(encryptSpec)
    return this.toBase64(encrypted)
  }

  // 解密
  async decrypt(ciphertext: string): Promise<string> {
    const decryptSpec: cryptoFramework.HDecryptSpec = {
      algorithm: 'AES256',
      alias: this.keyAlias,
      purpose: cryptoFramework.CryptoPurpose.DECRYPT,
      data: this.fromBase64(ciphertext)
    }
    const decrypted = await cryptoFramework.decrypt(decryptSpec)
    return this.decode(decrypted)
  }

  private async getKey(): Promise<void> {
    // 查询密钥是否存在
    const keySpec: cryptoFramework.HKeySpec = {
      alias: this.keyAlias,
      purpose: cryptoFramework.CryptoPurpose.ENCRYPT_DECRYPT
    }
    await cryptoFramework.getKey(keySpec)
  }

  private encode(str: string): Uint8Array {
    return new TextEncoder().encode(str)
  }

  private decode(bytes: Uint8Array): string {
    return new TextDecoder('utf-8').decode(bytes)
  }

  private toBase64(bytes: Uint8Array): string {
    // Base64 编码逻辑
    let result = ''
    const len = bytes.length
    for (let i = 0; i < len; i += 3) {
      const b1 = bytes[i] || 0
      const b2 = bytes[i + 1] || 0
      const b3 = bytes[i + 2] || 0
      result += String.fromCharCode(
        (b1 >> 2) & 0x3F,
        ((b1 & 0x3) << 4) | ((b2 >> 4) & 0xF),
        ((b2 & 0xF) << 2) | ((b3 >> 6) & 0x3),
        b3 & 0x3F
      )
    }
    return btoa(result)
  }

  private fromBase64(str: string): Uint8Array {
    const decoded = atob(str)
    const bytes = new Uint8Array(decoded.length)
    for (let i = 0; i < decoded.length; i++) {
      bytes[i] = decoded.charCodeAt(i)
    }
    return bytes
  }
}

存储安全审计要点

  • [ ] 用户密码、Token 是否通过 Asset Store Kit 存储(不是明文 Preferences)

  • [ ] 数据库中敏感字段是否加密(不能明文存身份证号)

  • [ ] 加密密钥是否通过 Universal Keystore Kit 管理(不是硬编码在代码里)

  • [ ] 日志中是否打印了敏感数据(搜索 console.log 中的 password/token/phone)

  • [ ] 网络传输是否全站 HTTPS(不允许 HTTP 回退)

四、隐私合规检查

个人信息收集清单

按照《个人信息保护法》要求,应用收集个人信息必须告知用户并取得同意。上线前整理一份清单:

收集的数据

用途

存储位置

第三方是否共享

用户是否可关闭

手机号

账号注册

加密存本地+服务端

否

注销账号时删除

位置(大致)

附近服务推荐

仅内存,不持久化

否

关闭后功能降级

设备标识

设备绑定

Asset Store

否

不可关闭

剪贴板

快捷粘贴

不存储

否

仅粘贴时单次访问

隐私政策对齐

代码中实际收集的数据,必须和隐私政策文档描述一致。常见违规:

  1. 隐私政策说"不收集位置信息",但代码里有 geoLocationManager 调用

  2. 隐私政策说"不使用第三方 SDK",但实际集成了统计/推送 SDK

  3. 隐私政策没有提及设备标识收集,但代码用了 deviceInfo.deviceID

权限说明文案

module.json5 中每个权限的 reason 字段必须填写,且文案要具体:

{
  "name": "ohos.permission.APPROXIMATELY_LOCATION",
  "reason": "$string:permission_location_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "always"
  }
}
// resources/base/element/string.json
{
  "name": "permission_location_reason",
  "value": "用于显示您附近的商家和推荐服务"
}

五、第三方 SDK 审计

引入第三方 SDK 前需要检查:

检查项

要求

权限范围

SDK 申请的权限是否超出其功能需要

数据收集

SDK 是否在后台收集设备信息、位置等

网络通信

SDK 是否向非声明域名发送数据

加密传输

SDK 网络请求是否使用 HTTPS

隐私政策

SDK 是否有对应的隐私政策文档

混淆兼容

SDK 是否和应用的混淆规则冲突

SDK 审计方法

  1. 权限层面:集成 SDK 后,对比 module.json5 中 requestPermissions 的变化

  2. 网络层面:用 DevEco Studio 的 Network Profiler 观察应用启动时的网络请求

  3. 代码层面:反编译 SDK 的 HAR 包,检查是否有异常数据上报逻辑

六、代码混淆与反逆向

第 33 篇讲过混淆配置,这里补充审计要点:

// build-profile.json5 中的混淆规则
{
  "arkOptions": {
    "obfuscation": {
      "ruleOptions": {
        "enable": true,
        "files": ["obfuscation-rules.txt"]
      }
    }
  }
}
# obfuscation-rules.txt

# 保留对外 API
-keep
./src/main/ets/api/

# 保留资源名
-keep-resource-name

# 名称混淆
-enable-name-obfuscation

# 属性名混淆
-enable-property-obfuscation

# 字面量混淆
-enable-literal-obfuscation

# 顶层声明混淆
-enable-toplevel-obfuscation

# 保留异常类名(便于线上 Crash 定位)
-keep-class-name
BusinessError

审计要点:

  • [ ] obfuscation 是否开启(默认关闭!)

  • [ ] 对外 API 是否正确保留(-keep)

  • [ ] 是否保留异常类名(否则 Crash 日志无法定位)

  • [ ] 反调试是否启用(第 33 篇)

七、安全审计流程清单

把上面的内容汇总成一份可执行的审计清单,按以下顺序逐项检查:

第 1 步:权限审计

  • [ ] 扫描代码中所有 requestPermissionsFromUser 调用

  • [ ] 确认没有使用被禁的 9 类权限

  • [ ] 确认权限申请有明确触发时机,不是启动时批量申请

  • [ ] 确认权限被拒绝后有降级方案

第 2 步:数据审计

  • [ ] 扫描 Preferences/RDB 存储的内容,确认敏感数据已加密

  • [ ] 确认密钥通过 Keystore 管理,非硬编码

  • [ ] 扫描日志输出,确认不打印敏感信息

第 3 步:网络审计

  • [ ] 确认所有网络请求使用 HTTPS

  • [ ] 确认有证书校验逻辑

  • [ ] 确认 API 请求有防重放机制(nonce + timestamp)

第 4 步:隐私合规

  • [ ] 个人信息收集清单与隐私政策一致

  • [ ] 权限说明文案具体且准确

  • [ ] 第三方 SDK 有对应隐私政策

第 5 步:混淆加固

  • [ ] 代码混淆已开启

  • [ ] 对外 API 正确保留

  • [ ] 异常类名保留

第 6 步:第三方 SDK

  • [ ] 每个 SDK 有安全评估报告

  • [ ] SDK 权限不超出功能范围

  • [ ] SDK 网络请求已审查

小结

安全审计的本质是在上线前回答一个问题:你的应用收集了什么数据、怎么存的、谁能拿到、用户知不知道。

这份清单不是一次性的——每次发版都应该过一遍。尤其是新增权限、新增 SDK、新增数据收集时,对应的审计项必须更新。

星盾架构提供了系统层的安全能力,但开发者层的安全意识才是第一道防线。系统帮你拦住了不该有的权限申请,但如果你自己把用户 Token 明文存在 Preferences 里,系统也帮不了你。


评论