
HarmonyOS 私密相册实战:一张照片移入保险箱后,怎样让普通相册真的看不见
真机环境:ALN-AL80,OpenHarmony 7.0.0.32(Beta2),API 26。验证覆盖移入保险箱、系统身份认证、二级密码解锁、普通相册隔离和恢复原相册。
私密相册不只是多一个带锁页面
很多私密相册 Demo 只做了一个密码页:输入正确就跳到另一组图片。真正接入相册后,风险往往出现在入口之外------首页缩略图、搜索结果、地图回忆、最近照片和视频选片都可能继续显示同一条记录;应用切到后台时,任务缩略图还可能保留私密画面。
双镜记忆相机的目标是把一张公开照片移入保险箱后,普通相册立刻不再展示;进入保险箱必须经过系统身份认证或本地二级密码;离开保险箱再次锁定;恢复照片后,它才重新回到普通相册。
本次使用一张虚构成年旅行者公开测试图,完整走一遍"公开---私密---解锁---隐藏---恢复---重新出现"的状态变化。
先划清实现的安全边界
User Authentication Kit 官方能力页明确支持锁屏密码、人脸和指纹认证,并把访问隐私相册列为用户操作授权场景;认证凭据由系统安全隔离环境存储和处理。官方同时允许生物认证不方便时切换到业务自定义认证方式。
二级密码使用 HarmonyOS SDK 中的 Asset Store Kit 保存。Asset Store Kit 面向密码、Token 等短敏感数据,适合保存应用自己的 6 位解锁凭据。
当前实现的边界分为四层:
- 相册记录的
visibility决定公开或私密,所有公开入口统一过滤; - User Authentication Kit 负责系统人脸、指纹和锁屏密码认证;
- Asset Store Kit 保存应用二级密码,作为可控的备用入口;
- 私密照片详情开启窗口隐私模式,离开保险箱立即锁定。
照片文件位于应用沙箱,本文验证的是应用内可见性、访问授权和窗口保护。若业务需要独立文件加密、密钥轮换或跨设备密文同步,还要在文件层增加专门的加密方案。
第一步:从真实照片详情移入保险箱
操作前,人物照片位于普通相册详情,页面可以直接看到"移入保险箱"入口。
移动操作不复制第二份数据库记录,而是更新同一条记录的可见性并持久化:
ts
private async moveRecordToVault(recordId: string): Promise<void> {
const wasViewingDetail = this.galleryViewMode === 'detail';
await this.updateRecordVisibility(recordId, 'private');
this.vaultSelectedId = recordId;
this.vaultUnlocked = false;
if (wasViewingDetail) {
this.galleryViewMode = 'album';
this.switchTab('vault');
this.vaultStatusText = '照片已移入保险箱,验证身份后可查看';
}
}
updateRecordVisibility() 同步更新相册记录、地图标记和持久化数据。状态写入成功后才关闭详情,避免页面先消失而写盘失败。

图 1:真机提示照片已经移入保险箱,当前页面仍处于锁定状态。
这一步完成了数据归类,没有自动解锁保险箱。即使操作来自照片详情,查看私密内容仍需要重新验证身份。
第二步:优先调用系统身份认证
系统认证同时请求 PIN、人脸和指纹三种本地认证类型,并为本次认证生成 16 字节随机 challenge。不同设备支持的安全等级可能不同,因此从 ATL3 依次回退到 ATL2、ATL1;只有返回"不支持当前等级"或"不支持认证类型"时才继续回退,普通认证失败不会被当作降级理由。
ts
const authParam: userAuth.AuthParam = {
challenge: createRandomChallenge(),
authType: [
userAuth.UserAuthType.PIN,
userAuth.UserAuthType.FACE,
userAuth.UserAuthType.FINGERPRINT
],
authTrustLevel: userAuth.AuthTrustLevel.ATL3
};
const instance = userAuth.getUserAuthInstance(
authParam,
{ title: '验证身份' }
);
instance.on('result', callback);
instance.start();
真机固定在测试支架上,前置摄像头没有正对已录入人脸,本次系统认证明确返回"未识别成功"。系统面板同时给出指纹和锁屏密码出口。

图 2:真机 User Authentication Kit 返回人脸未识别成功,并提供指纹和锁屏密码入口。
认证失败不会打开保险箱。页面保留锁定状态,用户可以重试系统认证,也可以使用应用的二级密码。
第三步:用 Asset Store Kit 保存二级密码
首次使用二级密码时,页面要求输入并确认 6 位数字。截图中的输入内容已经由密码框隐藏。

图 3:系统认证失败后进入二级密码设置,密码内容不显示在页面和文章中。
保存时不使用 Preferences 明文写入。Asset Store 条目设置为设备解锁后可访问、要求设备已经设置锁屏密码、禁止同步到其他设备:
ts
const attributes: asset.AssetMap = new Map();
attributes.set(asset.Tag.SECRET, toBytes(pin));
attributes.set(asset.Tag.ALIAS, toBytes('vault.pin.v1'));
attributes.set(asset.Tag.ACCESSIBILITY, asset.Accessibility.DEVICE_UNLOCKED);
attributes.set(asset.Tag.REQUIRE_PASSWORD_SET, true);
attributes.set(asset.Tag.SYNC_TYPE, asset.SyncType.NEVER);
attributes.set(
asset.Tag.CONFLICT_RESOLUTION,
asset.ConflictResolution.OVERWRITE
);
await asset.add(attributes);
验证时按别名查询唯一条目,并使用固定时间比较,避免在第一位不同时过早返回:
ts
function constantTimeEqual(left: Uint8Array, right: Uint8Array): boolean {
let difference = left.length ^ right.length;
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index++) {
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
}
return difference === 0;
}
二级密码保存成功后,保险箱在当前会话中解锁,刚刚移入的照片作为唯一私密记录出现。

图 4:真机保险箱已经解锁,私密照片数量为 1。
第四步:所有普通入口统一过滤私密记录
相册首页不能只在 UI 层隐藏某一张卡片。双镜记忆相机先从记录集合生成公开视图,再把这份集合交给相册、语义搜索、地图、最近照片和视频选片。
ts
const publicRecords = records.filter(
record => record.visibility !== 'private'
);
const vaultRecords = records.filter(
record => record.visibility === 'private'
);
语义搜索返回图片路径后还会再次查找公开记录。这样即使系统索引尚未完成删除,私密照片也不会绕过当前业务权限重新出现在结果页。
从保险箱返回普通相册后,测试照片已经从公开记录集合中消失,相册计数同步减少一张。
页面离开保险箱时还会执行 lockVault(),清空当前选择并关闭私密详情。再次进入必须重新认证,不能依赖上一次解锁状态长期驻留。
第五步:窗口隐私模式保护任务缩略图
打开私密照片详情时,应用为当前窗口打开隐私模式:
ts
const currentWindow = await window.getLastWindow(context);
await currentWindow.setWindowPrivacyMode(true);
关闭私密详情时恢复普通窗口状态;离开保险箱则同时关闭详情并重新锁定。窗口隐私模式负责系统截屏和任务缩略图层面的保护,业务记录过滤负责应用内部入口,两者解决的是不同问题,不能互相替代。
第六步:恢复照片并确认重新出现
在已解锁保险箱中点击"恢复到相册",同一条记录的 visibility 改回 public。写入成功后,页面提示"已恢复到相册"。

图 5:恢复操作已经完成,保险箱中的私密记录被移回公开集合。
返回相册后,人物照片重新进入公开记录集合,相册数量也恢复到移动前的值。至此,公开---私密---公开的状态闭环完成。
真机过程里最值得保留的两个细节
系统认证失败也必须有完整路径
固定支架导致人脸识别失败,正好验证了异常路径:系统面板没有被应用绕过,保险箱保持锁定;用户仍可选择指纹、锁屏密码或应用二级密码。真实产品不能只验证"人脸一次成功"的理想情况。
"看不见"要覆盖所有数据消费者
只修改相册首页会留下搜索、地图、最近照片和成片选片等旁路。把公开/私密过滤放到记录层,所有功能域得到的都是已经过权限判断的集合;搜索结果还要在跳转详情前重新检查可见性。
最终效果
这次 ALN-AL80 真机验证完成了完整状态闭环:公开照片进入保险箱;保险箱保持锁定;系统人脸认证失败且没有放行;二级密码安全保存并成功解锁;私密照片在保险箱可见;普通记录集合不再包含这张照片;执行恢复操作后,照片重新回到公开记录集合。
User Authentication Kit 提供可信的系统认证界面,Asset Store Kit 保存应用备用凭据,记录层控制全部业务入口的可见性,窗口隐私模式保护私密照片详情。四层组合以后,保险箱不再只是一个带密码的页面,而是从数据访问到系统窗口都能保持一致状态的完整功能。