尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

React Native鸿蒙版SecureStorage落地实践:HUKS加密与密钥生命周期管理

发布时间:2026/9/29 16:10:28

资讯中心
01
ARTICLE

React Native鸿蒙版SecureStorage落地实践:HUKS加密与密钥生命周期管理

React Native鸿蒙版SecureStorage落地实践:HUKS加密与密钥生命周期管理
把应用适配到鸿蒙之后我遇到的第一个“看似简单”的模块就是用户会话的安全存储。起初我并没太当回事想着无非是AsyncStorage多包一层直到团队准备上架、过安全合规那关要求敏感数据必须加密存储才发现这根本不是“找一个库”能解决的事。iOS 有 KeychainAndroid 有 Keystore鸿蒙也有自己的系统安全能力但生态远没到npm install一个库就能躺平的程度。这篇记录就是我在 React Native 鸿蒙版里落地 SecureStorage 加密存储的全过程包括鸿蒙底层能力选型、原生模块封装、密钥生命周期管理以及我在真机上踩过的一堆坑。1. 为什么鸿蒙版React Native需要一套独立的SecureStorage方案1.1 AsyncStorage和SecureStorage不是同一种东西先说清楚一个容易混淆的概念。很多同学把react-native-async-storage/async-storage当成“安全存储”其实它的定位是轻量级 key-value 持久化底层在 Android 对应 SQLite 或者文件映射在 iOS 对应类似 plist 的实现。它解决的是“数据重启不丢”不解决“数据别人拿不到”。真正意义的 SecureStorage至少要有三个特征存储空间受系统级保护其他应用无法直接读取密钥由系统安全硬件或系统组件托管而不是躺在应用文件里支持按需加密和解密密钥的生命周期与用户身份、设备绑定。iOS 的 Keychain、Android 的 Keystore 都满足这三点。鸿蒙这边对应的能力是 HUKSHarmonyOS Universal KeyStore、Asset Store 以及加解密框架 CryptoFramework。它们之间的职责划分和我们熟悉的 Keychain 与 Keystore 不完全一样如果不花时间梳理清楚后面越写越别扭。1.2 鸿蒙生态里RN开发者的真实处境网上能找到的鸿蒙 RN 示例大多停留在“Hello World 跑通”和“页面布局适配”层面。一旦涉及设备能力调用比如指纹、安全存储、剪贴板官方示例通常给的是 ArkTS 的 API 文档而 RN 侧没有现成封装。你要么去社区找别人做好的模块要么自己动手写一个原生模块。我当时的处境是团队已经有比较成熟的 iOS/Android RN 应用业务上要快速跑通鸿蒙渠道。用户登录态、refresh token、设备唯一标识、API 请求签名密钥这些数据在 iOS 上走 Keychain在 Android 上走 EncryptedSharedPreferences 加 Keystore。到了鸿蒙我翻了现有依赖列表没有一个库能同时满足加密和原生桥接两个要求。于是只能自己做。好消息是鸿蒙的底层安全能力设计得并不差甚至在某些细节上比 Android 传统方案更清晰坏消息是网上能搜到的实践太少很多接口要靠自己试错。这也是我愿意把方案写出来的原因。1.3 我最终选定的技术路线在做选型时我重点考虑了三条路线方案核心思路优点问题A. 直接使用 Asset Store把 token、密钥作为资产交给系统存储实现最简单系统托管查询能力偏“资产管理”不适合大块自定义数据结构B. HUKS CryptoFrameworkHUKS 生成保存 AES 密钥CryptoFramework 做 GCM 加解密密文存 Preferences可控性强密钥不出安全硬件跨端行为可预期需要自己处理密钥生命周期和密文格式C. 纯 CryptoFramework生成随机密钥加密后把密钥也存到本地实现简单密钥一旦泄露等于明文存储安全意义不大我最终选了方案 B。理由是它能较好地兼顾安全性和可控性密钥由 HUKS 托管应用只持有 alias数据用标准 AES-256-GCM 加密后存入普通存储未来业务扩张时可以灵活切换多个 key 版本而不需要依赖 Asset Store 的资产管理语义。Asset Store 更适合“一次性保存密码并让系统帮我们校验”的场景比如密码保险箱而我们的核心诉求是给 React Native 业务层提供一套读写透明的加密存储接口。2. 鸿蒙侧的能力梳理HUKS、CryptoFramework、Preferences怎么配合2.1 HUKS负责“管钥匙”不是“管保险柜”HUKS 的定位和 Android Keystore 很像它负责生成密钥、保存密钥、并用密钥执行加解密、签名、验签等密码学操作。关键特征是密钥一旦生成进入安全硬件或安全系统应用拿不到密钥明文只能通过 alias 引用它。打个比方HUKS 是你家小区的物业可以帮你保管备用钥匙但你自己手里只有一把“钥匙编号”。你告诉物业“用编号 AX39 的钥匙开我家的门”物业替你开但不会把钥匙给你。在鸿蒙 ArkTS 侧生成一个 AES-256 密钥大概长这样import { huks } from kit.UniversalKeystoreKit; const alias com.example.app.secure_store_v1; const genOptions: huks.HuksOptions { properties: [ { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES }, { tag: huks.HuksTag.HUKS_TAG_KEY_SIZE, value: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256 }, { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT }, { 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 }, ], }; await huks.generateKeyItem(alias, genOptions);需要注意HUKS 的 API 在不同 SDK 版本上有细微差别比如从ohos.security.huks迁移到kit.UniversalKeystoreKit后部分枚举名称变了。写模块的时候一定要以你工程里实际的 SDK 版本为准编译报错就查官方 API diff。2.2 CryptoFramework负责实际的加解密HUKS 本身也能完成加解密但我更建议把“加解密运算”交给 CryptoFramework把“密钥管理”交给 HUKS。原因有两个一是 CryptoFramework 的update、doFinal接口更接近 Node.js、Java 里的常规加密写法JavaScript 侧也更容易理解和对接二是后续如果要支持其他算法比如 AES-CBC、SM4CryptoFramework 的切换成本更低。我这里选择的算法是 AES-256-GCM。为什么不用 CBCCBC 只保证机密性不保证完整性攻击者可以把密文翻转之后重放导致解密后得到篡改数据。GCM 在加密的同时生成认证标签任何一位密文被篡改解密时都会直接失败这对存储用户 token 这种“宁可读不出也不能读错”的场景非常关键。加密流程示意import { cryptoFramework } from kit.CryptoArchitectureKit; const generator cryptoFramework.createSymKeyGenerator(AES256); const symKey await generator.generateSymKey(); const cipher cryptoFramework.createCipher(AES256|GCM|NoPadding); const iv { data: cryptoFramework.createRandom().generateRandom(16) }; await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, { iv }); const cipherText await cipher.doFinal(plainText);在实际封装里symKey并不是直接由 CryptoFramework 随机生成而是通过 HUKS 导出的密钥字节来构造的。这样保证密钥的源头在 HUKS业务代码里自然拿不到持久化密钥明文。2.3 Preferences只碰密文不做安全承诺鸿蒙的 Preferences 是一个轻量级 key-value 数据库适合放小体积结构化数据。它本身不提供任何加密和 Android 的 SharedPreferences 一样文件落在应用沙箱内。方案 B 里Preferences 只负责保存加密后的密文、IV、alias、版本号这些元信息真正敏感的原始 token 永远不出现在这里。选择 Preferences 而不是直接存文件主要是为了省去自己做文件读写的麻烦而且 Preferences 支持flush原子落盘批量写入体验也还可以。需要注意它的单 key 大小和总体积限制在不同版本上有差异我在后面避坑章节会详细说。2.4 数据格式设计跨语言桥接最怕各端数据结构不一致所以我从一开始就固定了持久层的数据格式{ v: 1, alg: AES-256-GCM, alias: com.example.app.secure_store_v1, iv: base64字符串, ct: base64字符串 }v是数据版本号后续做密钥轮换和迁移全靠它alg记录算法标识避免将来算法升级后旧数据无法识别alias指向 HUKS 中的密钥iv是每次加密随机生成的初始化向量ct是密文。每次读取时先读出这个 JSON根据v选择对应的解密流程用alias从 HUKS 拿到密钥句柄再对ct做 GCM 解密。这样做的好处是密钥升级时不需要同时迁移所有存量数据每次读取触发懒迁移即可。3. 原生模块封装从ArkTS到JS的桥接3.1 准备一个RN鸿蒙原生模块工程react-native-harmony 的项目结构和 iOS/Android 有些不同通常在工程根目录下有一个harmony子工程里面放着 ArkTS 源码和原生模块配置。我们要做的原生模块是在这个子工程里新增一个独立的SecureStorage模块。我习惯把原生模块的职责控制在“薄薄一层”只提供saveItem、getItem、removeItem三个方法不做业务判断不做重试逻辑。业务侧可以在 JS 层再包一层封装统一处理异常和缓存。原生层一旦参与了过多业务逻辑后面调试和复用都会很痛苦。如果你在 openharmony 社区里下载过 RN 鸿蒙模板工程会发现Index.ets里有很多示例模块。新加模块时注意不要直接改它而是单独建目录比如harmony/secure_storage/src/main/ets/SecureStorageModule.ets这样后续升级模板时冲突少。3.2 ArkTS侧实现SecureStorage核心类核心类我命名为SecureStorageModule它负责调用 HUKS 生成或获取 alias 对应的密钥导出密钥字节并交给 CryptoFramework 构造对称密钥执行 AES-256-GCM 加密/解密把密文 JSON 写入 Preferences。一个关键的实现细节是HUKS 默认生成的密钥可能是“可导出”的。exportKeyItem真的会把密钥字节返回给调用者所以在完成密钥导出并交给 CryptoFramework 之后内存里的密钥字节要尽快清空。ArkTS 里没有像 Swift 那样的SecureEnclave内存保护我们至少要做到不在日志里打印密钥不做全局缓存。加密方法的大致骨架async encrypt(alias: string, plainText: string): Promisestring { // 1. 确保 HUKS 中已存在 alias 对应的 AES 密钥 await this.ensureKey(alias); // 2. 生成随机 16 字节 IV const iv cryptoFramework.createRandom().generateRandom(16); // 3. 从 HUKS 导出密钥并构造 SymKey const keyData await this.exportKeyFromHuks(alias); const symKey await this.buildSymKey(keyData); // 4. 创建 AES-GCM 密码器并加密 const cipher cryptoFramework.createCipher(AES256|GCM|NoPadding); await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, { iv }); const cipherText await cipher.doFinal(plainText); // 5. 组装持久化结构 return JSON.stringify({ v: 1, alg: AES-256-GCM, alias, iv: this.toBase64(iv), ct: this.toBase64(cipherText) }); }ensureKey这一步我单独抽出来是因为要处理“密钥已经存在”和“密钥不存在”两种情况。反复调用generateKeyItem会报错所以我通常先尝试getKeyItemProperties如果拿不到再生成。3.3 注册TurboModule并设计JS APIRN 鸿蒙版同样支持 TurboModule 机制。注册方式大致是在Index.ets的RNPackage实现里把SecureStorageModule加入创建列表然后 JS 侧就可以通过NativeModules.SecureStorage访问。JS 侧 API 设计我遵循了react-native-keychain的习惯// SecureStorage.ts import { NativeModules } from react-native; const { SecureStorage } NativeModules; export async function saveItem(key: string, value: string): Promisevoid { try { await SecureStorage.saveItem(key, value); } catch (e) { // 统一抛业务异常或者做降级处理 } } export async function getItem(key: string): Promisestring | null { return SecureStorage.getItem(key); } export async function removeItem(key: string): Promisevoid { return SecureStorage.removeItem(key); }每个业务 key 在原生层都会映射成一个 Preferences key而这个 Preferences key 的 value 就是我们上面定义的密文 JSON 结构。业务侧不需要知道任何加密细节这样将来即使要替换底层库JS 接口也可以保持不变。3.4 线程模型问题加密解密虽然不算特别慢但一次doFinal涉及安全芯片或系统服务调用如果直接同步执行会卡住调用线程。在 RN 里原生模块如果跑在 JS 线程同步方法会直接影响 UI 响应速度。我建议所有原生方法都设计成异步 Promise 风格。ArkTS 侧的方法返回Promisestring而不是同步返回字符串。RN 桥接层会自动把 Promise 转换为 JS 侧的 Promise异步操作不会阻塞 JS 线程。这里有一个坑如果你在模块里用了TaskPool或者Worker线程执行加解密需要确保 HUKS 的调用线程上下文正确。HUKS 并不强制要求主线程但部分安全硬件访问逻辑和线程绑定有关遇到偶发错误时先试试回退到模块默认线程。4. 密钥生命周期与数据迁移最容易翻车的三个场景4.1 卸载重装后已存数据为什么突然解不开HUKS 密钥的生命周期与应用绑定应用卸载后默认情况下密钥会被系统删除。这意味着你在卸载前用 HUKS 密钥加密的存量数据重装后会解不开。这不是 bug而是安全设计——防止卸载重装后旧数据在新应用身份下被访问。业务上怎么应对分两类看如果是用户登录态常见做法是接受“重装后需要重新登录”。refresh token 的有效期有限被清掉反而更安全。如果是业务需要保留的数据比如用户手动输入的加密备份、离线凭证就需要引入应用级恢复方案比如把密钥用用户口令派生出来不依赖 HUKS 的密钥存活。我在项目里采用了折中方案短期会话 token 直接依赖 HUKS长期可恢复数据比如用户绑定的一些非敏感设置走设备唯一 ID 加密钥派生避免卸载就永久丢失。这个取舍一定要提前和产品对齐不要等用户反馈“更新 App 之后所有数据都没了”再补救。4.2 别把alias当成随便一个字符串alias是 HUKS 中区分不同密钥的唯一标识。我见过有人直接用session_key这种字符串结果多个环境共用一个 alias测试环境覆盖了生产环境的密钥。建议分环境、分版本设计 alias 规则{bundleId}.secure_store.{environment}.v{version}比如com.example.app.secure_store.staging.v1com.example.app.secure_store.prod.v1版本号放进 alias还有一个额外好处密钥轮换时老数据可以继续用老 alias 解密新数据用新 alias 加密等老数据自然过期后再清理掉而不是一次迁移全部数据。4.3 版本升级和数据迁移一旦把v和alias版本化数据迁移就变成一个可控过程。比如密钥从 v1 升级到 v2我会这样做新写入的数据全部用secure_store.prod.v2加密读取数据时发现v1就先用 v1 密钥解密再用 v2 密钥重新加密写回一段时间后确认存量数据迁移完成再从 Habits 里删除 v1 密钥。这种懒迁移策略在移动端很实用不需要用户升级后触发一次全量重写。当然前提是 v1 和 v2 的密钥在迁移期间必须同时存在于 HUKS 中别急着删旧密钥。5. 真机调试避坑实录五个印象深刻的问题5.1 模拟器正常真机GCM报错第一次在模拟器上跑通时我心里还挺得意结果真机一跑就崩错误信息是 GCM 解密时 tag 校验失败。排查了很久最终定位到是两个环境对“空数据”和“认证标签”的处理不同。在模拟器上某些版本对 GCM 的doFinal做了宽容处理数据长度不够时会自动补零真机安全实现更严格数据长度和 IV 长度不匹配就直接抛错。解决方法是加密时严格生成 16 字节 IV解密时明确校验密文长度是 16 的倍数并且包含完整的 GCM tag。后来我养成一个习惯任何安全相关功能第一天就在真机全流程跑一遍不要在模拟器上自我感动。5.2 IV写死导致的“安全漏洞”这是我看到团队里新同学最容易犯的错写 IV 时觉得“反正每次都用同一个也能加密”就写了个常量。如果不使用 GCMAES-CBC 模式下 IV 固定最多是算法强度降低但在 GCM 模式下固定 IV 遇上同一个密钥直接可以导致密钥流重用攻击者能在不知道密钥的情况下恢复明文或伪造密文。正确做法很简单每次加密都生成新的随机 IV并把 IV 和密文一起保存。我封装时把 IV 生成封装到了原生层JS 侧连传入 IV 的机会都不给从源头堵住这个隐患。5.3 长度字段和Base64变形数据跨过 RN 桥接层字符串编码转换是一个容易被忽略的细节。ArkTS 侧的doFinal返回的是Uint8Array转成 base64 字符串再给 JS 侧读取JS 侧回传时也是 base64 字符串。中间任何一处把二进制当成 UTF-8 处理解密出来的数据就是乱码或者空串。我建议在原生层统一封装 base64 编解码不要依赖 JS 层的btoa和atob因为 RN 不同平台对这些全局函数的支持不一致。另外base64 会把数据膨胀约 33%一个 1KB 的密文实际存下来大约 1.4KB设计 Preferences key 大小上限时要把这个膨胀考虑进去。5.4 存储压力测试把Preferences打爆Preferences 不是设计用来存大对象和大量 key 的。早期版本我们为了调试方便把每次网络请求的签名结果都存了一份几个月后用户在低配设备上出现写入失败。后来我在模块里做了两层保护单 value 超过 16KB 时直接拒绝写入并抛异常总 key 数超过 100 时强制要求业务侧做清理。实际业务里SecureStorage 只适合存 token、密钥、短文本。如果你要存大 JSON应该考虑用数据库或文件加密方案而不是硬塞进这里。5.5 权限/模块没有加载导致Module null有时明明代码写对了JS 侧NativeModules.SecureStorage却是null。常见原因是 native 模块没有在Index.ets里注册或者模块包没有打进最后的 hap 里。排查步骤很简单先确认构建产物里是否包含对应模块的 so 或 ArkTS 层代码在原生模块初始化时打一条日志确认模块被加载JS 侧加一个防御性判断为null时抛一个可读错误别在访问属性时才报 TypeError。我们的 CI 配置里还会额外跑一条命令检查产物中是否存在模块注册文件避免有人改了目录结构但忘记改配置文件把问题留到发布后才发现。6. 启动白屏问题加密存储如何影响首屏体验6.1 白屏不一定是渲染问题很多人遇到 RN 鸿蒙启动白屏第一反应是页面布局或者 bundle 加载问题我却曾经被“SecureStorage 导致白屏”坑过。原因是首屏有一个逻辑要读取用户登录态而这个读取被写成了同步阻塞调用或者是一个没有超时控制的 await底层在做 HUKS 加解密时把启动链路卡住了。在低端鸿蒙设备上一次密钥初始化和解密可能要几十毫秒到上百毫秒如果首屏刚好依赖这个结果才能决定跳转那么这段时间内用户看到的就是白屏。6.2 把解密从启动路径里挪走首屏需要登录态才能决定跳转这是业务强需求不能去掉。但我们可以把顺序优化一下启动后先进一个极简的 Splash 页面在 Splash 显示期间异步读取 SecureStorage拿到结果后再决定进入登录页还是主页。这样即使解密耗时 100ms用户看到的是 Splash 而不是白屏体感上就正常了。更进一步可以把“最近一次登录态”用一个普通但非敏感的标记字段存在内存缓存里启动时先展示对应页面骨架等 SecureStorage 解密完成后做最终路由校正。6.3 内存缓存与后台续期在内存里维护一个 session 缓存也是减少启动开销的关键。我的做法是冷启动时只从 SecureStorage 读一次解密后放入内存单例页面切换时直接读内存不再走原生桥接refresh token 到期前由后台任务提前续期并更新 SecureStorage。双写策略也有讲究更新 token 时先更新内存再异步写 SecureStorage避免每次接口调用都阻塞在安全存储写盘上。万一内存和 SecureStorage 不一致以 SecureStorage 的刷新时间为准做校正。6.4 性能实测结果最后给一组我们项目里的实测数据设备是某款中端鸿蒙机型SDK 版本为 API 12 左右操作平均耗时备注HUKS 密钥生成180ms只在首次安装时执行一次首次加密 1KB 数据45ms包含密钥导出和初始化首次解密 1KB 数据38ms冷启动链路二次解密 1KB 数据12ms受系统缓存影响Preferences 写入10ms不包含底层 flush实际冷启动影响在优化成异步读取后从用户点击图标到 Splash 结束整体增加约 60~80ms基本无感。相比安全合规的收益这个代价完全可以接受。写到最后分享一点我的个人体会。SecureStorage 这套东西真正的复杂度不在加密算法本身而在密钥的生命周期设计和工程化接入方式。选 HUKS 加 CryptoFramework 加 Preferences 的组合比 Android 上 EncryptedSharedPreferences 的思想更统一也更接近 iOS Keychain 的职责划分。如果你也在适配鸿蒙我的建议是先别急着找现成封装库花一个下午把 HUKS、Asset Store、CryptoFramework 这三个能力边界搞清楚。踩过一轮坑之后你对安全存储的理解会比单纯“会调一个接口”扎实得多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。