权限声明写得对,系统授权弹框也能弹出来,仍然不等于敏感能力的调用时机正确。一个常见风险是:应用冷启动后,页面还没完成隐私提示,某个自动初始化模块已经访问了相册、位置或麦克风。开发日志里只看到“初始化成功”,提审前的检查表也只核对了 module.json5,真正关键的先后关系没有证据。

这类问题不能靠一句“代码里已经加判断”收尾。需要把用户同意动作、业务调用意图和系统侧敏感权限使用记录放到同一条时间线上。HarmonyOS 的 PrivacyManagerService 提供基于 hidumper 的敏感权限使用记录查询,HiLog 则适合记录应用自己的状态标记。两者合在一起,可以做一条提审前的时序门禁。

本文用 ConsentTraceLab 演示。任务为 PRV-1942-611,检查时间 19:42,演示 Token ID 为 536994611,关注权限 ohos.permission.READ_IMAGEVIDEO。初次冷启动回放共检查 25 个节点,完成 17 个时进度为 68%;系统记录的敏感访问时间是 19:42:15.204,应用同意标记是 19:42:16.880,提前 1676ms,状态因此为 REVIEW_BLOCKED。这些数据服务于文章和配图对账,不是实际审核结论,也不代表某个真实应用或真实用户。

一、先确定我们能证明什么

官方说明中,PrivacyManagerService 可以按应用进程的 Token ID 获取敏感权限使用记录,输出包含 bundleName、permissionName、lastAccessTime、lastAccessDuration 和 accessCount。命令形态是:

hidumper -s PrivacyManagerService -a '-t <tokenId>'

这份记录很有价值,但它不是完整调用栈,也不是无限保留的事件流水。特别是 lastAccessTime,表达的是记录中最近一次访问时间,不能单独证明某个函数就是根因。应用自己的同意标记同样不是系统授权证据,它只说明业务状态走到了哪个节点。

所以这套门禁的结论必须克制:当干净冷启动、固定操作脚本和同一轮采样条件成立时,如果敏感权限最近访问时间早于业务同意标记,就判定这轮回放存在时序风险;反过来,即使没有命中,也不代表应用在所有路径上都合规。

另一个容易混淆的点是“隐私政策同意”和“系统权限授权”。两者不是同一个布尔值。应用可以先展示隐私说明,再在用户真正触发某项功能时请求系统权限;也可能用户过去已经授权系统权限,但本次业务仍需遵循自己的告知与开关状态。代码里应该分别记录 privacyConsent、systemPermission 和 featureIntent。

二、冷启动标记要放在真正的边界上

为了让时间线可读,EntryAbility 只记录进程和页面阶段,具体功能页记录用户动作。不要在日志中写用户姓名、照片路径或隐私文案全文。门禁只需要稳定的任务号、代次、状态和毫秒时间。

下面这段 ArkTS 代码解决“同意状态和系统权限状态被压成一个变量”的问题。requestMediaAccess() 只有在用户明确触发导入后才调用;自动初始化不得调用它。日志参数用公开标记,敏感值保持私有或不输出。

import { hilog } from '@kit.PerformanceAnalysisKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { Permissions } from '@kit.AbilityKit';

const DOMAIN = 0x1942;
const TAG = 'ConsentTrace';
const MEDIA_PERMISSION: Permissions = 'ohos.permission.READ_IMAGEVIDEO';

class ConsentGate {
  private privacyConsent: boolean = false;
  private generation: number = 611;

  acceptPrivacy(): void {
    this.privacyConsent = true;
    hilog.info(DOMAIN, TAG,
      'task=PRV-1942-611 event=CONSENT_ACCEPTED generation=611');
  }

  async requestMediaAccess(context: common.UIAbilityContext): Promise<boolean> {
    hilog.info(DOMAIN, TAG,
      'task=PRV-1942-611 event=FEATURE_INTENT feature=MEDIA_IMPORT');
    if (!this.privacyConsent) {
      hilog.warn(DOMAIN, TAG, 'event=BLOCKED_BY_PRIVACY_GATE');
      return false;
    }
    const manager = abilityAccessCtrl.createAtManager();
    const result = await manager.requestPermissionsFromUser(
      context, [MEDIA_PERMISSION]
    );
    const granted = result.authResults[0] === 0;
    hilog.info(DOMAIN, TAG,
      `event=SYSTEM_PERMISSION_RESULT granted=${granted}`);
    return granted;
  }
}

这段代码只表达门禁关系,不声称覆盖所有权限结果。实际项目要按当前 API 返回码处理“拒绝”“已禁止再次询问”等状态,并在每次敏感操作前重新核验权限。页面销毁后也不要持有失效的 UIAbilityContext;需要请求时再从有效页面或 Ability 上下文进入。

三、系统记录与业务日志不能直接混着比较

系统记录常用时间戳,HiLog 有自己的输出时间。先统一时区和毫秒单位,再比较同一轮冷启动。若设备时钟、主机时钟和导出文件时间不一致,脚本会制造假阳性。

演示目录拆成:

  • entryability/EntryAbility.ets:写入冷启动代次;
  • pages/ConsentAuditPage.ets:显示同意、权限与功能意图;
  • service/ConsentGate.ets:承载顺序门禁;
  • tools/permission-timeline.ts:解析两类记录;
  • evidence/PRV-1942-611/:保存本轮脱敏输入与摘要。

应用日志固定包含:

[19:42:14.118] task=PRV-1942-611 event=COLD_START generation=611

[19:42:16.880] task=PRV-1942-611 event=CONSENT_ACCEPTED generation=611

系统演示记录转换后为:

[19:42:15.204] permission=ohos.permission.READ_IMAGEVIDEO accessCount=3

DevEco Studio 配图展示的正是这组数据:右侧模拟器显示 REVIEW_BLOCKED,底部日志用红色细箭头标出系统访问早于同意标记。该图是演示配图,不冒充真实 IDE 或真机证据。

四、解析脚本先拒绝模糊数据

hidumper 输出可能带服务标题和 JSON 主体。工具脚本应先定位 JSON,再验证字段类型。解析失败不能当作“没有敏感访问”,否则采集故障会被误判为通过。

下面这段 TypeScript 解决“空输出默认放行”和“秒、毫秒混用”的问题。它只提取本次关注权限,要求 lastAccessTime 是有限数字,并把证据不足单独标记为 EVIDENCE_INVALID。

interface PermissionRecord {
  bundleName: string;
  permissionName: string;
  lastAccessTime: number;
  lastAccessDuration: number;
  accessCount: number;
}

type AuditState = 'ORDER_OK' | 'PRE_CONSENT_ACCESS' | 'EVIDENCE_INVALID';

function parsePermissionDump(raw: string): PermissionRecord[] {
  const start = raw.indexOf('{');
  const end = raw.lastIndexOf('}');
  if (start < 0 || end <= start) throw new Error('DUMP_JSON_NOT_FOUND');
  const value = JSON.parse(raw.slice(start, end + 1)) as {
    permissionRecord?: PermissionRecord[]
  };
  if (!Array.isArray(value.permissionRecord)) {
    throw new Error('PERMISSION_RECORD_MISSING');
  }
  return value.permissionRecord.filter(item =>
    typeof item.permissionName === 'string' &&
    Number.isFinite(item.lastAccessTime) &&
    Number.isInteger(item.accessCount));
}

function auditOrder(accessMs: number, consentMs: number): AuditState {
  if (!Number.isFinite(accessMs) || !Number.isFinite(consentMs)) {
    return 'EVIDENCE_INVALID';
  }
  return accessMs < consentMs ? 'PRE_CONSENT_ACCESS' : 'ORDER_OK';
}

脚本不能随便修正时间戳。如果输入看起来像秒而另一边是毫秒,应让采集阶段显式声明单位,而不是用数值位数猜。更不能在没有记录时返回 ORDER_OK。无证据与顺序正确是两种状态,提审门禁里应分别处理。

五、先看到阻断,再讨论修复

手机运行页把三层状态并列显示:隐私同意 PENDING、系统敏感访问记录 3 次、最近访问 19:42:15.204、同意标记 19:42:16.880,差值 -1676ms。25 个审计节点完成 17 个,进度 68%,最终 REVIEW_BLOCKED。

看到阻断后,不要第一反应去改门限。这里的目标不是“让检查通过”,而是找到自动访问的触发点。常见来源包括页面预加载、媒体索引预热、三方 SDK 提前初始化,或者恢复上次任务时直接打开数据源。

本文的演示根因是 MediaWarmup 在首页 aboutToAppear 中调用了媒体访问。修复不是延迟 2 秒,而是把预热拆成不触碰敏感数据的内存准备,以及用户同意且主动进入导入页后的数据访问。时间延迟不能建立业务因果,低端设备或不同启动速度下仍会反转。

六、修复要用新的代次回放

同一进程里直接再点一次,旧的 lastAccessTime 和 accessCount 可能干扰判断。演示规定每轮回放使用新代次,并记录采样前置条件。真实项目可以采用清晰的测试设备状态、固定脚本和独立证据目录,但不要宣称一次通过就覆盖所有入口。

下面这段脚本解决“只比较两个时间却没有绑定同一任务”的问题。它要求任务号和代次匹配,并输出可供 CI 或人工复核读取的摘要。示例中第二轮仍显示 17/25、68%,但关键顺序已经改为同意 19:43:10.120,敏感访问 19:43:11.406,状态 ORDER_OK。

interface TimelineInput {
  taskId: string;
  generation: number;
  consentMs: number;
  accessMs: number;
  permission: string;
  completed: number;
  total: number;
}

function buildVerdict(input: TimelineInput) {
  if (input.taskId !== 'PRV-1942-611' || input.generation !== 612) {
    throw new Error('EVIDENCE_GENERATION_MISMATCH');
  }
  const state = auditOrder(input.accessMs, input.consentMs);
  return {
    taskId: input.taskId,
    generation: input.generation,
    permission: input.permission,
    deltaMs: input.accessMs - input.consentMs,
    progress: Math.round(input.completed * 100 / input.total),
    state,
    reviewGate: state === 'ORDER_OK' ? 'READY_FOR_REVIEW' : 'REVIEW_BLOCKED'
  };
}

第二轮详情页不再展示第一轮的阻断卡片,而是展示代次 612 的事件线:CONSENT_ACCEPTED → FEATURE_INTENT → SYSTEM_PERMISSION_GRANTED → SENSITIVE_ACCESS。红圈标在 ORDER_OK,箭头说明“访问晚于同意 1286ms”。两张手机图承担不同任务,一张说明问题,一张说明修复后的证据链。

七、提审门禁应该保留哪些证据

最小证据包包括:候选包身份、任务号、冷启动代次、设备与系统版本、操作脚本版本、脱敏后的 HiLog 片段、PrivacyManagerService 原始输出摘要、解析脚本版本和最终判定。若原始输出包含不该进入共享材料的信息,应在本地保留受控原件,对外只提供脱敏摘要。

证据包还要说明能力边界。lastAccessTime 是最近访问记录,不是完整调用栈;accessCount 增加能提示行为发生,但不能告诉你是哪一行业务代码;HiLog 标记由应用写入,也不能替代系统授权状态。因此结论应写成“本轮固定回放未观察到同意前访问”,不要写“应用绝对不存在隐私风险”。

在持续集成里,这个门禁更适合作为需要人工确认的阻断项。采集失败、Token ID 不匹配、JSON 解析失败、时钟单位不明都应该阻断证据生成,而不是让流水线绿灯。审核资料的可信度,往往就差在这些“失败时默认通过”的小分支上。

八、这篇文章没有覆盖的内容

本文没有代替法律或合规判断,也没有列出某个市场当前全部审核规则。政策会变化,提交前仍需查看官方审核指南和应用隐私保护要求。

本文也没有把系统权限弹框当成隐私政策同意。二者目的、触发点和证据来源不同,实际产品要分别设计。

演示中的 Token ID、时间、访问次数和任务号均为配图数据契约,不对应真实用户。PrivacyManagerService 命令应在符合官方环境要求的调试设备上使用,并根据当前输出格式做解析。

最后,REVIEW_BLOCKED 和 READY_FOR_REVIEW 是本文工具的应用层状态,不是系统 API 返回值,也不是华为审核结论。它们的作用是提醒团队:在交付候选包之前,先把一条敏感访问的先后关系说清楚。

比起再做一张权限声明清单,这条时间线更接近问题本身。权限有没有写,只回答“能不能访问”;访问发生在用户同意之前还是之后,才回答“什么时候访问”。把这两个问题分开,提审前的自查才不会只停留在配置文件表面。

参考资料:

  • Huawei HarmonyOS:PrivacyManagerService(基于 hidumper 按 Token ID 查询敏感权限使用记录)
  • Huawei HarmonyOS:访问控制与用户授权权限开发概述
  • Huawei HarmonyOS:HiLog ArkTS API
  • HUAWEI AppGallery:应用审核指南与应用隐私保护说明
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐