HarmonyOS 7 新特性实战(29):JPEG 原件签名验证与元数据解析
图像加上时间文字之后,看起来像有了来源证明,但文字可以重新绘制。内容溯源需要验证与图像关联的可信材料;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/);
});
| 操作或边界 | 应检查的结果 |
|---|---|
| 输入不满足前提 | 不调用后续平台操作 |
| 平台拒绝或抛错 | 保留错误,不生成成功状态 |
| 再次进入或重试 | 检查旧状态不会污染新任务 |
更多推荐


所有评论(0)