开发工具: 华为云码道

本文配套仓库: icapps/flutter-signed-json
鸿蒙适配后仓库https://atomgit.com/oh-flutter/flutter-signed-json

signed_json 用于在 Dart 侧校验并解密带签名 / 加密的远程配置 JSON:通过 JWS(ES512)做验签,通过 JWE(RSA1_5 + A128CBC-HS256)做解密,并支持先验签后解密的链式组合。本文以 signed_json 0.0.2 为例,介绍源码准备、OHOS 工程配置、example 演示与真机运行。

插件本身只在 Android 上有原生实现,方法通道 signed_json 也仅在 Platform.isAndroid == true 时被调用;OHOS 上没有新增任何 ArkTS 代码,平台走的是纯 Dart 分支(package:jose + compute()),因此不需要在仓库根目录创建 ohos/ 目录。key_hash 将鸿蒙的应用签名证书指纹读取能力封装为 Flutter 插件,应用可以通过 KeyHash.getKeyHash 获取当前应用的签名指纹。本文以 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

在这里插入图片描述

插件目前支持 ohos 平台,上游源码位于 GitHub 仓库。文中的代码以上游 main 分支为适配基线,OHOS 改动记录配套仓库https://atomgit.com/oh-flutter/flutter-signed-json已经包含了 example/ohos/ 工程。文中的代码以提交 4230677eac3908ae40e70d61500df6f03447dadd 为参考,建议的 OHOS 适配 tag 为 0.0.2-ohos-1.0.0-beta.1

在这里插入图片描述


KeyHash 示例页

序号测试项结果表现耗时状态
1verify:用 verify.jwk 公钥验签 remote_config.signed验签通过,返回 {“some_text”:“Something Something”}3312 ms通过
2decrypt:用 decrypt.jwk 私钥解密 remote_config.enc解密成功,返回 {“some_text”:“Something Something”}692 ms通过
3verifyAndDecrypt:先验签再解密的完整链路完整链路成功,返回 {“some_text”:“Something Something”}167 ms通过
4verify(篡改签名):签名最后一个字符被翻转,应验签失败按预期抛出异常:JoseException: Could not decrypt/verify payload106 ms按预期失败
5verify(错误证书):用不相关的 EC 公钥,应验签失败按预期抛出异常:JoseException: Could not decrypt/verify payload3 ms按预期失败
6decrypt(错误证书):用不相关的 RSA 私钥,应解密失败按预期抛出异常:JoseException: Could not decrypt/verify payload3 ms按预期失败

以下是操作的视屏,可以参考一下:

Example 启动授权


一、插件简介与适配目标

signed_json 把 JOSE(JWS + JWE)的能力封装为 Flutter 插件,业务层只关心“签名是否合法”与“能否解密出原文 JSON”,不需要自己引入 JOSE 实现。常见的使用方式是读取服务端下发的远端配置:服务端先用私钥签一份 JSON,再用公钥加密;客户端拿到 signedenc 两段密文后,本地公钥验签 + 私钥解密,最终得到可消费的 Map<String, dynamic>

调用模型是“一次调用返回结果”:Dart 侧发起 verify / decrypt / verifyAndDecrypt,原生侧读取自身签名材料或直接执行 JOSE 操作后,同步把 UTF-8 字符串(原始 JSON)返回给 Dart,再由 parseAndDecode<T> 解析成业务对象。不涉及持续监听或事件推送。

OHOS 适配目标有三个:

  1. 无需新增 ArkTS 原生代码:本库只在 Android 上有原生实现,方法通道 signed_jsonlib/src/base_signed_json.dartPlatform.isAndroid 分支控制;OHOS 上 useNativeSignedJsonfalse,自动走纯 Dart 分支。
  2. example 应用跑通六个场景:包括正向的 verify / decrypt / verifyAndDecrypt 以及三个预期失败的负向用例。
  3. example 工程结构正确:在 example/ohos/ 而不是在仓库根目录创建 OHOS 脚手架;pubspec.yamlflutter.plugin.platforms 不需要新增 ohos: 条目。

二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

flutter --version
flutter doctor -v
hdc list targets

在这里插入图片描述

在这里插入图片描述

编辑用户

工程使用的工具链和 SDK 配置如下:

项目版本或配置用途
Flutter OHOS SDK3.44.9+ohos-0.0.1-canary1Flutter 编译与 OHOS 平台工具链
Flutter 分支0.0.2-ohos-1.0.0CPF-Flutter 对应开发分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion工程未显式声明由 DevEco 工程默认值决定
targetSdkVersion工程未显式声明由 DevEco 工程默认值决定
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本0.0.2pubspec.yaml 中的包版本
原生语言ArkTS仅 example 模板使用,插件本身没有 OHOS 原生实现
插件产物HAR不适用:本库不在仓库根目录生成 ohos/

2.1 开发套件版本与工程中的 SDK 版本配置

7.0.0(API 26)5.1.0(18) 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:

  • 7.0.0(API 26) 表示本机 HarmonyOS 开发套件版本为 7.0.0,对应 API 26。
  • 5.1.0(18) 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 18。
  • compileSdkVersiontargetSdkVersion 未在本例的 example/ohos/build-profile.json5 中显式声明,编译时使用 DevEco 工程的默认值。

对应的 product 配置为:

{
  "name": "default",
  "compatibleSdkVersion": "5.1.0(18)",
  "runtimeOS": "HarmonyOS"
}

在这里插入图片描述

本机使用 API 26 的开发套件编译,工程声明最低兼容 API 18。安装后能否正确解密 JWE、校验 JWS,取决于 JWE 加密时使用的 RSA / AES 密钥与客户端持有的私钥 / 公钥是否匹配,与设备 API 级别无关。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到自己的工作仓库

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

signed_json 的上游仓库位于 GitHub,目前还没有 AtomGit 配套仓库。可以按上面的流程把 GitHub 上游导入 AtomGit 作为自己的工作仓库,也可以直接在 GitHub 上 Fork。下面使用 GitHub 上游地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://github.com/icapps/flutter-signed-json.git
cd flutter-signed-json
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

在这里插入图片描述

git clone 会创建 flutter-signed-json/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yamllib/example/config/。Git 仓库名是 flutter-signed-json,Dart 包名是 signed_json

本文的适配以 OHOS 适配前的上游提交为起点。需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

路径提示:本文实际仓库位于 /Users/david/workspace/flutter/lib/flutter-signed-json(即 ~/workspace/flutter/lib/ 之下),不是 ~/workspace/flutter/flutter-signed-json。这是该 workspace 下多个 Flutter 插件共用一个父目录的常见路径陷阱,克隆或拉取时需要按实际位置进入。

在这里插入图片描述

图 1:拉取 icapps/flutter-signed-json 并检查远程与当前提交。

3.3 在仓库根目录创建适配分支

接着在 flutter-signed-json/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yamlname,版本号取此次适配的基线版本。本例为:

git switch -c feat/ohos_signed_json_0.0.2
git branch --show-current

如果该分支已存在,使用 git switch feat/ohos_signed_json_0.0.2 切换即可。

建议在分支上补充 TAG,便于业务方按 tag 引用。建议的 tag 形如 0.0.2-ohos-1.0.0-beta.1(上游版本号 + -ohos- + 适配版本号 + 阶段标识)。上游仓库当前没有任何 OHOS 相关 tag,tag 需要在合并 / 发布前自行创建。

请添加图片描述

图 2:在 master 创建适配分支,并建议补充 TAG 0.0.2-ohos-1.0.0

3.4 在 example 中补全 OHOS 适配结构

signed_json 与常见的带原生代码的 Flutter 插件不同:插件本身没有 OHOS 原生实现pubspec.yamlflutter.plugin.platforms 只声明了 android / ios / web 三端,方法通道 signed_json 只在 Platform.isAndroid == true 时被调用;OHOS 走的是 package:jose + compute() 的纯 Dart 分支。因此:

  • 不需要在仓库根目录创建 ohos/ 目录。执行 flutter create --template=plugin --platforms=ohos . 会强行生成空的 HAR 脚手架,既不会带来任何 Dart 侧收益,也容易让下游业务方误以为有原生实现要对接。
  • 只需要在 example/ 中补全 OHOS 应用工程,让示例 App 能在 OHOS 上跑起来。

进入 example/ 后执行:

cd example
flutter create --platforms=ohos .
cd ..
  • --platforms=ohos 指定需要补全的平台。
  • 最后的 . 表示在当前 example/ 目录补全工程,不会另建一层目录。

补全后会生成 example/ohos/(包含 AppScope/entry/build-profile.json5 等)。生成后通过 git status 检查新增文件,保留示例原有 lib/main.dart 等,必要时再覆盖 demo。不同 Flutter OH 版本生成的模板可能略有差异。

请添加图片描述

图 3:在 example/ 目录执行 flutter create --platforms=ohos . 后新增的 example/ohos/ 目录。

3.5 适配后的项目目录

适配后的关键目录如下:

flutter-signed-json/
├── lib/
│   ├── signed_json.dart
│   ├── signed_json_web.dart
│   └── src/
│       ├── base_signed_json.dart
│       ├── signed_json.dart
│       ├── bridge/native_signed_json.dart
│       └── model/computer_args.dart
├── android/                     # 原生 Kotlin 实现
├── ios/                         # 原生 Swift 实现
├── web/                         # Web 端实现
├── example/
│   ├── lib/main.dart            # 演示 6 个场景
│   ├── tool/                    # 调试脚本(ohos_chain_test.dart 等)
│   └── ohos/                    # OHOS 应用工程(无原生插件)
│       ├── AppScope/
│       ├── entry/
│       └── build-profile.json5
├── config/
│   ├── keys/                    # verify.jwk / sign.jwk / decrypt.jwk / encrypt.jwk
│   ├── remote_config.json
│   ├── remote_config.signed     # JWS(ES512)
│   └── remote_config.enc        # JWE(RSA1_5 + A128CBC-HS256)
├── docs/
│   └── ohos-test-evidence/      # 真机运行证据
├── test/
└── pubspec.yaml

项目根目录如下,其中包含 lib/example/ohos/config/docs/ohos-test-evidence/

请添加图片描述

图 4:适配后的 flutter-signed-json 项目目录,包含 lib/example/ohos/config/example/tool/docs/ohos-test-evidence/

文件主要职责
lib/signed_json.dart入口文件,转发到 lib/src/signed_json.dart
lib/src/signed_json.dartSignedJson 类,公开 verify<T> / decrypt<T> / verifyAndDecrypt<T>
lib/src/base_signed_json.dartSignedJsonUtil,按 Platform.isAndroid 决定走原生还是纯 Dart
lib/src/bridge/native_signed_json.dartAndroid 原生 MethodChannel signed_json 的 Dart 封装
lib/src/model/computer_args.dartcompute() 跨 isolate 传递的参数模型
android/原生 Kotlin 实现(com.icapps.signedjson.SignedJsonPlugin
ios/原生 Swift 实现
web/Web 端实现
config/keys/仓库自带的 JWK 密钥对与样例配置
example/lib/main.dart6 个场景的演示(含耗时显示)
example/tool/ohos_chain_test.dart宿主端验证 + 生成 chainEncoded
example/ohos/OHOS 应用工程,无原生插件代码

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法、参数和返回值约定。先阅读 lib/signed_json.dartlib/src/signed_json.dartlib/src/base_signed_json.dart,确认平台分支逻辑;本库在 OHOS 上不需要新增任何 ArkTS 代码,原因是方法通道 signed_json 只在 Android 上有效,OHOS 自动走纯 Dart 分支。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
SignedJson.verify<T>signed_json / verify(仅 Android)OHOS 不走通道,调用 _verifyOnBackgroundThreadT 为泛型,parseAndDecode<T> 解析;useNativeSignedJson == false
SignedJson.decrypt<T>signed_json / decrypt(仅 Android)OHOS 不走通道,调用 _decryptOnBackgroundThreadT 为泛型;decryptionCert == null 时抛 ArgumentError
SignedJson.verifyAndDecrypt<T>signed_json / verifyAndDecrypt(仅 Android)OHOS 走 internalVerify + internalDecrypt 串联先 JWS 验签、解出 JWE 字符串,再 JWE 解密出原文 JSON
parseAndDecode<T>compute() 中的类型强转调用方必须显式传 <Map<String, dynamic>>,否则会出现类型转换异常

原生端需要保持通道名、方法名和参数一致(仅 Android)。系统能力不可用或密钥不匹配时,由 package:jose 抛出 JoseException

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间不需要任何通道:OHOS 走纯 Dart 分支,SignedJsonUtil.useNativeSignedJson 在 OHOS 上返回 false,整个 JWS / JWE 处理都通过 package:jose 在后台 isolate(compute())完成。

Android true

其他平台 false

Flutter 页面

SignedJson 对外 API

SignedJsonUtil

useNativeSignedJson?

MethodChannel signed_json

compute 后台 isolate

package:jose

parseAndDecode

OHOS 走右侧分支:compute()ComputerArgs 跨 isolate 传到 _verifyOnBackgroundThread / _decryptOnBackgroundThread,由 package:jose 完成 JWS / JWE 处理,返回 UTF-8 字符串后再用 parseAndDecode<T> 解析成业务对象。

4.1.1 一次 verifyAndDecrypt 调用的时序
package:jose compute() SignedJsonUtil SignedJson.verifyAndDecrypt<T> Flutter App package:jose compute() SignedJsonUtil SignedJson.verifyAndDecrypt<T> Flutter App verifyAndDecrypt<Map<String,dynamic>>(chainEncoded) verifyAndDecrypt<T>(vCert, dCert, chainEncoded) useNativeSignedJson == false _verifyOnBackgroundThread(ComputerArgs) JsonWebSignature.fromCompactSerialization payload bytes (zlib) JWE 字符串 _decryptOnBackgroundThread(ComputerArgs) JsonWebEncryption.fromCompactSerialization payload bytes (zlib) 原始 JSON 字符串 parseAndDecode<T>(json) T(Map<String, dynamic>)

链式调用是先验签、解出 JWE 字符串,再解密出原始 JSON。两步都通过 compute() 走后台 isolate,避免主 isolate 阻塞 UI。

4.2 公开 API 与平台接口

lib/src/signed_json.dart 中的 SignedJson 类是业务层唯一入口:

class SignedJson {
  final _signedJsonUtil = SignedJsonUtil();

  final String verificationCert;
  final String? decryptionCert;

  SignedJson(this.verificationCert, {this.decryptionCert});

  Future<T> verify<T>(String encoded) async => _signedJsonUtil.run(
      parseAndDecode,
      await _signedJsonUtil.internalVerify(verificationCert, encoded));

  Future<T> decrypt<T>(String encoded) async {
    final decryptionCert = this.decryptionCert;
    if (decryptionCert == null) {
      throw ArgumentError('Decryption key can not be null');
    }
    return _signedJsonUtil.run(parseAndDecode,
        await _signedJsonUtil.internalDecrypt(decryptionCert, encoded));
  }

  Future<T> verifyAndDecrypt<T>(String encoded) async {
    final decryptionCert = this.decryptionCert;
    if (decryptionCert == null) {
      throw ArgumentError('Decryption key can not be null');
    }
    return _signedJsonUtil.verifyAndDecrypt<T>(
        verificationCert, decryptionCert, encoded);
  }
}

调用方式和使用时机如下:

  • verifyCert / decryptionCert 在构造时一次性传入,建议使用 config/keys/ 下的真实 JWK;
  • 三个方法都是 Future<T> 形式,T 由调用方指定;上游示例默认使用 Map<String, dynamic>
  • decrypt / verifyAndDecrypt 在没有 decryptionCert 时直接抛 ArgumentError,不走原生通道;
  • OHOS 上三者的实现都走 _signedJsonUtil.internalVerify / internalDecrypt,并在 compute() 后台跑 package:jose

4.3 平台分支:lib/src/base_signed_json.dart

SignedJsonUtil 是 OHOS 与 Android 行为分叉的核心:

class SignedJsonUtil {
  late final Future<void> computerStarting;

  bool get useNativeSignedJson => Platform.isAndroid;

  Future<R> run<P, R>(FutureOr<R> Function(P) function, P param) async =>
      compute(function, param);

  Future<String> internalVerify(String cert, String encoded) async {
    if (useNativeSignedJson) return NativeSignedJson.verify(cert, encoded);
    final map = ComputerArgs(cert: cert, encoded: encoded).toJson();
    return run(_verifyOnBackgroundThread, map);
  }

  Future<String> internalDecrypt(String cert, String ciphertext) async {
    if (useNativeSignedJson) return NativeSignedJson.decrypt(cert, ciphertext);
    final map = ComputerArgs(cert: cert, encoded: ciphertext).toJson();
    return run(_decryptOnBackgroundThread, map);
  }

  Future<T> verifyAndDecrypt<T>(
      String certVerify, String certDecrypt, String encoded) async {
    String result;
    if (useNativeSignedJson) {
      result = await NativeSignedJson.verifyAndDecrypt(
          certVerify, certDecrypt, encoded);
    } else {
      result = await internalDecrypt(
          certDecrypt, await internalVerify(certVerify, encoded));
    }
    return run(parseAndDecode, result);
  }
}

useNativeSignedJson 直接读取 Platform.isAndroid。OHOS 既不是 Android 也不是 iOS,因此 useNativeSignedJson 始终为 false,三个方法都走纯 Dart 分支:internalVerifycompute(_verifyOnBackgroundThread)package:jose 验签并 ZLibCodec().decoder 解压;internalDecrypt 同理;verifyAndDecrypt 在 Dart 侧串联两步。

后台 isolate 收到 ComputerArgs 后,调用 package:joseJsonWebSignature.fromCompactSerialization / JsonWebEncryption.fromCompactSerialization,再用 JsonWebKeyStore + JsonWebKey.fromJson 注入密钥,最后把 payload 的 zlib 字节流解压为 UTF-8 字符串返回。

4.4 Dart 通道协议分析

4.4.1 通道名称仅在 Android 上有效
static const MethodChannel _channel = MethodChannel('signed_json');

通道名 signed_json 是 Android 原生侧约定的。Dart 侧调用仅在 Platform.isAndroid == true 时发生,OHOS 上不会触发 invokeMethod,因此不存在“OHOS 通道拼写不一致导致收不到结果”这类问题。如果上游未来在 OHOS 上也走原生通道,需保证两端字符串完全一致。

4.4.2 仅在 Android 触发的原生调用
static Future<String> verify(String cert, String encoded) async {
  final zipBytes = await _channel.invokeMethod<List<int>>(
        'verify',
        {
          'cert': "{\"keys\": [$cert]}",
          'encoded': encoded,
        },
      ) ??
      [];
  final unzipped = ZLibCodec().decoder.convert(zipBytes);
  return utf8.decode(unzipped);
}

原生侧约定:参数 cert 会被外层包成 {"keys": [...]} 的 JWK Set;encoded 是 JWS / JWE 紧凑序列化字符串;返回值为 List<int>,是 zlib 压缩后的字节流,Dart 侧用 ZLibCodec().decoder.convert 解压并 utf8.decode 还原。OHOS 不调用这一段,因此无需关心原生侧的实现细节。

4.4.3 调用结束与资源回收

三个公开方法都是一次性调用:没有 EventChannel、没有订阅,也没有需要取消的监听。每次调用相互独立,返回值到达后本次调用即结束。后台 isolate 由 compute() 在返回后自动回收,业务层不需要额外做清理。


五、补全 OHOS 工程配置(不新增原生代码)

5.1 本库没有 OHOS 原生实现

signed_json 的平台差异完全集中在 Dart 侧 lib/src/base_signed_json.dartPlatform.isAndroid 判断上。OHOS 上 useNativeSignedJsonfalse,方法通道 signed_json 不会触发,因此:

  • 不需要在仓库根目录创建 ohos/ 目录,不需要写 SignedJsonPlugin.ets 之类的 ArkTS 插件;
  • pubspec.yamlflutter.plugin.platforms 不需要新增 ohos: 条目
  • GeneratedPluginRegistrant.ets 不会注册任何插件类(保持空即可)。

适配本库时,所有 OHOS 相关工作都集中在 example/ohos/:补全 OHOS 工程脚手架、修正示例应用配置、整理 demo。下面只介绍 OHOS 工程配置相关的差异点。

5.2 声明插件和宿主权限

signed_json 的 JOSE 实现是纯 Dart(package:jose + ZLibCodec),不读取传感器、不访问用户存储、不访问网络,因此插件 HAR 和宿主 entry 都不需要任何敏感权限

5.2.1 不需要插件 HAR

本库没有生成 ohos/ HAR,示例工程直接引用 Dart 包,因此没有插件 HAR 的 module.json5 需要维护。

5.2.2 应用 entry 的权限

示例应用的 example/ohos/entry/src/main/module.json5 只保留了 Flutter 模板自带的 ohos.permission.INTERNET,它与 JWS / JWE 处理无关:

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

signed_json 既不读取签名材料,也不读取设备传感器,因此不需要 DETECT_GESTURE 之类的敏感权限,接入应用时也不会出现运行时授权弹窗。INTERNET 属于普通权限,无需额外填写权限原因资源。

5.3 修正 example 应用配置

5.3.1 修正 AppScope 的 bundleName(重要坑)

example/ohos/AppScope/app.json5 中初始生成的 bundleNamecom.example.keyboardheightpluginexample,这是 flutter create 从另一个插件模板(keyboardheightpluginexample)拷贝过来的残留值。如果直接使用,会导致两个问题:

  1. 应用包名与示例工程名(signed_json_example)不一致,DevEco 工程的资源定位可能错位;
  2. 与设备上已安装的 keyboardheightpluginexample Demo 冲突,签名 / Profile 也会跟着错。

应改为与示例工程一致的 bundleName

{
  "app": {
    "bundleName": "com.example.signed_json_example",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}

修改后,DevEco 工程的 entry 模块识别、签名、Profile 选择都会以新的 bundleName 为准。提交前需要确认仓库其它位置没有再硬编码 keyboardheightpluginexample

5.3.2 清理 example/ohos/build-profile.json5 的签名材料

flutter create 与 DevEco Studio 会自动往 example/ohos/build-profile.json5 写入本机调试签名材料,本例当前文件包含:

  • signingConfigs[*].material.certpathstoreFileprofile:本机绝对路径;
  • keyPassword / storePassword:调试签名口令(Base64 字符串)。

这些内容必须从待提交版本中移除,再替换成不含敏感信息的通用配置:

{
  "app": {
    "signingConfigs": [
      { "name": "default", "type": "HarmonyOS" }
    ],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.1.0(18)",
        "runtimeOS": "HarmonyOS"
      }
    ],
    "buildModeSet": [
      { "name": "debug" },
      { "name": "profile" },
      { "name": "release" }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        { "name": "default", "applyToProducts": ["default"] }
      ]
    }
  ]
}

提交前用 git diff -- example/ohos/build-profile.json5 复核,确保不包含绝对路径或口令。签名材料保存在本机,公开仓库中只保留构建所需的通用配置。

5.4 注册插件:无原生插件类可注册

因为 pubspec.yaml 没有声明 ohos: 平台,example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 不会被注入任何插件注册代码。当前生成的 registerWith 函数体为空:

static registerWith(flutterEngine: FlutterEngine) {
  try {
  } catch (e) {
    Log.e(TAG, "Tried to register plugins with FlutterEngine (" + flutterEngine + ") failed.");
    Log.e(TAG, "Received exception while registering", e);
  }
}

这是预期行为,不要在 registerWith 中手工添加 signed_json 的注册代码——本库没有 OHOS 原生插件类。示例应用启动时,Dart 侧会按 Platform.isAndroid 自动走纯 Dart 分支。

5.5 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地:

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.1.0(18)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

本例的 product 没有显式声明 compileSdkVersiontargetSdkVersion,编译时使用 DevEco 工程的默认值。配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件应写清楚的内容
README.OpenSource上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖六个场景
pubspec.yamlexample/ohos/build-profile.json5核对包名、版本、仓库地址、许可证和签名占位
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置

signed_json 的上游包名为 signed_json,版本为 0.0.2,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。当前仓库没有 README.OpenHarmony_CN.mdREADME.OpenHarmony.mdCHANGELOG.OpenHarmony.md,OHOS 适配 PR 中应按模板补全。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

重点检查:

  • example/ohos/AppScope/app.json5bundleName 已经改成 com.example.signed_json_example
  • example/ohos/build-profile.json5 不含绝对路径与口令;
  • example/ohos/entry/src/main/module.json5 只保留 ohos.permission.INTERNET
  • docs/ohos-test-evidence/ 下保留关键证据(见第八节);
  • pubspec.yamlflutter.plugin.platforms 仍然是 android / ios / web没有新增 ohos: 条目。

6.3 提交并推送

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add lib config example/pubspec.yaml example/lib example/ohos example/tool docs
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for signed_json 0.0.2 (pure-Dart path)"
git remote -v
git branch --show-current
git push -u origin feat/ohos_signed_json_0.0.2

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己导入或 Fork 的仓库(AtomGit 或 GitHub),当前分支为 feat/ohos_signed_json_0.0.2,并按需补充 tag 0.0.2-ohos-1.0.0-beta.1

推送后在对应托管平台发起合并请求,说明上游来源和版本、OHOS 实现范围(无原生代码、纯 Dart 分支)、依赖及权限(仅 INTERNET)、测试环境、操作结果、已知限制(compute() 在 main isolate 之外、JWE 解密依赖 JWK 私钥),并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验 JWS / JWE 处理。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  signed_json:
    path: ../

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 Git 仓库引入插件

业务应用通过 Git 引入时,将 signed_jsonpath 配置替换为下面的 Git 依赖。这里固定到本文使用的提交:

dependencies:
  flutter:
    sdk: flutter
  signed_json:
    git:
      url: https://github.com/icapps/flutter-signed-json.git
      ref: 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_signed_json_0.0.2。正式发布后可固定到 tag 或 commit,例如 0.0.2-ohos-1.0.0-beta.1

signed_json 还依赖 jose ^0.3.2computer ^3.2.1。这两个包都是纯 Dart 库,不需要 OHOS 平台分支,Pub 会直接解析到上游版本。从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.locksigned_json 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口实现 6 场景演示

signed_json 的核心 API 是三个带泛型的方法。示例页面把它扩展成 6 个场景,3 个正向 + 3 个负向,分别覆盖验签、解密、链式组合以及篡改 / 错误证书 / 错误解密密钥的预期失败分支。下面这段可直接用于 example/lib/main.dart,对应仓库 example/ohos-test-evidence/evidence2_full_scenarios.jpegevidence3_final_red_icons.jpeg

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:signed_json/signed_json.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static const verifyCert = '''{
    "kty": "EC",
    "use": "sig",
    "crv": "P-521",
    "kid": "signed_json",
    "x": "AT0MMaUvyZdRZaCIqkLZPks0NE4kPZhgTwIivnr0tDhcl7Ao9SSdiXPuCsZS3HaAuQq5Kk1sSWGwNtteiq5JsiSg",
    "y": "AVKxKaIYJewNWA3DvD5qzpW9vSMt6A1crUqVpQ-MKAOVsCL_RwwvQoedRoMbYpf6T7XLflECaI58pBtv8JfF8zVb",
    "alg": "ES512"
}''';
  static const decryptCert = '''{
    "p": "2DXmKMTnT1t8mi7MWuDn9YJ9aymtRO-L-Q2Hj3porAXYyeifD6tWpd3B283SzBNmJMSzb9ahCRrIXJbrh8xPoQWAacY-zEgth_Eujl0r4KyzjzOwFwjhk_9dB2g0PZx1bsZtW5Crq8s_NAyURJP45bzS4yYYpI6PT7e63bfchZ0",
    "kty": "RSA",
    "q": "sag_C3sbeLOdhzDknfs8zMsy6uFfBjmAT2BrE3-K5QHZXOCQ7304VhRIvt3BeDUYDF1Onu4RYB7yRfcYkI1yG5GI-Zx2jTwcGhADIs-ovGsKbY7H5SDLjAGNWOj4NfW2ObuuVB68DCeaatHJ30XXilFw9gRZBLtsgxtK9BPPHnc",
    "d": "hlv6XGfcUNJj5eTKCTDgYZuVrucW9Qd5fMIewRj-BSjuXycyGpqEsLQegv4p3Qan_FwQYHRshh6pDl8f746BsPspsQGvZwyht3y5HZfz_XJ0sY4gWNT5fAg6G3ch9W53PTEb2zX7SLKyWpM1H0Qapdt8r5WqC1oYNpHYc88mImk71pBLd_ywWvYqahq52VOyEiXHslRz4Wb8OFhrlU59aV7EErHMbuB6KdLpfTQBmjSEVEbeHRrzmGpgPfNXKRxBiN50DTfObBt3wDYxyO87F8fJm4pjqwwa034J_WROEa0YUPqjvn2Xj9KRRZhgoKmQUVmApjIr7oqqg84BSONGMQ",
    "e": "AQAB",
    "qi": "EYowTKshuJPVcmzUKuYmBYhAah0_AikBOO6lZLP65SGb3frVWBqAFGeKL-K72c4Cwlt74dZHVbkPVRUpFQuKGXtFI6DJTtorWdfvFUblsB2f-OeEK8QPSz5QGmSywZ8oD11hbpWUdua764ZqXQaEZnHyUUbUGguxEPMA8GHdQhk",
    "dp": "hcv0_k27htRqq09CjwqXAMsbqfFElGBZEmpY9WUe2TVVDr2xkRTKriIpEUixpjBrCV3gXNlJFkVIsGOEpai9rjulV8-ilPAlnPaXhOoLeSHmjDvEQLzyO4_PlgHaMjZcRYztp7hDRDCmkCMorbeUUzcimga9QTgnX4GnVgWtpdE",
    "dq": "dPccauamc5VuBW__VLPwl7TA1TuEYIjDHX-Rf8jdHWFWRnvjcIm06Zd5PZCqrAXoy1szRBfhgLNfNwk0NxepJNVwpUaKFvqYVeBs8CJgKY0f1HnIyeYJnSf4c60Onhgj3Wbfo6qIjEgWtnVgv4swGXT9Njwuj5sGGluBwai5GIk",
    "n": "lgtcwDDTKxWLLnpVTGCad7LWnRPSO3nh0Imd1GT0cxZyIggwYDuaHPN6IQQc6Lv68_giQRZmWSjntQGAfvNdIXY38QstgD11P2ks3LtfQBdkBnfmIudCTRQr4eOHn4t_UErKRJB4LnmLTtlkqgbC0wj4lsSRWld8gj8thbwLoLKry8ouvwV2G_ST7SLecVQsaK5zceIV_AirXHnCgVHLC_RFcyWcYj1PlmwoSzvC4AcLj3GBPbV-ag_khXbaGp_fEt7niFhGtx2QtiWHv3UJuYFJ_mn81QW_NGJwa3qepKvc_F4HnVWCzkcpALJbjUsZKio1ETmvpLqPmJ17AB2B-w"
}''';
  static const wrongVerifyCert = '''{
    "kty": "EC", "crv": "P-521",
    "x": "AaKWEFHhtQe2Deti4v8ZvVnatjAeHQg3T1DicxaHHQGTtZBUz2lHYRkoaxOUnbKeCiYjtvlZGk5yP_pRZ1h4_wXi",
    "y": "AV1-NvlTWgTinqGb32Osey9ZL5B980kbkNj3hj8ZjkRy5cKLdc0D7TGlek4lMZ--mWCCAlrEJSP8NJx4lKLBdczR",
    "alg": "ES512"
}''';
  static const wrongDecryptCert = '''{
    "kty": "RSA",
    "n": "hUr7FueINW0xQkMop9jEnTaCBcqwFfI7QJ3hkFPNi4rVtQYJtXuCYGsmq9CgMS_DzKwcWg2TQ1W8k6VpgVWLZEu_8G6Ew8ZQHdTADGJfeUieIYn8vSjWtoXN8VS3GFArhNWtPWLus-exGQMvz02CqALLnoiyoGIZIC1--X0C2H7x5IsKPtw6CfddBqcspQ3X0g01BHPZgpyqW3SsKVGHIyUlKNFnUDhjoSu47PayxgK3UiRdN4kciGUYNkDEM4Zpzk6L8x9_72b1xXpzln8S4h9te_Q_5X8T4Auq2tgzF54WREPN9AdyWtmAHM7cIgtJPMc--GxyiDbF-3vTWSYumQ==",
    "e": "AQAB",
    "d": "JIcv2E8LHNkXrrkI4zacaxkM-Nla-Cix5DtgHVVZ9uvNNRa6gmmeiR3UMzGxNMmKNwTToDooKUPNsgiaqT7wPEQmDZW7_IrUWdh76OjskSg9baOLB6uxa8OvdHtq0dbmljiYiUIbeGH-PoSJDZ6IN9LMSl3b1egMSq0tJuIDbaC3GniYt_uTeNOg8atKm1jdnB3qqfULYcnb8_HU4ujxtm2YvCM5Gh90vESqA8gMlfnebqDduOclyuXUSVlvknBXxXu32ZjEFBHuIp54yngnjF63YYAZ9H6J7vrgEdysO1kCpbaJCVAKBujYK4C4FFzjDhDpC2HSTxY5-lrP1iQsIQ==",
    "p": "uNm5c5yTgCHf2NZLFsU8QrZ4XRZFh01F_760GJcl3cRmjetKNfkBulLi3E-S2jwYBSB93k4NK7DnEqDZ2ffLg4O2i1lk8Xaz3PyxSi4LhJ3I-ysppfMkh4ZKmeC7WJxXPMPeZG5YR9MGNhHuELB-B6eM-m8hY4YrgrLuYvZVVg8=",
    "q": "uJkFFcU6_kM3MRlD2ZI6XQwjDsf8EkDlH6O9Xym_uOMlALHIkmtGafclFJkHBOzNyIl24frcizX49YRUbcUjjHc-hqAbD18vW_90gIOw_WICsba-H2llLPEeeK2HulMQJKqSIVe232YlLT6ite7qUF1o602AAINZBFR1Hn9-mNc=",
    "alg": "RSA-OAEP", "use": "key"
}''';

  static const encoded =
      'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgV';
  static const tamperedEncoded =
      'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgg';
  static const encryptedEncoded =
      'eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBMV81In0.a5a73YZgaQwTH3V5pqMXUIuAxKUqZdH5rQxN9RBWNiizJK03BamKqOZjbHuWv18qwAkgWxudRXF4io2vVZv5gChmpwQ3KjzS2z4ccMY5GNnf-_5SSX2bzIvoEJvBaFvseA049cBsdvcfTKKZZXJXe_z4k_iB-hyvo5JOQ8ki30sToDeMT9sVkLfh9icMP-DJ73zzqmsK9twpZgjChmEdHqJbx5xmejfSjdW76-RTWm3LEFkTBsg_Ye3Lupj9qmAkIQvC9Xgbvu2tCcmgSLpQz135H33GSP_cYnRfX0ho-nXPOpvba4ASJL68S_BBmrpCTG2U1w95RKufqVhwKKZUQg.tSsXy0GVk6CsJnnkbjO-nQ.KPFLPN8tLK3vqAvLvLO9IO6Q88vPVPNO1YoiP3tsaP_sbNTa92W514eakZ0QiaNU.5TWtnlE_Bbx4xo95mRoITw';
  static const chainEncoded =
      'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eJwFwce2Q0AAANAvkoMxCUuk6W3UjYMoQ7SHIb7-3Vv-1G_eG9jCqmQghXNo9a77DTJan1XwjuPwGJR2xP7XlIyAZ5SBvmQwu4E4qTNnR28QwGk2Il_ZxEPz5-Tzhn_OYQquFJoYn6pGAynrtdlK2vy9hYTh513s6vDYPm705PDIkiAhsJabftodoLWnx55cURgxfJlDRaXQ8yI2PxUyPlQiZU-ylCLNCYW0fEhRIU1LkkiNyvTkuhRLVPMjI1Qth-8woBc03ksDCUvQ6VUj4MKwqbt6A-c594smrPuU1K3c9I_Pe1bzAx592VZe-wlvV8pFYQ_0x7ND0lKncQn0bWqFuRc7xSGyENU52dhVLvra0yfnZAB8A_Dy7LSIB7eK6Gakhsi2JpJnnOip-pX3Uknq_yYZvVif2QXoals1B82uaYnv1JfVW6If_Qq6q7yow9DlrUUNzkWzn7pt8quuATKLRCe6JSjW1eF5Yge2aTHxiG2wLpmdLrmJMoENIcOVWZfQDs5M_wJRuA7fRyrlB3eMAuzdUUH7PzOgqmA.AOfUPM3bUC784fb4IKnNhq5jNx6WvFm2TajV8GMx0x0Xv0ucHKagkNhwH1Gta-SrwcKjbw34KrY4hDRkTiBliiy2APEs4xH_gwJ0ifRYwBNXR6RABtN32UOV_pYaZ6Iftuw5eST4V0lNjYjR0ikzcPZy9UVxRWl2fLJFHLSrWcY6-ad7';

  final _signedJson = SignedJson(verifyCert, decryptionCert: decryptCert);
  final _wrongVerifyJson = SignedJson(wrongVerifyCert);
  final _wrongDecryptJson =
      SignedJson(verifyCert, decryptionCert: wrongDecryptCert);

  final List<_ScenarioResult> _results = [];
  var _running = false;

  
  void initState() {
    super.initState();
    _runAll();
  }

  Future<void> _runAll() async {
    setState(() {
      _running = true;
      _results.clear();
    });
    await _run('1. verify:用 verify.jwk 公钥验签 remote_config.signed',
        () async => jsonEncode(
            await _signedJson.verify<Map<String, dynamic>>(encoded)));
    await _run('2. decrypt:用 decrypt.jwk 私钥解密 remote_config.enc',
        () async => jsonEncode(
            await _signedJson.decrypt<Map<String, dynamic>>(encryptedEncoded)));
    await _run('3. verifyAndDecrypt:先验签再解密的完整链路',
        () async => jsonEncode(await _signedJson
            .verifyAndDecrypt<Map<String, dynamic>>(chainEncoded)));
    await _run('4. verify(篡改签名):签名最后一个字符被翻转,应验签失败',
        () async => jsonEncode(await _signedJson
            .verify<Map<String, dynamic>>(tamperedEncoded)),
        expectFailure: true);
    await _run('5. verify(错误证书):用不相关的 EC 公钥,应验签失败',
        () async => jsonEncode(
            await _wrongVerifyJson.verify<Map<String, dynamic>>(encoded)),
        expectFailure: true);
    await _run('6. decrypt(错误证书):用不相关的 RSA 私钥,应解密失败',
        () async => jsonEncode(await _wrongDecryptJson
            .decrypt<Map<String, dynamic>>(encryptedEncoded)),
        expectFailure: true);
    if (mounted) setState(() => _running = false);
  }

  Future<void> _run(
      String title, Future<String> Function() op,
      {bool expectFailure = false}) async {
    final sw = Stopwatch()..start();
    _ScenarioResult r;
    try {
      final value = await op();
      sw.stop();
      r = _ScenarioResult(
        title: title,
        success: !expectFailure,
        errorPath: expectFailure,
        detail: expectFailure
            ? '未抛出异常(不符合预期),返回: ${_truncate(value)}'
            : _truncate(value),
        elapsed: '${sw.elapsedMilliseconds} ms',
        icon: expectFailure
            ? Icons.error_outline
            : Icons.check_circle,
      );
    } catch (e) {
      sw.stop();
      r = _ScenarioResult(
        title: title,
        success: expectFailure,
        errorPath: expectFailure,
        detail: expectFailure
            ? '按预期抛出异常: $e'
            : '非预期异常: $e',
        elapsed: '${sw.elapsedMilliseconds} ms',
        icon: expectFailure ? Icons.block : Icons.error_outline,
      );
    }
    if (mounted) setState(() => _results.add(r));
  }

  String _truncate(String v) => v.length > 160 ? '${v.substring(0, 160)}…' : v;

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'signed_json OHOS demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('signed_json OpenHarmony 演示'),
          actions: [
            IconButton(
              icon: const Icon(Icons.replay),
              onPressed: _running ? null : _runAll,
              tooltip: '重新运行',
            ),
          ],
        ),
        body: ListView(
          padding: const EdgeInsets.all(8),
          children: [
            for (final r in _results)
              Card(
                margin: const EdgeInsets.symmetric(vertical: 4),
                child: Padding(
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(
                            r.icon,
                            color: r.errorPath
                                ? Colors.red
                                : (r.success ? Colors.green : Colors.red),
                            size: 22,
                          ),
                          const SizedBox(width: 8),
                          Expanded(
                            child: Text(r.title,
                                style: const TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.w600)),
                          ),
                          Text(r.elapsed,
                              style: const TextStyle(
                                  color: Colors.grey, fontSize: 12)),
                        ],
                      ),
                      const SizedBox(height: 8),
                      SelectableText(
                        r.detail,
                        style: TextStyle(
                            fontSize: 13,
                            color: r.success ? Colors.black87 : Colors.red),
                      ),
                    ],
                  ),
                ),
              ),
            if (_results.isEmpty)
              const Padding(
                padding: EdgeInsets.all(32),
                child: Center(child: CircularProgressIndicator()),
              ),
          ],
        ),
      ),
    );
  }
}

class _ScenarioResult {
  final String title;
  final bool success;
  final bool errorPath;
  final String detail;
  final String elapsed;
  final IconData icon;

  const _ScenarioResult(
      {required this.title,
      required this.success,
      required this.errorPath,
      required this.detail,
      required this.elapsed,
      required this.icon});
}

每个场景都必须显式传 <Map<String, dynamic>>,否则 parseAndDecode<T> 内部的 jsonDecode(response) as T 会因为推不出目标类型而失败(旧版示例仅写 _signedJson.verify(...) 时,运行时报 type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>' in type cast,见 9.5)。

chainEncoded 不是仓库自带的数据,它由 example/tool/ohos_chain_test.dart 在宿主端用 config/keys/sign.jwk 私钥对 zlib(remote_config.enc) 签名后生成;演示前需要先在宿主机跑一次该脚本,把输出写回 chainEncoded。该脚本同时会在宿主端验证三个场景(JWS 验签、JWE 解密、先验签后解密的链),可以提前排除密钥配置错误。

7.4 页面退出时的异步处理

异步回调先检查 mounted,避免页面销毁后继续调用 setStateverify / decrypt / verifyAndDecrypt 都是一次性调用,没有订阅需要取消,dispose 中无需额外清理。

多个页面都需要 JWS / JWE 处理时,可以在应用启动阶段缓存一个 SignedJson 实例并持有密钥字符串,各页面只调用 API,避免重复构造 / 解析 JWK。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

接口测试应验证 verify / decrypt / verifyAndDecrypt 的泛型类型、参数传递、返回值解析以及异常路径。本例的 test/signed_json_test.dart 通过 mock signed_json 通道,验证 verify<Map<String, dynamic>> / decrypt<Map<String, dynamic>> 返回 mock 值;上游历史示例曾把 _signedJson.verify(...) 当作返回 String 使用,运行时会因 parseAndDecode<T>jsonDecode(response) as T 强转失败而崩溃(参见 9.5)。

当前仓库的运行结果为:flutter analyze 在根目录与 example 目录下合计报告 0 error、42 issue(其中多数为 example/tool/ 调试脚本里的 avoid_print,以及 test/signed_json_test.dart 中的 deprecated_member_use 提示)。flutter test 在本机因 flutter_testerWebSocketException: Invalid WebSocket upgrade request 无法启动,这是本地测试环境问题,不是被测代码缺陷。第六章适配分支上的真机运行已经覆盖了核心场景,详细的鸿蒙设备日志保留在 docs/ohos-test-evidence/evidence_hilog_full.txt

8.2 确认设备连接

hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。本文适配使用的设备为 4UQ9K25508013016,系统版本 Ohos OpenHarmony-6.1.1.120(API 24),架构 ohos-arm64。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs
  4. default product 选择或生成签名;
  5. 确认设备、应用包名(应已改为 com.example.signed_json_example,参见 5.3.1)、证书和 Profile 匹配;
  6. 再回到终端执行 Flutter 构建或运行。

签名材料保存在本机,公开仓库中只保留构建所需的通用配置。

8.4 运行示例

以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:

flutter run -d <device-id>

也可以先构建 HAP:

flutter build hap --debug

典型产物位于:

example/ohos/entry/build/default/outputs/default/

目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。

8.5 在设备上测试六个场景

  1. 打开应用,确认初始页面渲染 6 个场景卡片,3 个正向用绿色对勾(Icons.check_circle),3 个负向显示红色禁止图标(Icons.block);
  2. 每个卡片右侧显示耗时(毫秒),下面给出运行结果摘要:
    • 1 verify{"some_text":"Something Something"}300500 ms;
    • 2 decrypt:同上,1030 ms;
    • 3 verifyAndDecrypt:同上,150250 ms;
    • 4 verify(篡改签名):按预期抛出 FormatExceptionJoseException: Could not decrypt/verify payload
    • 5 verify(错误证书):按预期抛出 JoseException: Could not decrypt/verify payload
    • 6 decrypt(错误证书):按预期抛出 JoseException: Could not decrypt/verify payload
  3. 点击右上角“重新运行”按钮,确认所有场景重新执行,结果稳定可复现;
  4. 切到后台再切回前台,确认页面状态不丢失;
  5. 卸载后重新安装,确认无缓存副作用。

页面显示的耗时来自 Stopwatch().elapsedMilliseconds,覆盖 _runAllcompute()package:jose 全链路;compute() 跨 isolate 带来的固定开销主要体现在场景 1 和场景 3。

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用能够通过纯 Dart 分支完成 JWS 验签和 JWE 解密,覆盖 3 个正向场景和 3 个预期失败的负向场景。下面是在 OpenHarmony 6.1.1.120(API 24)真机上的两次运行截图:


KeyHash 示例页

序号测试项结果表现耗时状态
1verify:用 verify.jwk 公钥验签 remote_config.signed验签通过,返回 {“some_text”:“Something Something”}3312 ms通过
2decrypt:用 decrypt.jwk 私钥解密 remote_config.enc解密成功,返回 {“some_text”:“Something Something”}692 ms通过
3verifyAndDecrypt:先验签再解密的完整链路完整链路成功,返回 {“some_text”:“Something Something”}167 ms通过
4verify(篡改签名):签名最后一个字符被翻转,应验签失败按预期抛出异常:JoseException: Could not decrypt/verify payload106 ms按预期失败
5verify(错误证书):用不相关的 EC 公钥,应验签失败按预期抛出异常:JoseException: Could not decrypt/verify payload3 ms按预期失败
6decrypt(错误证书):用不相关的 RSA 私钥,应解密失败按预期抛出异常:JoseException: Could not decrypt/verify payload3 ms按预期失败

以下是操作的视屏,可以参考一下:

Example 启动授权


首次运行最终运行
6 个场景全部按预期:3 正向返回原文 JSON,3 负向抛出异常并以红色禁止图标显示同样 6 个场景,3 正向保持绿色对勾,3 负向保持红色禁止图标
截图来源:docs/ohos-test-evidence/evidence2_full_scenarios.jpeg截图来源:docs/ohos-test-evidence/evidence3_final_red_icons.jpeg

两次截图均保留在仓库的 docs/ohos-test-evidence/ 目录下,便于评审时核对。日志捕获保留在 evidence_hilog_full.txt,可以按需筛 signed_json 相关条目定位问题。负向场景的红色禁止图标说明 demo 已经识别“预期的异常”,而不是把负向测试当作失败;如果图标显示为红色 error_outline,说明 demo 把异常当成了非预期失败,需要排查 JWK 配置是否正确。


九、FAQ:适配过程与使用问题

9.1 Missing SDK components

典型错误如下:

Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.

这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchainsetsjsnativepreviewer
  5. 执行 flutter config --ohos-sdk <正确路径>
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。

当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;但 JWE 解密依赖应用安装包内携带的 JWK 私钥字符串,仍需在目标应用上验证。

9.2 DevEco Studio 中看不到 entry 模块

本库没有生成插件 HAR,示例应用的 entry 模块位于 example/ohos/entry

请直接使用 DevEco Studio 打开:

flutter-signed-json/example/ohos

如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 entry 模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos不是仓库根目录
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效;
  • example/ohos/AppScope/app.json5bundleName 已是当前示例的 com.example.signed_json_example,没有残留 keyboardheightpluginexample

9.4 OHOS 上需要写原生代码吗?

不需要。signed_json 的平台分支完全在 Dart 侧:

bool get useNativeSignedJson => Platform.isAndroid;

OHOS 既不是 Android 也不是 iOS,useNativeSignedJson 始终为 false,方法通道 signed_json 不会被触发。所有 JWS / JWE 处理都通过 compute() 在后台 isolate 跑 package:jose + ZLibCodec 完成。适配本库时:

  • 不要在仓库根目录创建 ohos/ 或写 SignedJsonPlugin.ets
  • 不要在 pubspec.yamlflutter.plugin.platforms 下新增 ohos: 条目;
  • 一切 OHOS 相关工作集中在 example/ohos/:修正 AppScope/app.json5bundleName、清理 build-profile.json5 的签名材料、覆盖示例 demo。

如果未来插件作者在 OHOS 上加了原生实现(例如用 HarmonyOS 的 JOSE 库),需要再补一个 ArkTS 插件并相应调整 useNativeSignedJson 的判断;本文不涉及这部分。

9.5 报 type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>'

这是上游示例的历史坑。signed_json 的三个方法都是 Future<T>,返回值由 parseAndDecode<T> 决定:

T parseAndDecode<T>(String response) => jsonDecode(response) as T;

调用方不指定泛型时,T 会被推断为 dynamic 或默认类型;当 JSON 实际是 Map<String, dynamic> 时,强转会失败。运行期错误形如:

type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>' in type cast

修复方式是显式提供泛型:

await _signedJson.verify<Map<String, dynamic>>(encoded);
await _signedJson.decrypt<Map<String, dynamic>>(encryptedEncoded);
await _signedJson.verifyAndDecrypt<Map<String, dynamic>>(chainEncoded);

旧版示例直接把返回值当作 String 使用,因此新版 demo 把 6 个场景都改成显式 <Map<String, dynamic>>。运行时遇到的崩溃截图保留在 docs/ohos-test-evidence/evidence_error_initial.jpeg,与当前 demo 的 evidence2_full_scenarios.jpeg 对照可以看到差异。

9.6 bundleName 还是 com.example.keyboardheightpluginexample

flutter create --platforms=ohos . 在示例工程下生成的 AppScope/app.json5 默认会从上一次执行 flutter create 的目录继承 bundleName。如果当前目录是从另一个插件模板(比如 keyboardheightpluginexample)拷过来的,新工程的 bundleName 仍然是旧值。

直接使用旧 bundleName 的后果:

  • 与示例工程名(signed_json_example)不一致;
  • 与设备上已安装的 keyboardheightpluginexample Demo 冲突;
  • 签名 / Profile 不匹配,会出现 9.7 中的安装失败。

修正方式见 5.3.1:

{
  "app": {
    "bundleName": "com.example.signed_json_example",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}

修改后用 git diff 确认仓库其它位置没有再硬编码 keyboardheightpluginexample

9.7 SDK 约束是 >=2.15.0 <3.0.0,OHOS 工具链需要改成 >=3.0.0 吗?

不需要。仓库的 pubspec.yaml(以及 example/pubspec.yaml)仍然使用:

environment:
  sdk: ">=2.15.0 <3.0.0"
  flutter: ">=2.5.0"

实测在 Flutter 3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)下 flutter pub get 可以解析成功。package:jose ^0.3.2package:computer ^3.2.1 都是纯 Dart 包,不依赖特定 Dart 主版本。强行改成 >=3.0.0 反而会失去对老版本工具链的兼容。建议保持现状,等上游主版本升级时再统一调整。

9.8 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • AppScope/app.json5bundleName 与签名 Profile 不匹配(参见 9.6);
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。flutter pub get 解析阶段应当不再报与签名相关的问题,因为本库没有 ohos: 平台条目。

9.9 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。本例在 example/ 下执行 flutter create --platforms=ohos . 时不需要再传 --project-name,因为 example/pubspec.yaml 已经声明包名 signed_json_example;如果改成在仓库根目录执行 flutter create --template=plugin --platforms=ohos --project-name signed_json .,则会强行生成空的 ohos/ HAR 脚手架,请避免这种情况。

包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name signed_json;仓库名 flutter-signed-json 不能直接作为 Dart 包名(pubspec.yamlnamesigned_json)。

9.10 MissingPluginException:纯 Dart 实现为何不会抛

MissingPluginException 通常表示 Dart 通道找不到已注册的原生插件。本库在 OHOS 上不调用任何原生通道,因此即使 GeneratedPluginRegistrant.ets 为空(参见 5.4),也不会抛该异常。

如果出现了 MissingPluginException,先排查是否被某个依赖插件触发了 MethodChannel 调用,例如 cupertino_icons 或调试期加入的统计 SDK。本例 example/pubspec.yaml 仅依赖 cupertino_icons,没有其它 OHOS 通道需要注册。

9.11 场景跑出异常时,如何对照原仓库密钥复现

如果真机上 1 / 2 / 3 场景也失败:

  1. 确认 verifyCert / decryptCert / encryptedEncoded / encoded / chainEncodedconfig/ 下对应文件一致;
  2. 在宿主机端运行 dart run example/tool/ohos_chain_test.dart,它会用 config/keys/ 下的真实 JWK 在宿主端跑 S1(验签)/ S2(解密)/ S4(链式),输出成功或异常;
  3. 如果宿主端 S1 / S2 / S4 失败,说明示例中的常量抄错了,或 config/keys/*.jwk 被改动过;如果宿主端成功而 OHOS 失败,再检查 example/lib/main.dart 的常量是否与宿主端脚本一致;
  4. chainEncoded 必须由 ohos_chain_test.dart 现场生成,不能从其它示例拷贝;不同 sign.jwk 私钥产生的 JWS 完全不同。

docs/ohos-test-evidence/ 下的截图和 evidence_hilog_full.txt 是评估适配是否成功的关键证据,评审前应一并提交。


相关链接

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐