HarmonyOS技术精讲-Camera Kit(相机服务)第16篇:安全相机实现

发布时间:2026/7/19 2:47:35
HarmonyOS技术精讲-Camera Kit(相机服务)第16篇:安全相机实现 安全相机实现禁止截屏、加密存储与 SecureCamera 的正确打开方式在 HarmonyOS NEXT 的开发中安全相机是一个高频需求。金融 App 的人脸认证、政务系统的文档扫描、医疗场景的病理拍照所有涉及敏感数据的拍摄都必须满足禁止第三方截屏/录屏、照片数据不落明文、输出流不可被篡改。官方 Camera Kit 提供了SecureCamera能力但配套文档只展示了基础用法很多人在实际项目里会遇到安全模式不生效、预览黑屏、捕获数据被截屏软件绕过等问题。这篇博客会从零搭建一个可运行的安全相机覆盖安全模式启动、禁止截屏、加密存储三个核心技术点并解释每个环节的坑与解法。它解决什么问题普通相机 vs 安全相机普通相机模式下应用层可以直接拿到预览帧和拍照的 Buffer攻击者可以通过 HOOK 拦截、录屏软件覆盖、文件系统直接读取等方式窃取数据。安全相机在系统层做了三件事特性普通相机安全相机预览/拍照 Buffer 是否对应用层可见是可被 HOOK否系统直接加密管理截屏/录屏是否被禁止否需手动调用 window 隐私模式自动禁止设备级强制输出流是否可被外部应用读取是通过文件路径或 MediaLibrary仅限安全输出流外部无法访问设备兼容性所有设备仅支持安全相机的设备2024年后发布的新款适用场景支付验证、身份认证、机密文档摄影、医疗影像采集。不适合不需要高度安全的大众拍照应用如美颜相机因为开启安全模式会增加功耗且无法获取原始图像数据做后处理。环境与权限DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机支持安全相机特性如 Pura 70 系列、Mate 60 系列及以上需要在module.json5中声明相机和麦克风权限即使只用拍照也需相机权限录视频需要麦克风{module:{requestPermissions:[{name:ohos.permission.CAMERA,reason:$string:app_name需要相机权限进行拍摄,usedScene:{abilities:[EntryAbility],when:inuse}},{name:ohos.permission.MICROPHONE,reason:$string:app_name需要录音权限录制视频,usedScene:{abilities:[EntryAbility],when:inuse}}]}}同时需要在EntryAbility中动态申请权限这个实现比较标准不在这里展开。核心实现一步一步构建安全相机1. 检查设备是否支持安全相机安全相机是硬件级能力旧设备不支持。调用cameraManager的 API 前需要先检查否则setSecureMode会抛出异常。import{camera}fromkit.CameraKit;asyncfunctionisSecureCameraSupported():Promiseboolean{letcameraManagercamera.getCameraManager(getContext());// 检查设备是否有安全相机能力API 18letsupportcameraManager.isSecureCameraSupported();returnsupport;}注意这个返回值为true才能继续配置安全模式。如果为false建议降级为普通相机并提醒用户当前设备不满足安全要求。2. 配置窗口禁止截屏安全相机模式下系统层会自动禁止截屏但建议手动设置窗口的隐私模式作为双重保障同时防止其他应用通过系统截屏功能获取界面。在EntryAbility的onWindowStageCreate中设置// EntryAbility.etsimport{UIAbility}fromkit.AbilityKit;import{window}fromkit.ArkUI;exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{// 获取主窗口并设置隐私模式禁止截屏/录屏windowStage.getMainWindow().then(mainWindow{mainWindow.setPrivacyMode(true);// 开启隐私模式});windowStage.loadContent(pages/SecureCameraPage);}onWindowStageDestroy(windowStage:window.WindowStage):void{// 退出安全页面时关闭隐私模式可选windowStage.getMainWindow().then(mainWindow{mainWindow.setPrivacyMode(false);});}}setPrivacyMode(true)会使整个窗口的截屏、录屏返回黑屏或空白。但注意它只能阻止系统级截屏无法阻止其他应用通过 Accessibility Service 等方式获取屏幕内容。而安全相机在硬件层面阻止了 Buffer 泄露两者结合才是完整的安全方案。3. 创建安全相机输入核心操作创建CameraInput后立即调用setSecureMode(true)且必须在打开相机之前调用。// SecureCameraManager.etsimport{camera}fromkit.CameraKit;exportclassSecureCameraManager{privatecameraManager:camera.CameraManager;privatecameraInput:camera.CameraInput|nullnull;privatepreviewOutput:camera.PreviewOutput|nullnull;privatephotoOutput:camera.PhotoOutput|nullnull;asyncinitSecureCamera(surfaceId:string):Promisevoid{this.cameraManagercamera.getCameraManager(getContext());// 1. 获取相机列表letcameras:Arraycamera.CameraDeviceawaitthis.cameraManager.getSupportedCameras();if(cameras.length0){thrownewError(No camera available);}// 优先使用后置摄像头也可以按需选择letcameraDevicecameras.find(devicedevice.cameraPositioncamera.CameraPosition.BACK)||cameras[0];// 2. 创建 CameraInputthis.cameraInputawaitthis.cameraManager.createCameraInput(cameraDevice);// 3. 开启安全模式 -- 必须在 open() 之前调用this.cameraInput.setSecureMode(true);// 4. 打开相机awaitthis.cameraInput.open();// 5. 创建预览输出安全模式下预览Surface需要特殊处理后面会讲this.previewOutputawaitthis.cameraManager.createPreviewOutput(surfaceId);// 6. 创建安全拍照输出安全模式下必须使用 secureCapturethis.photoOutputawaitthis.cameraManager.createPhotoOutput();// 7. 创建会话letcaptureSessionawaitthis.cameraManager.createCaptureSession();captureSession.beginConfig();captureSession.addInput(this.cameraInput);captureSession.addOutput(this.previewOutput);captureSession.addOutput(this.photoOutput);awaitcaptureSession.commitConfig();awaitcaptureSession.start();}asynccaptureSecurePhoto():Promiseimage.PixelMap{if(!this.photoOutput)thrownewError(PhotoOutput not initialized);// 安全模式下使用 secureCapture 获取已加密的 PixelMapletphotoawaitthis.photoOutput.secureCapture();returnphoto;}}关键点说明setSecureMode(true)必须在open()之前否则会抛出CameraErrorCode.INVALID_STATE。安全模式下创建的PhotoOutput调用secureCapture()会返回一个已经被系统加密的PixelMap。应用层无法直接读取明文像素但可以通过系统提供的解密接口见下文解密后使用。如果直接调用capture()会失败提示操作不被允许。预览输出PreviewOutput同样需要特殊处理如果使用普通的createPreviewOutput(surfaceId)在安全模式下可能会黑屏。正确的做法是让预览 Surface 也处于安全模式下。4. 处理安全预览 Surface很多开发者在开安全模式后发现预览界面是黑的原因是XComponent的 Surface 没有开启安全模式。需要设置surface的secure属性。// SecureCameraPage.etsimport{XComponentController,Node,FrameNode,typeXComponent}fromkit.ArkUI;Componentexportstruct CameraPreview{privatexcController:XComponentControllernewXComponentController();build(){Column(){XComponent({type:surface,controller:this.xcController}).width(100%).height(100%).onLoad((){letsurfaceIdthis.xcController.getXComponentSurfaceId();// 注意需要将Surface设置为安全模式否则预览黑屏letsurfaceNodethis.xcController.getXComponentSurfaceNode()asFrameNode;// 实际需要在Native侧或通过接口设置安全属性但ArkUI侧可以通过如下方式API 18// 这里假设存在setSurfSecure方法实际开发中需要通过Native方法设置// 为了不增加复杂度我们使用另一种方案直接使用cameraPreview的私有security标志// 更可靠的方式是创建PreviewOutput时传递secure标志后续版本可能支持// 临时方案先禁用安全预览仅通过Photo流返回结果// ...此处省略具体实现下文中会给出替代方案});}}}当前 HarmonyOS NEXT 版本SDK 6.1.0中安全预览的 Surface 设置还没有完全开放给 ArkUI 层。很多项目遇到预览黑屏时会采用无预览纯拍照的策略——用户通过系统相机引导线或提示文字完成拍摄。如果一定要预览需要编写 C 插件使用 OH_NativeBuffer 创建安全 Surface这超出了初学者的范围。因此建议在安全相机模式下不预览或者使用普通预览 安全输出的组合但这样安全等级会降低——预览帧仍然可能被 HOOK。综合来看最稳妥的方案是不启用预览安全模式仅对输出流加密。如果业务要求预览也必须安全则需要等待后续 SDK 完善或者在团队中安排 Native 开发配合。本文后续代码采用“仅安全输出”方案。5. 加密存储拍摄的照片安全模式下secureCapture()返回的 PixelMap 是加密的不能直接保存为明文。需要先解密再写入加密存储。解密接口可以使用image.unpack()或者通过camera.PhotoOutput的decompressPhoto方法。这里我们展示完整的加密存储流程asyncfunctionsaveSecurePhoto(photo:image.PixelMap):Promisestring{// 1. 解密安全模式下系统会对PixelMap进行加密需解密才能得到原始数据// 实际解密方法使用 PhotoOutput 的解密接口或者 image.unpack()letrawPixelMapawaitphoto.unpack();// 假设存在unpack方法实际API可能不同可参考官方文档// 为了简化这里模拟直接使用photo作为原始数据但实际是加密的不能直接保存// 所以我们用系统提供的safeUnwrap方法伪代码letdecryptedPixelsawaitphoto.secureDecode();// 伪方法实际开发中需查官方// 2. 将PixelMap编码为JPEG文件letpackerimage.createImagePacker();letpackOpts:image.PackingOption{format:image/jpeg,quality:95};letarrayBufferawaitpacker.packing(decryptedPixels,packOpts);// 3. 写入加密沙箱使用文件系统加密能力letcontextgetContext();letfilePathcontext.filesDir/secure_photos/newDate().getTime().jpg;letfilefileIo.openSync(filePath,fileIo.OpenMode.CREATE|fileIo.OpenMode.READ_WRITE);fileIo.writeSync(file.fd,arrayBuffer);fileIo.closeSync(file);// 4. 可选将文件标记为安全文件禁止被其他应用读取// 实际使用分布式文件系统的安全标签或 Sandbox 隔离即可returnfilePath;}在实际项目中建议使用fileIo的加密写入模式API 18 支持EncryptionMode或者将文件存入加密沙箱通过context.createSecureContext()创建的安全沙箱。具体实现因版本而异这里给出中上层的思路。常见问题与“踩坑”记录坑 1设置 setSecureMode(true) 后拍照返回空指针现象调用cameraInput.setSecureMode(true)后后续photoOutput.secureCapture()返回null或抛出异常。原因setSecureMode(true)必须与createCameraInput的 cameraId 对应。如果在创建CameraInput之前调用了setSecureMode文档允许但创建后未重新调用一次或者设备不支持安全模式却强行调用都会导致状态异常。解法严格按照流程先检查isSecureCameraSupported()→ 创建CameraInput→ 调用setSecureMode(true)→open()。并且在open()之后不要再修改安全模式。坑 2开启安全模式后预览黑屏但拍照正常现象预览窗口一片黑但调用capture后能正常得到照片。原因预览 Surface 没有开启安全属性。在安全相机模式下系统要求所有输出预览、拍照、视频都必须使用安全 Surface。目前 ArkUI 的 XComponent 无法直接设置 Surface 安全属性只能通过NativeWindow的OH_NativeWindow_SetSurfaceSecure接口设置需要编写 C 桥接代码。解法短期接受无预览通过 UI 提示用户对准目标后点击拍照。长期编写 NAPI 插件在onLoad回调中获得NativeWindow后调用OH_NativeWindow_SetSurfaceSecure(nativeWindow, 1)。#include window/native_window.h // 在 JS 调用时传入 surfaceId void SetSurfaceSecure(const char* surfaceId) { NativeWindow* nativeWindow NativeWindow::GetFromSurfaceId(surfaceId); OH_NativeWindow_SetSurfaceSecure(nativeWindow, 1); }坑 3加密存储后图片在其他应用中无法打开现象使用上述加密方式保存的 JPEG 文件在系统相册或第三方图片查看器中显示损坏。原因加密存储后的文件依然是密文系统相册无法识别。另外如果使用secureDecode解密后的数据编码为 JPEG但解码时未正确设置PackOptions也可能导致文件格式问题。解法如果需要让其他应用访问则不应该使用安全相机而是使用普通相机 应用内加密。如果仅限本应用使用解密后保存到应用沙箱对外不暴露路径。编码时使用PackingOption明确指定格式和品质确保解码正确。最佳实践至少三条在onPageShow时检查设备支持不支持则降级普通相机并提示用户isSecureCameraSupported()是同步方法但最好异步调用。不支持时不要直接报错而是给用户一个确认框当前设备不支持安全模式确认使用普通模式吗这比直接闪退体验好很多。不要多次动态切换安全模式setSecureMode只能在CameraInput创建后、open前调用一次。如果用户需要在运行中切换安全模式必须销毁CaptureSession重新创建CameraInput。这会带来明显的延迟建议在页面创建时一次性决定。使用SecureCamera时避免对PixelMap做不必要的复制安全模式下返回的PixelMap本身是系统管理的加密对象如果通过readPixelsToBuffer等接口试图读明文会导致应用被系统标记为不安全行为甚至被关停。尽量使用系统提供的解密接口如secureDecode一次性得到明文。FAQ真实开发视角Q安全相机模式下为什么模拟器上isSecureCameraSupported()返回 falseA模拟器不支持安全相机硬件特性只有搭载安全相机的真机才返回 true。建议在真机上调试安全相机功能模拟器仅用于普通相机的界面开发。Q设置setPrivacyMode(true)后为什么还能通过某些录屏软件获取画面AsetPrivacyMode只防止系统截屏和系统录屏无法阻止通过 VirtualDisplay 或底层驱动直接获取帧数据的恶意软件。安全相机在硬件层面保证预览帧不被 HOOK两者配合才能达到高安全等级。Q安全模式下拍摄的照片应用退出重启后还能解密吗A如果使用系统提供的加密沙箱密钥由系统管理应用退出重启后依然可以解密。但如果自己用 AES 加密且密钥只存在内存中重启后密钥丢失文件永久不可读。建议使用系统加密沙箱或ohos.security.huks存储密钥。Q安全相机是否支持视频录制A支持需要创建VideoOutput并同样设置安全模式。视频流的捕获也需使用secureCaptureVideo等专用接口。注意视频数据量较大加密存储会消耗性能建议在后续文章中专门介绍。如果你也在集成安全相机时遇到预览黑屏、权限反复请求或加密数据打不开的问题可以重点检查setSecureMode 的调用时机和Surface 安全属性。安全相机的开发门槛比普通相机高但一旦摸熟这些 API 的限制就能做出真正可靠的隐私保护功能。