Flutter安全库sec鸿蒙化适配实践:加解密与密钥存储迁移指南
发布时间:2026/10/13 7:17:14 锦皓数字建站

接手把 Flutter 三方库 sec 做鸿蒙化适配的时候我一开始是带着侥幸心理的。这个库在 Android 和 iOS 上已经稳定跑了两三年功能无非是加解密、密钥存储、token 保护这几件事。我天真地以为鸿蒙适配只是把工程切到模拟器重新编译一遍顶多改改原生配置。结果第一个加密调用跑起来直接抛 MissingPluginException整个 channel 都没注册成功。那一刻我才清醒过来Flutter 的“跨平台”从来不是免写原生代码而是把原生侧的实现责任压到开发者头上。sec 库的 Android 实现和 iOS 实现写得再漂亮鸿蒙侧没有对应的原生代码Dart 层照样一无所有。这篇文章就围绕这件事展开怎么把 sec 库的加解密逻辑、密钥存储、安全资产能力完整迁移到鸿蒙原生层以及我在这个过程中踩过的坑。如果你是做 Flutter 跨平台应用、并且准备把应用带到鸿蒙设备的开发者这篇指南基本能帮你省掉一大半试错时间。1. 为什么 Flutter 的三方库 sec 到了鸿蒙就成了“半残废”1.1 Flutter 插件不是“写一次到处跑”而是“一处接口多处实现”很多 Flutter 开发者的误解在于认为 Flutter 插件等于“一套 Dart 代码到处都能跑”。实际完全不是这样。Flutter 的跨平台只发生在 UI 层和 Dart 逻辑层一旦涉及原生能力比如 Keychain、Keystore、指纹识别就必须由各平台自己写原生实现。Dart 层只是通过 MethodChannel 发一个方法名和若干参数过去原生层有没有人接、接得好不好完全取决于插件作者有没有为这个平台写代码。sec 库也一样。它的 Dart 层暴露了诸如encryptData、decryptData、saveToken、getToken这类接口底层在 Android 上走的是 Keystore JCE 的 Cipher在 iOS 上走的是 Keychain Security framework。到了鸿蒙两个原生实现里没有任何一个能直接复用因为鸿蒙既不是 Android 的 ART 运行时也不兼容 iOS 的 ObjC 运行时。于是 Dart 层一调用原生侧没有注册对应的 channel handlerFlutter 引擎只能返回一个 MissingPluginException。可以把这套机制理解成家电遥控器Dart 层是遥控器按键method name是固定的Android 电视、iOS 电视、鸿蒙电视是不同品牌内部电路完全不同。你不能因为遥控器同一个按键在安卓电视上有反应就认定在鸿蒙电视上也有反应。适配的本质就是给鸿蒙这台“电视”把内部电路接上让同一套按键重新生效。1.2 sec 库依赖的原生能力鸿蒙侧几乎全部缺席动手之前我先把 sec 库的接口依赖做了一次盘点。它表面上只有加解密和安全存储底层靠的是四个能力域而这四个能力域在鸿蒙侧要么没有现成封装、要么 API 模型完全不同能力域Android 实现iOS 实现鸿蒙侧的对应能力对称/非对称加解密JCE CipherAES/GCM、RSA/OAEPCryptoKit / Security framework鸿蒙 Crypto Framework配合 HUKS 密钥使用密钥安全存储Android KeystoreTEE/StrongBoxKeychainSecure Enclave鸿蒙 HUKSUniversal Keystore安全资产存取EncryptedSharedPreferences KeystoreKeychain 通用数据项鸿蒙 Asset 服务生物认证BiometricPromptLocalAuthentication鸿蒙 UserAuth 认证服务这四项里前两项是加解密逻辑的基石后两项是安全资产实战的必经之路。每一项在鸿蒙都有“能力”但都被包在完全不同的 API 模型里。所以鸿蒙化适配的核心工作不是把 Java 代码翻译成 ArkTS而是把 Android/iOS 的安全语义映射到鸿蒙的 API 上。比如 Android 的 Keystore 用KeyGenParameterSpec定义密钥属性鸿蒙 HUKS 用一组HuksParam定义两者能表达的属性能力是可以对齐的但代码结构和调用时序完全两码事。这里我建议所有准备做适配的人开工前先画一张“能力映射表”把 sec 库所有接口涉及的底层操作列出来再逐一标出鸿蒙侧的对应方案。这张表就是你后面所有开发的蓝图。我没做这一步的时候是边写边查结果反复返工。2. 先把 sec 库的加解密与安全资产逻辑拆开看2.1 数据加密AES-GCM 与 RSA-OAEP 的实际应用场景sec 库的加解密逻辑从上层看很简洁无非是“明文进去、密文出来”但实际上分了两条线对称加密和非对称加密。对称加密的主力是 AES-256-GCM用于加密本地文件、缓存、数据库里的敏感字段特点是快适合大数据量。非对称加密的主力是 RSA-OAEP用于加密小体积的关键内容比如主密钥、会话密钥、证书等特点是慢但安全性更高公私钥分离。为什么选 GCM 而不是 CBC因为 GCM 是 AEAD 算法加密的同时产生认证 tag解密时先校验 tag 再返回明文机密性和完整性一把抓。CBC 模式只负责加密不负责认证如果业务层没有额外做 HMAC很容易被 padding oracle 攻击。这一点在迁移时不要为了省事换回 CBC保持 GCM 是正解。但 GCM 有几个前置条件必须统一IV随机数推荐 12 字节即 96 位同一把密钥下绝不能重复使用否则会直接泄露密钥信息tag 长度固定Android 默认 128 位sec 库的 Dart 层通常也是按 16 字节解包AAD附加认证数据是可选参数但一旦加密时传了 AAD解密时必须传一模一样的 AAD否则解密必失败sec 库在 Dart 层普遍采用“iv ciphertext tag”的拼接格式原生层返回结果时必须按这个顺序组装。RSA-OAEP 这边关键参数是摘要算法和 MGF1 的哈希函数。Android 默认可能用 SHA-1iOS 可能用 SHA-256如果两边不一致鸿蒙解密时会出现“InvalidParameterException”或者乱码。迁移时建议把摘要算法固定为 SHA-256并且两侧实现都显式声明不要让默认值替你做决定。2.2 密钥管理从 Keychain/Keystore 到鸿蒙 HUKS 的映射密钥管理是安全库的灵魂。sec 库的密钥管理模型分两层应用层有一个主密钥Master Key由系统级安全存储保管业务数据用主密钥派生的子密钥来加密。主密钥本身不能导出到明文只能以 alias 的形式引用。这套模型在 Android、iOS、鸿蒙上殊途同归但 API 差别很大。我建议用下面的映射关系去做设计Android KeystoreiOS Keychain鸿蒙 HUKSkey alias 字符串key 服务名 属性keyAlias 字符串KeyGenParameterSpec.BuilderkSecAttrAccessible kSecAttrAccessControlHuksParam 数组算法、用途、大小、模式、摘要KeyStore.getKey()SecItemCopyMatching不允许直接取密钥只能通过 init/update/finish 操作Cipher.init() 绑定密钥CryptoKit 封装huks.init / huks.update / huks.finish 三段式generateKey 后删除SecItemDeletehuks.deleteKeyItem这里最需要注意的一点是HUKS 生成密钥后私钥和对称密钥默认不可导出。Android Keystore 也类似但 iOS Keychain 在某些配置下可以导出密钥数据。如果 sec 库原来的设计里有“将密钥备份到云端”或“跨设备恢复”的功能迁移到鸿蒙时必须重新设计改用importKeyItem在有授权的前提下导入备份密钥而不是直接读密钥字节。还有一个细节容易被忽略HUKS 的密钥属性在生成时一旦确定后续不可变更。比如你生成 AES 密钥时指定了 GCM 模式那 init 阶段就只能在 GCM 参数下使用想换成 CBC只能另起 alias 重新生成。因此密钥属性的设计不能在原生实现里随手写而应该做成一份集中配置和 Dart 层的调用契约一一对应。2.3 安全资产Token、证书、生物认证凭据的存取之道除了加解密sec 库还经常负责保管“安全资产”登录 token、refresh token、API key、推送 token、设备唯一标识甚至用户证书。这些资产有一个共同点丢失或泄露的代价很高但又必须持久化在设备上。Android 上常用 EncryptedSharedPreferencesiOS 上直接塞 Keychain鸿蒙这边的做法是结合 HUKS 和 Asset 服务。一般来说我把安全资产分成两类普通机密资产需要加密存储但不要求每次读取都做用户认证。适合 token、API key、用户偏好里的敏感项。做法是 HUKS 生成一个加密密钥把资产加密后写入持久化存储。受控机密资产必须以生物认证或锁屏密码保护用户认证通过后才能读取。适合支付凭据、通行证、私密证书。做法是 HUKS 生成带“用户认证后才可用”属性的密钥读取时先拉起认证再解密。还有一个设计原则alias 命名不能太随意。不要用my_key这种全应用共用的名字建议按“业务域_账号_用途_版本”组织比如login_token_v1、payment_card_cert_v2。这样后续做密钥轮换、吊销、按账号隔离都会轻松很多。另外要提前想清楚离线场景设备锁定状态下受生物认证保护的密钥能不能操作这取决于 HUKS 的认证有效期配置。业务上需要明确“锁屏后是否允许读取私密内容”的开关不能让用户糊里糊涂地以为锁屏就是安全边界。3. 鸿蒙侧原生实现从 MethodChannel 到 ArkTS 的全链路改造3.1 搭建鸿蒙插件骨架第一步是搞定 Flutter 引擎的插件注册在鸿蒙上跑 Flutter 插件首先要把原生的鸿蒙工程和 Dart 包关联起来。sec 库要新增一个ohos目录在鸿蒙工程里以 hap 的形式打包并注册插件的入口类。这里我不是直接改 Android/iOS 的现有工程而是新建一个鸿蒙模块和原有的 android/ios 目录并列。Dart 侧的通道调用还是老样子比如const _channel MethodChannel(com.app.sec/encrypt); FutureUint8List encryptData({ required Uint8List plainText, required String alias, }) async { final result await _channel.invokeMethod(encryptData, { plainText: plainText, alias: alias, }); return Uint8List.fromList(result as Listint); }ArkTS 侧的插件类我按下面的骨架来写import { FlutterPlugin, FlutterPluginBinding, MethodCallHandler, MethodChannel } from ohos/flutter_plugin; export class SecPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), com.app.sec/encrypt); this.channel.setMethodCallHandler(this); } onDetachedFromEngine(): void { this.channel?.setMethodCallHandler(null); this.channel null; } async onMethodCall(call: MethodCall): Promisevoid { if (call.method encryptData) { const args call.args as Recordstring, Object; // 校验字段后调用 HUKS } else { // 返回 NotImplemented } } }特别注意MethodChannel 的通道名称在 Dart 侧和 ArkTS 侧必须一字不差大小写敏感。通常我会把通道名定义为常量放在一个共享文档里避免两边各写各的。3.2 用 HUKS 替代 Keychain/Keystore密钥生成/导入/使用密钥生成这块Android 的写法大家都很熟val keyGenerator KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore) val spec KeyGenParameterSpec.Builder(login_token_key_v1, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() keyGenerator.init(spec) keyGenerator.generateKey()鸿蒙 HUKS 对应的是属性数组驱动的生成接口import huks from ohos.security.huks; const properties: huks.HuksParam[] [ { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES }, { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT }, { tag: huks.HuksTag.HUKS_TAG_KEY_SIZE, value: 256 }, { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM }, { tag: huks.HuksTag.HUKS_TAG_DIGEST, value: huks.HuksKeyDigest.HUKS_DIGEST_NONE }, ]; await huks.generateKeyItem(login_token_key_v1, properties);再看加解密。JCE 里你拿一个 Cipher 实例init 之后反复 doFinal 就行HUKS 则是显式的会话模型init 开启会话update 喂数据finish 拿结果失败时 abort 收尾。我这里给出一个 AES-GCM 加密的示意流程// 1. init传 GCM 的 IV 和 AAD const initOptions: huks.HuksOptions { properties: [ { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES }, { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT }, { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM }, { tag: huks.HuksTag.HUKS_TAG_PADDING, value: huks.HuksKeyPadding.HUKS_PADDING_NONE }, { tag: huks.HuksTag.HUKS_TAG_NONCE, value: iv }, { tag: huks.HuksTag.HUKS_TAG_AEAD_TAG_SIZE, value: 128 }, { tag: huks.HuksTag.HUKS_TAG_ASSOC_DATA, value: aad ?? sec_default_aad }, ], }; const handle await huks.init(login_token_key_v1, initOptions); // 2. update分段喂明文 const updateOptions: huks.HuksOptions { properties: [], inData: plainText }; const updateResult await huks.update(handle.handle, updateOptions); // 3. finish取密文 const finishOptions: huks.HuksOptions { properties: [], inData: new Uint8Array(0) }; const finishResult await huks.finish(handle.handle, finishOptions);我自己的实测经验是忘掉abort的后果很严重。如果 update 或 finish 过程中出现了异常会话没被 abort下一次对同一 alias 的 init 可能拿到一个残留句柄导致操作卡死。务必在异常路径里主动 abort。3.3 加解密逻辑迁移算法参数、填充模式、编码格式的坑迁移过程中最容易出问题的是“默认值惯性”。很多参数在 Android 和 iOS 上都有各自的默认行为到了鸿蒙如果不显式声明就会得到不一致的结果。我列了一份必查清单AES-GCM确认 tag 长度128 位、IV 长度96 位、是否需要 AAD、密文里 tag 的拼接位置RSA-OAEP确认摘要算法SHA-256、MGF1 摘要算法也建议 SHA-256、明文长度上限RSA-PKCS1确认是 PKCS1 v1.5 还是 OAEP两者完全不兼容Base64 与 HexDart 侧如果统一 Base64原生侧就不要有“返回 hex”的分支PBKDF2 / HKDF密钥派生函数的迭代次数、盐长度、输出密钥长度必须和原有版本完全一致。举一个真实案例。Android 端 sec 库加密返回的 payload 格式是iv(12) ciphertext tag(16)但鸿蒙 HUKS 的 finish 返回的数据里tag 有时直接被拼在密文末尾有时又是一个独立输出取决于 init 时怎么设置 AEAD 参数。我第一版实现没处理 tag直接把 finishResult 当作“完整密文”返回给 Dart结果 Dart 层按原来的格式解包解密出来的内容全乱。这个问题的根源就是我没有把 payload 包装格式当作一份跨端契约来对待。4. 安全资产实战让 Token 和密钥真正“长在鸿蒙里”4.1 关键资产的安全存储类似 Keychain 的鸿蒙 Asset 方案token 和 API key 的存储我第一版用的是“HUKS 密钥 加密后写 Preferences”的方案可以跑通但访问控制粒度比较粗。后来我改用鸿蒙的 Asset 服务发现它更像 iOS Keychain按 alias 增删查改自带多种访问控制策略不需要自己拼装“加密存储”的组合逻辑。Asset 的基本流程是import asset from ohos.security.asset; // 添加资产 const addMap: asset.AssetMap { alias: login_token_v1, valueType: asset.AssetValueType.BYTES, value: new Uint8Array(...), // token 的字节 accessControl: asset.AssetAccessControl.ACCESS_CONTROL_USER_AUTH, accessible: asset.AssetAccessible.ACCESSIBLE_DEVICE_FIRST_UNLOCK, }; await asset.add(addMap); // 查询资产 const queryMap: asset.AssetMap { alias: login_token_v1, returnType: asset.AssetReturnType.ALL, }; const result await asset.query(queryMap); // 删除资产 await asset.remove({ alias: login_token_v1 });选择访问控制级别时我的建议是普通登录 token选择ACCESSIBLE_DEVICE_FIRST_UNLOCK级别设备首次解锁后即可读取支付卡信息、通行证书选择ACCESS_CONTROL_USER_AUTH每次读取都需要用户认证密钥轮换场景不要直接覆盖同一 alias先 add 新 alias再 remove 旧 alias保证旧数据在过渡期还能访问。补充一个容易踩的坑Asset 的 value 类型要求严格不能用普通字符串直接传必须先转成 Uint8Array。另外查询完一定要检查返回对象是否为 null不要假设“加进去了就一定能查出来”。我遇到过一次因为前后两次使用的模块参数不一致导致 query 返回空的问题排查了很久才发现是 alias 写错了一个字符。4.2 生物认证接入指纹/人脸校验的鸿蒙实现与回退逻辑有些业务需要主动拉起生物认证比如“点击查看完整卡号”。这时候 Asset 的自动访问控制不够灵活要用鸿蒙的 UserAuth 模块把认证流程接到业务交互里。核心流程分三步检查能力、发起认证、处理回调。示意代码大概长这样import userAuth from ohos.userAuth; const authParam: userAuth.AuthParam { authType: [userAuth.AuthType.ALL], authTrustLevel: userAuth.AuthTrustLevel.ATL1, }; const widgetParam: userAuth.WidgetParam { title: 验证身份以查看卡号, }; const userAuthInstance userAuth.getUserAuthInstance(authParam, widgetParam); userAuthInstance.on(result, { onResult: (result: userAuth.UserAuthResult) { if (result.result userAuth.UserAuthResultCode.SUCCESS) { // 认证成功继续解密 } else { // 处理失败 } }, }); await userAuthInstance.start();这里我要重点强调回退逻辑。现实情况里用户可能没录入指纹、没人脸、连续失败五次被系统锁定、或者中途点了取消。每一种情况都必须给用户一个可理解的结果而不能只往错误日志里写一行 code。我的做法是认证失败后先判断 result code如果是“用户取消”直接返回静默如果是“设备不可用”或“未录入生物特征”则提供“使用锁屏密码”的回退按钮如果是“失败次数过多”提示用户稍后再试不要无限重试。还有一个时效性问题认证成功后拿到的授权凭证是有有效期的。后台应用在登录态下长时间运行等真正要访问受控密钥时凭证可能已经过期这时候需要重新拉起认证。业务上要能区分“本次认证失败”和“认证凭证过期需要重新认证”否则用户会感觉“明明验证了指纹又让我验一次”。4.3 原生层与 Dart 层的数据隔离明文不过 Dart 堆安全库适配到后期我比较关注的一个问题是明文到底应不应该传回 Dart 层。理想状态下密钥、口令、完整卡号这类数据只在原生层处理Dart 层拿到的只是“解密成功/失败”的布尔值。但在真实业务里很多页面就是要展示明文那我们就需要做妥协。至少要做到这几点能给布尔结果就不传明文比如“能否打开加密数据库”这类判断让原生层返回成功还是失败必须传明文时优先用 Uint8List 而不是 String。Dart 字符串不可变且可能进入常量池而字节数组至少可以尽快覆盖任何日志输出都禁止打印密文、明文、IV、tag不要把 token 在 Dart 层缓存成全局变量每次使用前重新查询用完后立即置空。把 Dart 层想成前台接待大厅原生安全模块是后面的保险库MethodChannel 就是接待员。接待员负责传信封但绝不应该被要求打开信封看看里面是什么。这套思路虽然不能防住内存 dump 级别的攻击但能挡住绝大多数“日志泄露”“缓存拖库”和“备份恢复”的普通风险。5. 适配中我踩过的坑类型映射、线程切换、算法差异5.1 整型溢出与数值类型映射原版没炸我炸了鸿蒙适配过程中我遭遇的第一个隐蔽问题是数字类型精度。Dart 的 int 在 64 位下可以表示很大的整数但 MethodChannel 的 JSON 序列化会把数字变成 double 范围内的 number。当我从一个加密数据结构里取出一个时间戳字段时Dart 侧传过来的值经过 JSON 解析在 ArkTS 里变成了一个浮点数如果直接截断成 32 位整型精度直接丢了几位。这种问题最麻烦的地方是“看代码完全没问题只在特定大数值输入时崩”。我的解决方案是所有可能出现大数的字段Dart 侧直接转成字符串传递所有字节数组统一用 Base64 字符串传递不传 List 所有参数在 ArkTS 侧先做 instanceof / typeof 校验不做就报参数错误。还有一个小坑空字符串和 null 在 MethodChannel 里有时会互相转化。Dart 侧如果传了空字符串ArkTS 这边可能收到 null。我在 saveToken 接口里就遇到过一次“明明传进去空字符串取出来变成 null”的情况。处理办法是在 ArkTS 侧把 null 和空串统一归一化处理。5.2 后台线程与主线程的 Platform 回调陷阱HUKS 的接口几乎都是异步 Promise回调会落在异步线程。而 Flutter 的 MethodChannel 原生侧最终调用result.success()时如果不在正确的线程上下文会出现“时灵时不灵”的情况。我遇到过一类场景加密接口在测试环境跑了一百次都没问题一上真机高并发就偶尔报“callback already invoked”。这个问题根因是回调时序混乱。我的解决方式很简单onMethodCall直接声明为 async 函数里面统一用await等待所有异步操作结束最后再调用result.success()。不要把result作为回调参数传进底层的裸回调里否则一个 call 可能在 init、update、finish 三个回调里各被调用一次第二次开始就报错。为了让问题尽早暴露我还在开发期给每个 call 加了一个completed标记let completed false; function safeSuccess(result: MethodResult, data: Object): void { if (completed) { console.error(duplicate result callback); return; } completed true; result.success(data); }这个保护性写法看着笨但排查并发问题时能帮你快速定位是“哪一段代码重复回调了 result”。5.3 加密结果不一致GCM 的 tag 长度和 AAD 的兼容性适配完成后最痛苦的一关是跨端密文互解。我们当时有个业务同一份数据在 Android 上加密要在鸿蒙上解密展示。一开始我把密钥、IV、AAD 全检查了一遍还是失败最后发现是 tag 拼接位置的问题。具体情况是Android 的 sec 库在 Dart 层把 payload 组成了iv ciphertext tag而鸿蒙 HUKS 的 finish 返回数据里tag 的位置不固定。我在处理时没有把 tag 单独取出来按 Dart 层的约定拼接而是直接把 finish 的原始输出当成整体返回导致 Dart 层解包错位解密出来的数据永远是乱的。另外 AAD 也给我上了一课。AAD 不参与加密输出所以从 payload 里根本看不到它的存在。但只要加密和解密时传递的 AAD 不一致解密就会失败而且错误信息很隐蔽你只看到“解密失败”根本不知道是哪个参数错了。我们最后把 AAD 固定成业务场景字符串并写进了跨端文档才算彻底消掉这一类问题。如果你现在才刚开始适配我强烈建议你先定一个统一的密文包装格式比如magic(4字节) version(1字节) iv(12字节) aadLength(2字节) aad ciphertext tag(16字节)然后让三端都按这个格式打包和拆包。这个格式越早定后面越省心。6. 验证方案与收尾加密正确性、兼容性与回归测试6.1 加解密的黑白盒验证用固定密钥向量做确定性测试很多适配项目死在“手工点一两下就觉得跑通了”。加密这块我强烈建议做固定测试向量的确定性验证。就是准备一组人肉确认过的输入和期望输出比如固定密钥、固定 IV、固定明文、固定 AAD然后断言加密结果必须等于指定 hex。我整理的测试用例至少包含这几类测试对象输入期望AES-256-GCM 加密短文本明文 “hello sec”固定 IV固定 AAD固定 hex 密文AES-256-GCM 空明文空字节固定 IV仅 tag 的合法输出AES-256-GCM 64KB 明文随机字节固定 IV解密后与原文一致RSA-OAEP 短数据128 字节以内的私密数据解密后与原文一致PBKDF2 派生固定口令、盐、迭代次数派生密钥等于固定值这套用例在三个平台上各跑一遍可以非常快地暴露参数默认值不一致的问题。我都把它们做成了flutter test里的集成测试通过 MethodChannel 真实调用原生实现而不是 mock。这样每次改动 DIP 层的 Dart 接口回归成本基本为零。6.2 全平台回归同一份密文能否在 Android/iOS/鸿蒙互相解密比固定向量更贴近实战的验证是跨端交叉解密。特别是业务里有多设备同步、备份恢复这类场景时必须保证Android 加密的密文能在鸿蒙上解开iOS 加密的密文能在 Android 上解开鸿蒙加密的密文其他两端也得认识。交叉测试的前提是“三端用的是同一把密钥”。这里分两种情况密钥各自生成那测试目的是验证“同一套算法参数和 payload 格式下三端都能正确处理自己的密文”并额外比对密文是否一致同一密钥导入到三端那测试目的是验证“同一把密钥在三个平台上能互相解开对方产生的密文”。我在实操中两个都测了。第一种能发现参数不一致第二种能发现密钥导入导出的格式问题。尤其要注意HUKS 里同一 alias 如果调用两次generateKeyItem新密钥会直接覆盖旧密钥所有用旧密钥加密的数据全部作废。所以在密钥轮换测试里我会故意保留旧的 alias 数据来验证“覆盖后旧数据不可读”的预期行为避免线上出现静默解密失败。6.3 性能与异常路径适配完成不等于适配结束功能都通了之后我还跑了一轮性能和异常路径的检查。性能方面对比了 100 次 AES-GCM 加密的耗时和内存峰值鸿蒙原生实现的耗时和 Android 在同一数量级没有出现数量级退化。异常路径方面我专门测了这几种情况密钥不存在时调用解密要返回明确错误而不是抛一个undefined认证被用户取消时Dart 层要拿到可区分的错误码tag 校验失败的密文解密必须失败且不能返回部分明文两个并发调用同时操作同一个 alias不能让 HUKS 会话互相干扰。这些异常路径的返回信息最终都会展示给用户或被上报到平台所以错误文案一定要写清楚。而且日志里绝对不能打印密文和明文这一点我在代码评审里卡得非常死。做完这一整套适配和验证我最大的感受是平台安全能力再强也架不住调用层把密钥当普通字符串到处传。鸿蒙化适配逼着我把 Android、iOS、鸿蒙三端的安全模型重新梳理了一遍这套经验反哺到原有的 Android 和 iOS 代码里也帮我发现了两处原先的容错问题。如果你正打算把一个 Flutter 安全库迁到鸿蒙记住一点先画能力映射表再定密文包装格式最后补交叉验证。三步走完你的适配才算真正落地。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。