【万能转换器|18】HarmonyOS ArkTS 权限与隐私实战:让 module.json5、功能说明和拒绝路径一致
本轮证据边界: 当前源码的
requestPermissions为空,未发现网络、位置、相机或麦克风能力调用;汇率使用静态表,检查到的剪贴板路径只写不读。与此同时,工程注册了EntryBackupAbility,且allowToBackupRestore为true,现有隐私文案没有解释系统备份恢复边界。本轮没有构建、安装、抓取运行时权限弹窗或核验 AGC 软件包。由于当前应用不申请运行时权限,文中的"拒绝路径"仅是未来新增受限能力时的建议模板,不代表现有页面已经实现权限拒绝交互。
权限合规最容易出现一种"每个文件单独看都没问题,合起来却互相打架"的状态:module.json5 没有声明敏感权限,产品文案写着完全离线,页面确实没有联网入口,但本地数据、系统剪贴板、备份恢复、反馈联系方式和清除能力又各自在不同模块中演进。审核真正核验的不是某一句承诺,而是声明、调用、界面、失败路径和上架资料能否组成同一个事实。
"万能转换器"的当前源码非常适合做一次反向审计。entry/src/main/module.json5 的 requestPermissions 是空数组;ArkTS 源码没有导入网络请求、Web、定位、相机或麦克风能力;oh-package.json5 没有生产依赖;历史记录、工具收藏、主题、字号和默认单位都写入 Preferences;汇率说明明确使用内置参考表;复制结果只向系统剪贴板写文本,没有读取剪贴板。就这些证据而言,"离线、免登录、无运行时权限申请"是可以复核的。
但源码也暴露出几处不能略过的真实边界。backup_config.json 把 allowToBackupRestore 设为 true,工程还注册了 EntryBackupAbility,而隐私文本只写"仅保存在设备本地,不上传至任何服务器",没有解释系统备份恢复边界;反馈邮箱和著作权人仍是显式占位值;清除历史记录不会同步清除独立的工具收藏和设置偏好;剪贴板写入失败时只记录日志,没有给用户失败反馈。本文不会把这些问题写成已经修复,更不会推断已上架安装包一定与当前源码快照相同,而是把它们作为发布前必须核对的差异。
本文面向 HarmonyOS 5.0 及以上版本,基于 module.json5、backup_config.json、EntryBackupAbility.ets、BrandConfig.ets、MinePage.ets、HistoryRepository.ets、FavoriteToolsService.ets、SettingsService.ets 和剪贴板调用代码,建立一套可复核的权限与隐私闭环。
唯一标记:
AGC18-HMOS-18-18-PERMISSION-PRIVACY-MANIFEST-DENIAL

一、权限审计要从事实矩阵开始
不要先写隐私政策,再去代码里寻找能够支持它的证据。更稳妥的顺序是先列出应用真实处理的数据和系统能力:
| 场景 | 当前实现 | 是否声明运行时权限 | 数据去向 | 当前失败路径 |
|---|---|---|---|---|
| 单位与计算换算 | 纯本地算法 | 否 | 内存 | 输入校验 |
| 汇率换算 | 内置参考汇率表 | 否 | 内存 | 不涉及网络失败 |
| 历史记录 | Preferences | 否 | 应用本地存储 | 初始化/写入异常记录日志 |
| 工具收藏 | Preferences | 否 | 应用本地存储 | 初始化失败记录日志 |
| 主题、字号、默认单位 | Preferences | 否 | 应用本地存储 | 写入失败记录日志 |
| 复制结果/邮箱 | 写系统剪贴板 | 否 | 系统剪贴板 | 成功 Toast,失败仅日志 |
| 备份恢复 | BackupExtensionAbility | 否 | 需按系统备份策略确认 | 当前回调只记录日志 |
这张表的价值是把"权限"和"隐私"拆开。没有运行时权限,不等于没有数据处理;数据只在本地,也不等于不需要说明保存内容、清除入口和备份边界。
二、module.json5 的空权限清单是可验证事实
当前模块配置为:
json5
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": ["phone"],
"requestPermissions": []
}
}
requestPermissions: [] 表示当前模块没有在配置层声明应用运行所需权限。华为文档说明,Stage 模型的 module.json5 会向编译工具、操作系统和应用市场提供模块、组件和权限信息,因此它不是普通注释,而是审核证据的一部分。
空数组只能证明"配置里没声明",还需要继续搜索调用面。本文对 entry/src/main/ets 的 Kit 导入和敏感 API 进行了复核,没有发现 requestPermissionsFromUser()、atManager、HTTP、WebSocket、Web、相机、位置、麦克风、通讯录或用户文件选择调用。
三、空权限不是免审通行证
权限最小化的核心不是把清单写空,而是功能不暗中依赖未声明能力。一次可靠检查至少包括三层:
text
Manifest:requestPermissions 声明了什么
Source:实际导入和调用了什么 Kit/API
Runtime:核心流程是否触发系统授权、跨应用访问或网络
如果只看 Manifest,可能漏掉三方 SDK 的初始化、动态代码路径或系统能力间接调用;如果只看源码,又可能漏掉构建产物合并后的权限。当前项目没有生产依赖,这降低了第三方 SDK 自动采集和自动加权权限的风险,但发布前仍应对实际 APP/HAP 产物做权限扫描,而不是把 oh-package.json5 空依赖当作最终结论。
四、离线声明有四组源码证据
BrandConfig.ets 的产品描述和 MinePage.ets 的隐私文本都把应用定义为完全离线。这个结论在当前源码中有四组支撑:
module.json5没有ohos.permission.INTERNET。- ArkTS 文件没有 HTTP、WebSocket、Web 组件或网络 Kit 导入。
oh-package.json5的生产依赖为空。- 汇率页使用应用内置参考汇率,不宣称实时联网价格。
这里必须保留"当前源码快照"这一限定。正式审核的是上传包,包内代码、权限、SDK 与 AGC 隐私标签才是最终事实。如果发布分支后来增加联网反馈、统计、广告或实时汇率,就必须同步改 Manifest、隐私政策、SDK 清单、数据用途和拒绝路径。
五、本地数据也要逐项披露
源码中至少有三份 Preferences:
text
app_history / items
app_favorites / favoriteList
app_settings / theme, fontScale, defaultUnits
HistoryRepository 把最多 200 条历史记录序列化为 JSON 字符串;FavoriteToolsService 保存收藏工具 key;SettingsService 保存主题、字号和默认单位。隐私文本已经覆盖"历史记录、工具收藏、主题、字号、默认单位",这点与实现基本一致。
不过"本地数据"仍需回答四个问题:为什么保存、保存多久、如何清除、是否参与系统备份。只写"不收集个人信息"无法替代这些说明,因为用户的计算记录即使不构成身份信息,也仍是用户产生的数据和应用状态。
六、Preferences 的本地边界要说准确
HistoryRepository 的写入过程是:
ts
const trimmed = items.slice(0, MAX_ITEMS);
await store.put(KEY_ITEMS, serializeHistory(trimmed));
await store.flush();
这说明数据持久化发生在应用的 Preferences 存储中,且历史数量有 200 条上限。适合对用户表述为"历史记录保存在应用本地,用于展示最近换算结果",不宜扩张为"任何情况下都永不离开当前设备",因为工程同时开启了备份恢复能力。
同理,Preferences 不是敏感数据保险箱。当前保存的是工具 key、主题和普通计算结果,不包含密码、令牌等关键资产;若以后加入账号凭据、支付标识或健康数据,就不能沿用同一套明文偏好存储与简单隐私描述。
七、备份恢复是当前最大的声明差异
backup_config.json 的实际内容为:
json
{
"allowToBackupRestore": true
}
module.json5 还注册了:
json5
{
"name": "EntryBackupAbility",
"type": "backup",
"exported": false,
"metadata": [{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}]
}
这意味着项目主动保留了系统备份恢复扩展点。当前 onBackup() 和 onRestore() 只记录日志,没有显式枚举文件或上传服务器;仅凭这两个空回调,不能武断地声称哪些 Preferences 一定被备份,也不能忽略 allowToBackupRestore: true。正确做法是根据目标 SDK、系统备份策略和实机行为确认实际范围,再二选一:
- 产品不需要迁移:关闭备份恢复,并验证升级、卸载和换机场景。
- 产品需要迁移:保留能力,在隐私文本中准确说明由系统提供的备份/恢复范围、触发条件和用户控制方式。
八、"仅保存在设备本地"需要补充边界词
当前隐私文本写着:
text
以下数据仅保存在您的设备本地,不上传至任何服务器。
这句话与应用自身不发网络请求的事实一致,但与"允许系统备份恢复"的配置之间存在解释空间。更稳妥的文案不是刻意模糊,而是先决定产品策略。
如果关闭备份,可写"数据保存在应用本地,应用自身不上传或同步"。如果保留系统备份,可写"应用自身不向开发者服务器上传;若用户启用系统备份恢复,相关应用数据可能按系统机制迁移",并以实测结果约束"相关数据"的范围。
关键是区分"应用开发者的服务器上传"和"操作系统提供的数据迁移",不能用一句"完全不上传"覆盖两个不同机制。

九、系统剪贴板写入不应被误写成读取
应用在 ConvertPage、CapitalForm、RadixForm 和 MinePage 中使用 pasteboard,但调用模式都是:
ts
const data = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN,
text,
);
pasteboard.getSystemPasteboard().setData(data);
源码没有读取剪贴板内容,也没有声明 ohos.permission.READ_PASTEBOARD。因此隐私清单应描述"在用户点击复制时,将当前结果写入系统剪贴板",不应因为看到 pasteboard 就误写成"读取剪贴板",更不能虚构剪贴板采集。
HarmonyOS 的单次授权文档把 READ_PASTEBOARD 列为支持"允许本次使用"的敏感权限之一,但那针对读取场景。当前项目只写入,不能把另一个 API 的规则机械套过来。
十、复制失败路径目前只有日志
copyResults() 在成功后显示"已复制到剪贴板",但 Promise 失败时只执行:
ts
.catch((err: Error) => {
console.error(`[ConvertPage] copy failed: ${err.message}`);
});
这不属于权限拒绝崩溃,却属于能力失败路径不完整。用户点击后没有成功 Toast,也没有失败提示,会误以为内容已经复制。建议把写入封装为统一服务,返回明确结果:
ts
// 建议方案,非当前源码
export interface CopyResult {
ok: boolean;
reason?: string;
}
async function copyPlainText(text: string): Promise<CopyResult> {
try {
const data = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN,
text,
);
await pasteboard.getSystemPasteboard().setData(data);
return { ok: true };
} catch (error) {
return { ok: false, reason: (error as Error).message };
}
}
页面根据 ok 显示"已复制"或"复制失败,请重试",日志只承担诊断职责。
十一、当前没有敏感权限,也就没有真实拒绝弹窗
本文标题中的"拒绝路径"不能伪造成已经存在。当前源码不申请相机、位置、麦克风等敏感权限,所以运行时不会出现这类系统授权拒绝。当前最好的拒绝策略恰恰是:无需权限的核心换算功能不弹权限框、不因拒绝退出、不要求用户先同意无关能力。
如果未来新增扫码识别、拍照取数或位置单位推荐,拒绝路径才需要进入产品设计。华为 UX 隐私规范强调,电话、通讯录、定位、短信、录音、相机、存储、日历等权限被拒绝后,应用不应退出或关闭。用户再次触发相关功能时,可以说明影响并提供前往设置的明确入口。
十二、未来新增权限应采用功能触发式申请
一个可维护的权限状态机应至少区分:
ts
// 建议模型,非当前源码
export type PermissionUiState =
| 'idle'
| 'requesting'
| 'granted'
| 'denied'
| 'blocked'
| 'unavailable';
申请顺序应是:
text
用户点击需要权限的功能
→ 检查当前授权状态
→ 调用系统授权接口
→ 允许:进入功能
→ 拒绝:留在原页,展示无权限降级状态
→ 再次触发:说明影响,并按平台规范提供设置入口
不要在启动时批量申请尚未使用的权限,也不要在系统权限弹窗前叠加一个强制自定义弹窗。当前应用没有敏感权限,因此保持零申请比搭建一套空洞的首次启动同意流程更符合最小化原则。
十三、隐私政策入口与首次运行提示不是同一件事
MinePage 在"关于"分组中提供"隐私政策"和"用户协议"入口,用户可在应用内查看,这是公开透明的一部分。隐私政策应是独立、可重复访问的文本,而不是用户点过一次后永远找不到。
是否需要首次运行同意,要看应用是否在启动阶段处理需征得同意的个人信息或初始化会采集数据的 SDK。当前源码没有登录、广告、分析、网络 SDK和敏感权限,不能为了"看起来合规"而添加一个不允许拒绝、不同意就无法使用离线换算的强制弹窗。
一旦引入会处理个人信息的 SDK,则必须在初始化之前完成必要告知与同意,并提供拒绝后的可用范围,而不是先初始化、后展示隐私文本。
十四、反馈联系方式占位是发布阻断项
当前 BrandConfig.ets 明确写着:
ts
export const BRAND_FEEDBACK_EMAIL =
'your-real-email@example.com';
export const BRAND_COPYRIGHT_OWNER =
'开发者名称';
EntryAbility.onCreate() 会通过 listPlaceholderFields() 检查并输出警告,这是很好的开发期防线,但日志警告不会阻止发布。隐私政策最后一节让用户通过"反馈与建议"联系开发者,页面又把占位邮箱复制到剪贴板;如果发布包确实包含该值,联系方式将不可用。
需要强调:队列记录的版本状态不能证明当前源码快照就是市场安装包。这里能确认的只有"当前被审计源码仍有占位值"。发布前应将占位检查升级为构建门禁,并核对 AGC 主体、应用内著作权人与隐私政策主体一致。
十五、清除历史不等于清除全部本地数据
设置页的"清除缓存"实际只调用:
ts
HistoryService.get().clearAll()
HistoryRepository.clear() 删除 app_history/items。工具收藏存放在独立的 app_favorites/favoriteList,主题、字号和默认单位存放在 app_settings。因此按钮文案"清除缓存"容易让用户理解成清除全部应用数据,但当前行为只是清除历史记录。
产品应二选一:
- 把文案改为"清除历史记录",准确描述现有行为。
- 增加"清除全部本地数据",明确列出历史、收藏和设置,二次确认后逐项删除并重建默认状态。
隐私政策中的数据主体控制,最终要落到这些可操作入口,而不是只写一句"用户可以删除"。
十六、删除能力需要可验证的完成语义
清除动作已有确认框和成功 Toast,但仍应验证三个层次:
text
持久层:对应 Preferences key 是否删除并 flush
状态层:AppStorage/页面列表是否立即更新
重启层:杀进程重启后数据是否仍为空
如果做"全部清除",不要只清空 UI 数组。建议由一个 LocalDataService 编排现有服务:
ts
// 建议方案,非当前源码
async clearAllLocalData(): Promise<void> {
await HistoryService.get().clearAll();
await FavoriteToolsService.get().clear();
await SettingsService.get().resetToDefaults();
}
其中 resetToDefaults() 当前并不存在,必须真实实现主题、字号、默认单位的清理和 AppStorage 回写后才能写进产品说明。
十七、AGC 隐私标签必须与包内事实同源
发布前至少要同时核对:
| 审核面 | 当前源码证据 | AGC 应填写的事实 |
|---|---|---|
| 网络 | 无 INTERNET、无网络调用 | 不提供网络服务,不虚构实时汇率 |
| 账号 | 无登录和认证依赖 | 无账号体系 |
| 权限 | requestPermissions: [] |
不声明未使用敏感权限 |
| SDK | 无生产依赖 | 不虚构第三方 SDK,也不漏报后来新增项 |
| 本地数据 | 三份 Preferences | 说明历史、收藏与偏好用途 |
| 备份 | backup 扩展已开启 | 先实测再决定披露或关闭 |
| 联系方式 | 当前仍为占位 | 发布包必须替换为真实可联系信息 |
最危险的不是"填少了",而是 AGC 写"不收集、不处理、不备份",包内却存在另一条链路。反过来,为了保险而勾选大量不存在的数据采集,同样会造成产品能力与声明不一致。
十八、自动化检查应拦住权限漂移
可以在发布前做一组轻量扫描:
powershell
rg -n "requestPermissions|ohos.permission" entry/src/main
rg -n "requestPermissionsFromUser|atManager" entry/src/main/ets
rg -n "http|fetch|Web\\(|socket" entry/src/main/ets
rg -n "your-real-email|example.com|开发者名称" entry/src/main
这些命令不能替代人工审计,但能防止"新增 API 忘记更新 Manifest"和"占位字段混进发布包"。更完整的门禁还应解析 JSON5 与依赖树,避免简单文本搜索误报注释或漏掉构建合并结果。
建议把检查结果保存为发布证据:
text
权限声明数
敏感 API 调用点
生产依赖与 SDK 清单
网络入口扫描
占位字段扫描
备份恢复配置
隐私政策入口截图
拒绝/失败路径截图
十九、真机验收要覆盖拒绝、失败和卸载
当前应用没有敏感权限,因此验收重点不是伪造"拒绝相机"场景,而是验证无权限核心流程:
- 首次安装启动,无权限弹窗、无登录阻断。
- 断网状态完成单位、生活和工程计算。
- 汇率页明确是参考汇率,不出现实时更新承诺。
- 写入历史后重启,记录仍在且不超过设计上限。
- 收藏、主题、字号和默认单位分别持久化。
- 点击复制,成功有 Toast;模拟失败时用户可见。
- 隐私政策和用户协议可在应用内重复打开。
- 清除历史后重启,历史保持为空,收藏和设置行为符合文案。
- 实测系统备份恢复,确认哪些 Preferences 被迁移。
- 卸载后重装,验证应用数据行为与系统策略一致。
如果未来增加敏感权限,再把"首次拒绝、再次拒绝、仅本次允许、设置中撤销、后台恢复、永久拒绝、能力不可用"加入矩阵。
二十、最小收口顺序与结论
基于当前源码,权限与隐私不需要大改架构,最小收口顺序很清楚:
- 保留
requestPermissions: [],继续维持离线核心流程不申请敏感权限。 - 对实际构建产物扫描权限、网络入口和生产依赖。
- 决定是否需要系统备份恢复;不需要就关闭,需要就实测并准确披露。
- 把"清除缓存"改成"清除历史记录",或实现真正的全部本地数据清除。
- 为剪贴板写入失败增加用户可见反馈。
- 将反馈邮箱和著作权人占位升级为发布构建阻断。
- 核对应用内隐私文本、AGC 隐私标签、SDK 清单和实际包行为。
- 保存断网、清除、重启、备份恢复和卸载重装的验收证据。
当前项目的优点是边界足够克制:无网络、无账号、无生产 SDK、无敏感权限,数据层也集中在三个 Preferences 服务里。真正需要警惕的不是复杂权限系统,而是几处小差异逐渐破坏可信度:备份配置没有进入隐私说明、清除文案覆盖范围过大、复制失败静默、联系信息仍是占位。
权限合规不是"弹一个隐私框",而是一条从功能目的、Manifest、API 调用、数据存储、拒绝或失败路径、删除能力到 AGC 声明的连续合同。合同中的每一句话,都应该能在源码、运行结果或发布后台找到对应证据。
官方资料可继续参考华为开发者的 应用配置文件概述、应用隐私保护、选择和同意、向用户申请单次授权 与 应用市场审核政策。具体权限等级、授权结果、备份范围和系统设置跳转能力,应以项目目标 SDK 与对应设备系统版本的官方文档和实测结果为准。

**AI 辅助声明:**本文在人工核对真实 HarmonyOS 模块配置、备份配置、隐私文案、本地存储和剪贴板调用后,使用 AI 辅助整理结构、润色表达并生成配图;权限状态、隐私差异和建议实现均按文中证据边界区分。
CSDN-SERIES:ALL-163250154