前言

最近在做 openharmony Bundle Manager 相关的权限方案设计,工作中需要将原本只对系统应用开放的能力逐步开放给三方应用。在这个过程中,我接触到了 BM 命令工具的各个接口,也完成了一系列权限架构的改进。

一、背景

在 HarmonyOS 的早期设计中,很多 Bundle Manager 的能力只对系统应用开放。这是因为涉及到底层系统操作,需要保证系统安全。但是随着生态发展,越来越多的应用管理类三方应用(比如手机管家类应用)需要这些能力。

我的工作就是设计一套权限方案,在不影响系统安全的前提下,将部分能力开放给三方应用。

需要开放的能力

经过梳理,主要涉及以下几个方面:

能力 原有限制 开放目标
安装 debug 包 仅系统应用 允许三方应用安装调试包
卸载应用 仅系统应用 允许三方应用卸载普通应用
清理缓存/数据 仅系统应用 允许三方应用清理用户数据
查询应用信息 仅系统应用 允许三方应用获取应用信息

二、BM 命令工具概览

在做权限方案之前,我先把 BM 命令工具梳理了一遍。BM(Bundle Manager)是 HarmonyOS 中管理应用生命周期的核心工具,提供以下命令:

命令 功能 核心接口
install 安装 HAP/HSP 包 StreamInstall()
uninstall 卸载应用或模块 Uninstall()
dump 查询应用信息 DumpInfos()
clean 清理缓存/数据 CleanBundleCacheFiles() / CleanBundleDataFiles()
compile AOT 编译管理 CompileProcessAOT() / CompileReset()
quickfix 热修复补丁管理 ApplyQuickFix() / DeleteQuickFix()

三、权限方案设计思路

3.1 设计原则

在做这个权限方案时,我遵循了几个原则:

1. 安全第一

  • 不能影响系统应用的使用
  • 要保留原有的安全检查机制
  • 三方应用的能力要有明确边界

2. 向后兼容

  • 系统应用的行为不能有任何改变
  • 原有权限继续有效
  • 只是增加新的能力通道

3. 设计一致性

  • 所有接口的权限检查方式要保持一致
  • 便于后续维护和扩展

3.2 统一的权限模式

在实际工作中,我发现原来的代码在不同接口中的权限检查方式各不相同。这给维护带来了困难。所以我设计了一套统一的权限检查模式:

原有检查:
  ├─ 系统应用检查: IsSystemApp()
  └─ 权限检查: VerifyCallingPermissionForAll(原有权限)

新权限模式:
  ├─ 系统应用检查: IsSystemApp() || 新权限例外
  └─ 权限检查: 原有权限 || 新权限

这个模式的核心思想是:在系统应用检查和权限检查两个地方都添加新权限的例外

这样做的好处是:

  • 保持了原有的检查结构
  • 新旧权限可以并存
  • 不会影响原有逻辑

3.3 具体实现:INSTALL_DEBUG_BUNDLE

第一个做的是安装 debug 包的权限。这个需求比较特殊:三方应用有了这个权限后,只能安装 debug 包,不能安装 release 包

我在 BundleInstallChecker::VaildInstallPermission 中加了一个检查:

// 如果有 debug 安装权限,检查是否所有 hap 都是 debug 类型
if (installDebugBundleStatus == PermissionStatus::HAVE_PERMISSION_STATUS) {
    for (uint32_t i = 0; i < hapVerifyRes.size(); ++i) {
        Security::Verify::ProvisionInfo provisionInfo = hapVerifyRes[i].GetProvisionInfo();
        if (provisionInfo.type != Security::Verify::ProvisionType::DEBUG) {
            return false;  // 有非 debug 包,拒绝安装
        }
    }
    return true;  // 所有包都是 debug 类型,允许安装
}

这里的关键是:权限检查的时机。我把这个检查放在常规权限检查之前,这样就能确保有 debug 权限的应用只能安装 debug 包。

3.4 具体实现:PERMISSION_ACCESS_BUNDLE_MANAGER

对于卸载、清理、查询等能力,我设计了一个通用的 PERMISSION_ACCESS_BUNDLE_MANAGER 权限。

在实现时,我在两个地方添加了检查:

1. 系统应用检查处

if (!BundlePermissionMgr::IsSystemApp() &&
    !BundlePermissionMgr::VerifyCallingPermissionForAll(PERMISSION_ACCESS_BUNDLE_MANAGER)) {
    return ERR_BUNDLE_MANAGER_SYSTEM_API_DENIED;
}

2. 权限验证处

if (!原有权限检查 &&
    !BundlePermissionMgr::VerifyCallingPermissionForAll(PERMISSION_ACCESS_BUNDLE_MANAGER)) {
    return ERR_BUNDLE_MANAGER_PERMISSION_DENIED;
}

这样的设计保证了两点:

  • 持有新权限的三方应用可以通过系统应用检查
  • 持有新权限的三方应用可以通过权限验证

3.5 安全边界

在设计权限方案时,安全边界是最重要的。我利用了现有的几个检查机制:

1. IsRemovable() 检查

  • 系统应用无法被三方应用卸载
  • 不可移除的预装应用也无法被卸载

2. userDataClearable 检查

  • 只有标记为可清理的应用才能被清理数据
  • 系统关键应用的数据不会被误删

3. 强制卸载检查

  • 强制卸载仍然需要 EDC_UID
  • 三方应用无法强制卸载预装应用

这些现有机制为我提供了很好的安全保障,不需要重新设计。


四、各接口的权限改造

4.1 install 接口

安装接口的改造比较复杂,因为涉及到多个入口:

  • BundleInstallerHost::Install(vector 版本和单文件版本)
  • BundleInstallerHost::CreateStreamInstaller
  • BundleInstallerHost::DestoryBundleStreamInstaller
  • BundleStreamInstallerHostImpl::CreateStream

每个入口都要做两件事:

  1. 在系统应用检查中添加 INSTALL_DEBUG_BUNDLE 例外
  2. 在权限验证中添加 INSTALL_DEBUG_BUNDLE 选项

核心修改点:在 BundleInstallChecker::VaildInstallPermission 中增加 DEBUG 类型检查,确保只有 debug 权限的应用只能安装 debug 包。

4.2 uninstall 接口

卸载接口相对简单一些,主要修改三个重载版本的 Uninstall 方法。核心是在 VerifyUninstallPermission 中添加新权限检查。

注意事项:利用现有的 IsRemovable() 检查,确保系统应用和不可移除的预装应用不会被三方应用卸载。

4.3 clean 接口

清理接口有两个:CleanBundleCacheFilesCleanBundleDataFiles。它们的检查逻辑几乎一样,我采用了相同的改造模式。

注意事项:保留了 userDataClearable 检查,只有标记为可清理的应用才能被清理。

4.4 dump 接口

Dump 接口涉及多个方法:

  • DumpInfos:根据不同的 flag 调用不同的子方法
  • GetLabelByBundleName:获取单个应用的标签
  • GetAllBundleLabel:获取所有应用的标签

这些方法都有独立的权限检查,需要逐一改造。

额外修改:应要求,还额外添加了对 GetBundleInfo 方法的权限支持。


五、遇到的问题和解决方案

5.1 权限检查的顺序问题

在最开始做的时候,我把新权限检查放在了原有权限检查之后。结果发现有些场景下,原有权限检查会失败,导致新权限检查根本没有机会执行。

解决方案:把新权限检查提前,或者用 || 运算符让新旧权限平等。

5.2 权限定义的位置问题

一开始我把 PERMISSION_ACCESS_BUNDLE_MANAGER 定义在 Constants 中,但是后来发现这个权限是扩展权限,应该定义在 ServiceConstants 中。

解决方案:在 bundle_service_constants.h 中定义,保持与 INSTALL_DEBUG_BUNDLE 等扩展权限的一致性。

5.3 系统应用检查的误修改

在做 dump 接口改造时,我误修改了 CheckIsSystemAppByUid 方法。这个方法是其他地方调用的,不应该被修改。

解决方案:仔细审查每个修改点,确保只修改需要改的地方。


六、经验总结

6.1 设计模式的重要性

在做了几个接口的改造后,我发现了一个统一的模式。把这个模式总结出来,后续的改造就快多了。所以在做过几个类似改造后,一定要总结模式

6.2 安全机制的复用

我一开始想自己设计一套安全机制,后来发现现有代码中已经有很多安全检查了。善用现有机制比自己重新设计更安全

6.3 测试场景的覆盖

在做权限方案时,测试场景非常重要。我整理了一个测试场景矩阵:

调用者类型 是否系统应用 自调用 有新权限 结果
系统应用 - - ✅ 通过
非系统应用(自己) - ✅ 通过
非系统应用(有新权限) ✅ 通过
非系统应用(无权限) ❌ 拒绝