图像加上时间文字之后,看起来像有了来源证明,但文字可以重新绘制。内容溯源需要验证与图像关联的可信材料;EXIF 和可见水印只能提供普通元数据与展示信息。

API 26 SDK 的 mediaAuthVerify 提供签名存在性检查、签名验证和元数据解析接口。验证端依次完成文件读取、签名检查、验签和元数据解释。可信拍摄与签名生成属于另一段链路,必须另行确认安全相机的开放接口和设备支持,不能用普通拍照结果冒充可信原件。

原件与展示副本分开保存

用户导入图片后,先保存获得授权的原始字节,并记录应用自己的文件标识。缩略图、裁切图和压缩分享图分别生成新文件,不覆盖原件。

原件的文件哈希用于判断实验前后字节是否一致,但哈希本身不证明来源可信。签名是否覆盖特定内容、哪些变换被允许,应以实际溯源格式和验证规则为准。

文件选择器返回的访问权限也需要遵守生命周期。后台验证前确认文件仍可读取,失去授权时提示重新选择,不把文件读取失败误判为签名损坏。

验证结果至少分成四类

没有签名、验证通过、验证失败和能力无法完成,是不同结果。以下为应用层状态,系统返回值由适配层转换。

type ProvenanceState = 'absent' | 'valid' | 'invalid' | 'unavailable';
interface ProvenanceResult {
  fileId: string;
  state: ProvenanceState;
  checkedAt: number;
  reasonCode?: string;
}

“没有签名”不等于图片虚假;“验证失败”也需要区分格式损坏、材料不可信或其他原因。界面可以显示详细检查阶段,但不能把技术错误直接翻译成对拍摄者的指控。

SDK 声明中的 hasImageSignature 判断签名是否存在,verifyImageSignature 返回验签结果材料,parseImageMetadata 解析其中的元数据。实际参数结构、错误码及返回含义应按配套文档实现,不能只依据函数名字猜测全部语义。

解析材料后仍要限制展示

元数据可能包含设备、时间、编辑过程或其他字段。展示层只取业务需要且用户可理解的信息;未知字段作为不支持处理,不将任意文本直接插入可执行页面。

时间戳应说明含义,是拍摄声明、签名生成还是本次验证时间。把验证时间显示成拍摄时间,会制造错误的来源印象。地理信息若存在,也应遵循用户授权和最小展示原则。

验证通过证明的是受验证材料在相应规则下成立,不能自动证明画面中的事件确实发生,也不直接判定版权归属。产品文案可以写“来源材料验证通过”,避免笼统的“绝对真实”。

用受控变换观察签名边界

实验需要一张来源明确、可合法使用的签名原件和一张无签名样本。对签名原件分别做复制、重新编码、裁切与元数据处理,保留每个输出文件和操作记录。

不同格式对变换的处理可能不同,预期结果应依据规范设置。不能先假设所有编辑都必须失败,再把实际支持的派生链判定为漏洞。若支持新的签名或派生声明,应单独验证它们的信任关系。

同一文件至少重复验证一次,确认界面不会复用上一张图片的结果。用户快速切换图片时,通过 fileId 和任务代次丢弃旧回调,避免 A 图的“通过”显示在 B 图上。

将拍摄链路留作明确的接入项

完整 Demo 应包含合法签名样本导入、真实验证调用、结果解释、派生文件对照及取消处理。若还要演示安全相机拍摄,必须增加真实签名生成链路和支持设备,不能只增加一个拍照按钮。

验收材料保留原件与派生件哈希、系统版本、验证阶段及脱敏结果。原图不具备分发权时,只交付获取方式和测试说明。文章的截图展示结果状态,样本本身则按授权范围保存。

导入验证与可信拍摄分成两次交付

阶段输入完成条件
验证端合法签名原件与无签名样本各状态可解释,切图不串结果
变换对照原件与受控派生文件字节和操作可追溯,结果按格式解释
拍摄端支持设备的真实生成链路拍摄产物可由验证端核对

先做验证端可以尽早发现样本与格式问题,直接同时接拍摄和验证则更难区分故障来源。测试时不把修改后的文件覆盖原件;并发任务结束后核对 fileId,再更新页面。取得签名样本是首个前提,随意加水印的图片不进入“签名通过”正向用例。

参考:华为开发者能力介绍;API 26 SDK 的 mediaAuthVerify 类型声明用于核对方法存在性。安全相机具体代次、签名生成接口与支持设备仍需在接入时确认。

文件句柄、验证结果与元数据的归属

通过系统文件选择器读取用户选择的 JPEG 原件,限制大小为 16 MiB,拒绝空文件及明显不符的文件头。读取结束总会关闭文件,不修改原件,也不先压缩再做签名验证。

先检查是否带签名,存在签名才执行验证,成功后解析元数据。无签名、签名无效、格式错误和平台不支持分别走不同结果或异常路径,不能统一显示为图片被篡改。

export class MediaVerifyProbe {
  async choose(context: common.UIAbilityContext): Promise<string> {
    const selection: string[] = await new picker.DocumentViewPicker(context).select({
      maxSelectNumber: 1, fileSuffixFilters: ['JPEG|.jpg,.jpeg']
    });
    if (selection.length === 0) { return '用户取消选择'; }
    const file: fileIo.File = await fileIo.open(selection[0], fileIo.OpenMode.READ_ONLY);
    try {
      const stat: fileIo.Stat = await fileIo.stat(file.fd);
      if (stat.size <= 0 || stat.size > 16 * 1024 * 1024) { throw new Error('JPEG must be 1 byte..16 MiB'); }
      const data: ArrayBuffer = new ArrayBuffer(stat.size);
      const read: number = await fileIo.read(file.fd, data);
      if (read !== stat.size) { throw new Error('incomplete read'); }
      return this.verify(new Uint8Array(data));
    } finally { await fileIo.close(file); }
  }
  async verify(bytes: Uint8Array): Promise<string> {
    if (bytes.length < 3 || bytes[0] !== 255 || bytes[1] !== 216 || bytes[2] !== 255) { throw new Error('JPEG signature missing'); }
    const request: mediaAuthVerify.ImageAuthData = { buffer: bytes, imageSize: bytes.byteLength,
      bufferType: mediaAuthVerify.BufferType.BUFFER_TYPE_DATA,
      imageFormat: mediaAuthVerify.ImageFormat.IMAGE_TYPE_JPEG };
    if (!await mediaAuthVerify.hasImageSignature(request)) { return '无签名:不能判断原件是否被篡改'; }
    const manifests: Uint8Array = await mediaAuthVerify.verifyImageSignature(request);
    const metadata: string = await mediaAuthVerify.parseImageMetadata(manifests);
    return '签名验证接口成功;元数据长度 ' + metadata.length;
  }
}

用无签名与验签失败路径核对源码

合法带签名原件、原件与修改件对照;可信相机拍摄和签名生成仍需对应设备。

检查层当前处理不能替代的结果
编译API 26 工程进行类型检查与打包目标设备支持
逻辑校验输入、状态或资源释放边界平台服务真实返回
联调按上面的输入和条件逐步执行不能以按钮文案代替核心结果

mediaAuthVerify 的参数、结果与错误码约束,以 HarmonyOS 官方文档中心和当前 SDK 声明为准。

无签名和验签失败使用不同路径

无签名时停止后续验证;有签名却验证失败时保留异常。两种结果不能统一显示为被篡改,元数据解析只在验证成功后发生。宿主测试直接载入 MediaVerifyProbe,以替身代替文件与安全 Kit;load 与 test 属于测试框架。

test('media: unsigned result never reaches verify or metadata', async () => {
    let calls = 0;
    const C = load('services/MediaVerifyProbe', {
        '@kit.CoreFileKit': {}, '@kit.AbilityKit': {}, '@kit.DeviceSecurityKit': {
            mediaAuthVerify: {
                BufferType: {
                    BUFFER_TYPE_DATA: 0
                }, ImageFormat: {
                    IMAGE_TYPE_JPEG: 0
                }, hasImageSignature: async () => false, verifyImageSignature: async () => calls++
            }
        }
    }).MediaVerifyProbe;
    assert.match(await new C().verify(new Uint8Array([255, 216, 255])), /无签名/);
    assert.equal(calls, 0);
});
test('media: validation failure is propagated, not converted to unsigned', async () => {
    const C = load('services/MediaVerifyProbe', {
        '@kit.CoreFileKit': {}, '@kit.AbilityKit': {}, '@kit.DeviceSecurityKit': {
            mediaAuthVerify: {
                BufferType: {
                    BUFFER_TYPE_DATA: 0
                }, ImageFormat: {
                    IMAGE_TYPE_JPEG: 0
                }, hasImageSignature: async () => true, verifyImageSignature: async () => {
                    throw Error('bad hash');
                }
            }
        }
    }).MediaVerifyProbe;
    await assert.rejects(new C().verify(new Uint8Array([255, 216, 255])), /bad hash/);
});
操作或边界应检查的结果
输入不满足前提不调用后续平台操作
平台拒绝或抛错保留错误,不生成成功状态
再次进入或重试检查旧状态不会污染新任务
Logo

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

更多推荐