最近在做 Flutter for OpenHarmony 方向的项目,碰到一个很实际的需求:要在本地保存一批敏感数据,比如第三方账号密码、API Key。直接明文存肯定不行,写到系统文件里又担心被其他应用翻出来。于是我把目光放到了 cryptography_flutter 这个第三方库,顺手用一周左右的时间做了一个安全密码管理器。这趟折腾下来,踩了不少 OpenHarmony 适配的坑,也把 AES-GCM、PBKDF2 这些加密概念在真实项目里跑通了一遍。如果你也在 Flutter for OpenHarmony 这条路上做本地数据加密,这篇文章应该能帮你少走很多弯路。
写这篇内容的主要对象是两类人:一是已经在 OpenHarmony 上跑 Flutter 应用、想给敏感数据加一层密码保护的开发者;二是对 cryptography_flutter 感兴趣,想知道它在非标准 Android、iOS 环境下到底能不能用的朋友。文章里不会堆理论,我会直接讲我是怎么选型、怎么设计数据模型、怎么写加密逻辑、以及最后上线前做了哪些安全加固。
1. 为什么在 OpenHarmony 上做密码管理器:需求与选型思考
1.1 项目背景:一次跨端开发遇到的“加密”难题
我手上的应用是一个跨端效率工具,原本只跑了 Android 和 iOS,最近要求适配 OpenHarmony。这个应用有一个“账号保险箱”功能,用户把自己的社交账号、支付平台、工作凭证等信息填进去,期望下次打开时还能看到。但这些数据敏感程度很高,如果直接以明文形式落在沙箱目录里,一旦设备被 root、备份被提取或者应用被调试,数据就裸奔了。
当时我面临三个选择:第一是自己用纯 Dart 写一套加密逻辑,不依赖任何原生代码,平台兼容性最好;第二是直接调 OpenHarmony 系统提供的密码学接口,通过通道桥接调用,性能好但代码侵入性强;第三是找一个现成的 Flutter 加密库,再看它能不能在 OpenHarmony 上跑起来。我这人比较务实,优先试了第三方案,毕竟能复用社区的踩坑经验就用社区的。最后锁定了 cryptography_flutter。
1.2 第三方库选型对比:cryptography_flutter 凭什么胜出
在 Dart 生态里做加密,其实绕不开 cryptography 这个底层包,cryptography_flutter 是它的 Flutter 插件版本。我当时对比了几个方案,列个表看得更清楚。
| 方案 | 实现方式 | OpenHarmony 适配难度 | 是否推荐 |
|---|---|---|---|
| 纯 Dart 手写 AES | 纯 Dart | 低 | 不推荐,安全性难保证,易出错 |
| 调系统密码学接口 | 原生通道 | 高,每个平台都要写 | 中,适合性能要求极高场景 |
| cryptography 纯包 | Dart 实现 | 低 | 推荐,基础够用 |
| cryptography_flutter | Dart + 平台密钥链 | 中,但可降级 | 最推荐,兼顾安全性与易用性 |
cryptography 包的核心优势是:它把算法实现封装成 Dart 类,AES、ChaCha20、RSA 等主流密码算法都有。cryptography_flutter 在其上增加了对系统密钥链的访问能力,在 Android 上可以借用 Keystore 的一部分能力,在 iOS 上会用 Keychain。到了 OpenHarmony 这边,由于系统层接口和 Android 不完全一致,这个插件有一部分能力会退回纯 Dart 实现。我在实际项目中恰恰利用了这一点:加密算法本身不依赖原生,唯一可能受影响的是密钥存储部分,我通过自研的存储方案绕开了。
所以选型结论很明确:如果你的目标是“先跑通,再加固”,cryptography_flutter 是当前 OpenHarmony 生态里性价比最高的选择。
1.3 密码管理器的核心架构设计
动手写代码之前,我把整个密码管理器的架构画成了四层。最底层是数据持久化,我用 shared_preferences 保存加密后的字符串,避免引入数据库依赖。倒数第二层是加密引擎,由 cryptography_flutter 提供,负责所有 AES 和 PBKDF2 运算。第三层是数据模型层,负责把账号密码记录序列化成 JSON。最上层是 UI 层,只负责展示和解密后数据的临时持有。
这四层各干各的,有一个关键原则:UI 层永远不直接碰原始密码。用户在添加一条记录时,页面把明文交给加密引擎,加密引擎立刻返回密文,页面手里只有密文。只有用户主动查看某一条记录时,才临时解密出来,并且当一个页面退出后,我已经把内存里的明文清掉了。关于这点,后面我会详谈 Flutter 里怎么做内存清理。
2. 环境准备:Flutter for OpenHarmony 开发环境搭建
2.1 工具链配置与设备连接
OpenHarmony 上的 Flutter 开发和传统 Flutter 开发有一个明显区别:你不能直接用安卓 SDK 那套构建工具,需要先配置 OpenHarmony 的 SDK 以及对应的 Flutter 交叉编译分支。我当时在开发机上同时装了标准 Flutter 和社区维护的 OpenHarmony Flutter SDK,用环境变量去做切换。
这一步最需要耐心的是设备连接。OpenHarmony 设备默认不开开发者调试模式,我手上的测试机是一块基于某厂商开发板的平板设备,需要在系统设置里连续点击“版本号”七次,然后在开发者选项里打开“USB 调试”和“无线调试”。每次连接完都要用命令行去验证设备列表,我习惯把这些命令写成一个脚本,方便切换设备后快速检查环境。
flutter devices如果能看到设备名且前面有 OpenHarmony 标识,就说明底座没问题。如果只能看到电脑本地,多半是驱动没装好,或者是开发者模式没开干净。我遇到过几次死活连不上的情况,后来发现是数据线不支持传输,只支持充电,换了一根线就正常了,这个坑可以提前避开。
2.2 依赖声明与 cryptography_flutter 包引入
环境准备好之后,接下来就是往 pubspec.yaml 里加依赖。我是这样加的:
dependencies: flutter: sdk: flutter cryptography_flutter: ^3.1.3 shared_preferences: ^2.2.2 crypto: ^3.0.3为什么还加了一个 crypto 包?因为我在做 PBKDF2 派生时,需要先对主密码做一次散列计算,crypto 包提供 SHA-256 这几个基础哈希函数,配合 cryptography_flutter 用起来很方便。实际跑下来,这两个包在 OpenHarmony 上都能正常编译,没有出现原生插件缺失的问题。
执行完 flutter pub get 后,再直接跑一次 flutter build 做预热编译。如果你在构建阶段遇到报错,先不要慌,八成是 OpenHarmony Flutter SDK 的版本和依赖不匹配,我后文会在常见问题一节里说明处理办法。
2.3 OpenHarmony 上的插件兼容性说明
说一个让我比较意外的情况:cryptography_flutter 这个包在设计上是为 Android 和 iOS 提供平台密钥链能力的,在 OpenHarmony 上,它并不会因为这些平台接口不存在就拒绝运行,而是自动落回纯 Dart 实现。我用代码探查了一下它的插件注册逻辑,发现它用了一些条件导入的技巧,也就是在支持的平台上去注册原生通道,在不支持的平台上只加载 Dart 侧的算法代码。
这个设计给 OpenHarmony 开发者留了一条活路。但代价是系统级密钥存储的能力没了,也就是说,你不能像在 Android 上那样让密钥直接存在 Keystore 里。我的处理方式是自己做一层密钥保护,把主密钥放在一个经过二次加密的配置文件中,具体做法在核心加密逻辑实现一节里展开。你如果也是 OpenHarmony 开发,记住这句话:库能用,但别对系统密钥链功能抱太大期望,自己补一层安全设计更靠谱。
3. 核心加密逻辑实现:从密钥派生到数据落盘
3.1 数据模型设计:账号条目如何存储
一个密码管理器存储的核心单位是账密条目。我的数据模型设计得比较简单,只解决了“能用”和“方便加密”这个问题。
class AccountEntry { final String id; final String title; final String username; final String password; final String note; final DateTime updatedAt; Map<String, dynamic> toJson() { return { 'id': id, 'title': title, 'username': username, 'password': password, 'note': note, 'updatedAt': updatedAt.millisecondsSinceEpoch, }; } factory AccountEntry.fromJson(Map<String, dynamic> json) { return AccountEntry( id: json['id'] as String, title: json['title'] as String, username: json['username'] as String, password: json['password'] as String, note: json['note'] as String, updatedAt: DateTime.fromMillisecondsSinceEpoch(json['updatedAt'] as int), ); } }这个模型里 password 是明文。但在我的设计里,它只存在内存中的瞬时模型,一旦要持久化到本地,我会先把它序列化成 JSON,再对整个 JSON 字符串做加密。也就是说,持久层拿到的永远是一段密文。你千万不要试图用 SQLite 的字段级加密思路去做,那会让加密逻辑散落得到处都是,最后哪里漏了都不知道。
3.2 主密码与密钥派生方案(PBKDF2 参数怎么定)
密码管理器不能没有主密码。用户设置一个主密码之后,我们需要把这个人类可记忆的字符串,扩展成一把适合 AES-256 使用的密钥。我选择的是 PBKDF2 算法,这是目前 dart 生态里最成熟的支持方案。
PBKDF2 的关键参数有三个:盐值、迭代次数、输出密钥长度。我用的参数是 16 字节随机盐、迭代 500000 次、输出 32 字节密钥。有人会问迭代 50 万次会不会太慢?在 OpenHarmony 平板上,我实测一次派生大约耗时 300 毫秒到 500 毫秒,这个时间对用户体验来说还是可以接受的。
final pbkdf2 = Pbkdf2( macAlgorithm: Hmac.sha256(), iterations: 500000, bits: 256, ); final secretKey = await pbkdf2.deriveKey( secretKey: SecretKey(utf8.encode(masterPassword)), nonce: salt, // 注意:pbkdf2中这个参数是盐 );这里有一个非常重要的常识:每次生成密钥时必须使用不同的随机盐。盐值不需要保密,甚至可以放在密文的前缀里。它的作用是让同一个主密码在不同设备、不同初始化时间下派生出不同的密钥,这样攻击者即使拿到数据库,也无法通过预计算彩虹表来批量破解。
整个 App 首次启动时,我会生成一个随机盐,然后提示用户设置主密码。主密码经过 PBKDF2 派生后得到主密钥,这个主密钥只用来加密真正用来加密账密数据的“数据密钥”。这种密钥层级结构我强烈建议你照抄,因为它是密码学里常见的“两级密钥”体系,能有效限制单个密钥的使用范围。
3.3 加密与解密流程:AES-GCM 实操代码
加密算法我选的是 AES-GCM,而不是单纯的 AES-CBC。原因很简单:GCM 模式自带认证能力,它不仅能加密数据,还能检测数据有没有被篡改。对于密码管理器这种安全要求高的场景,防篡改和防泄漏同样重要。
下面是 crypto 引擎封装类的核心代码。
import 'dart:convert'; import 'dart:math'; import 'package:cryptography/cryptography.dart'; class CryptoEngine { final SecretKey dataKey; final Random _random = Random.secure(); CryptoEngine(this.dataKey); Future<String> encrypt(String plaintext) async { final algorithm = AesGcm.with256bits(); final iv = algorithm.newNonce(); final encrypted = await algorithm.encrypt( utf8.encode(plaintext), secretKey: dataKey, nonce: iv, ); final combined = [...iv, ...encrypted.concatenation()]; return base64Encode(combined); } Future<String?> decrypt(String ciphertext) async { try { final raw = base64Decode(ciphertext); final algorithm = AesGcm.with256bits(); final ivLength = algorithm.nonceLength; final iv = raw.sublist(0, ivLength); final encryptedBytes = raw.sublist(ivLength); final decrypted = await algorithm.decrypt( encryptedBytes, secretKey: dataKey, nonce: iv, ); return utf8.decode(decrypted); } catch (e) { return null; } } }注意,我在密文里拼上了 IV(初始化向量)。IV 的作用和盐类似,它保证“相同明文加密两次得到不同密文”,防止攻击者从密文相似性中推断出信息。GCM 模式的安全性有一个铁律:绝对不能用同一个 IV + 同一把密钥加密两份不同数据。我的代码里每次都通过 random.secure() 生成新 IV,就是为了规避这个风险。
解密失败时,decrypt 返回 null 而不是抛异常。这是有意为之,因为密码管理器在 UI 层需要向用户展示“密码可能错了”的反馈,而不是直接把栈信息甩到用户脸上,这对安全产品来说非常重要,因为异常信息也是攻击者的情报源。
3.4 自动锁定与会话有效期:安全体验的平衡
光有静态加密还不够。用户把密码管理器打开挂在那里,时间久了,设备被别人拿起来就能看,这不行。所以我做了一个自动锁定机制:应用进入后台超过 60 秒,再次回到前台时必须重新输入主密码。
实现思路很简单,用 WidgetsBindingObserver 监听 App 生命周期。
class AppLifecycleObserver extends WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { _lastBackgroundTime = DateTime.now(); } if (state == AppLifecycleState.resumed) { if (_lastBackgroundTime != null && DateTime.now().difference(_lastBackgroundTime!).inSeconds > 60) { _lockVault(); } } } }这个设计的核心思路是会话状态分层。主密码派生出来的密钥不应该一直躺在内存里,它只在用户解锁后到锁定前的“会话期间”存在。每次解锁重新派生,相当于给攻击者增加一堵墙:即使拿到了加密数据库,也必须在内存中找到那把已经销毁的密钥,难度会高很多。
4. 持久化与整体安全策略:不能只靠一把锁
4.1 本地存储选型:SharedPreferences 与文件方案的取舍
之前提到我用 shared_preferences 来存密文。有人可能会问,为什么不直接用文件存 Key-Value?这里面有个实际原因:shared_preferences 在 OpenHarmony 上已经被社区适配好了,它对键值对类型的数据做了直接映射,我在编码侧不需要写一堆文件 IO 代码。而且它自动处理了崩溃恢复问题,减少了我处理数据损坏的工作量。
但我必须提醒你,shared_preferences 不是安全存储。它在某些平台上会以明文 XML 或 JSON 的形式暴露在应用沙箱里,如果攻击者拿到了应用的沙箱目录,这些数据是可以直接读取的。因此我存进去的内容必须已经是密文,绝对不能放任何明文密钥。另外,我建议把 shared_preferences 设置成只用于存储少量配置和密文索引,大批量的账密数据最好还是放到单独的文件里,用 File 类去管理,方便将来迁移到加密数据库。
在我的项目里,两种方案是混着用的:用户配置、自动锁时间这些非敏感数据走 shared_preferences;账密数据条目集合走一个本地文件。文件的写入我做了版本号,将来升级格式时就可以做迁移。
4.2 盐值与初始化向量管理:让密文更安全
盐和 IV 这些辅助数据,有个存储原则:它们公开,但不可预测。公开意味着你可以直接放在密文的前面、数据库字段里,甚至写死在配置里;不可预测意味着它必须来自密码学安全的随机源,不能用时间戳、自增 ID 这种可预测的值。
我在设计存储格式时,给每一条账密数据加了一个“版本头”:
[1字节版本号][12字节IV][密文]版本号让未来的算法升级成为可能。比如我现在用的是 AES-GCM,如果将来 OpenHarmony 环境里出现了性能更好、安全性更高的算法,我可以直接添加一个新版本号进来,旧数据仍然可以正常解密,新数据用新算法,这样就不会破坏用户的既有数据。
关于盐值,我单独建了一个配置项保存,它其实也是随机数,只是生命周期比较长,是整个密码库级别的。每次修改主密码时才需要重新生成盐并重新派生密钥,平时它不会变化。
4.3 威胁模型分析:在 OpenHarmony 设备上防什么
做安全功能最怕的一件事是“无差别恐慌”,什么攻击都防,最后一团糟。我在设计时专门做了一次威胁模型分析,只针对 OpenHarmony 平板的真实风险去做防御。
常见风险有三类:第一类是应用沙箱被直接读取,比如通过 adb 备份、root 权限访问应用数据目录,这类攻击的直接目标是密文,防御手段是强加密和密钥分离。第二类是动态调试,攻击者用调试器附加到应用进程,从内存里翻出明文,防御手段是自动锁和及时擦除内存。第三类是数据篡改,攻击者恶意修改存储文件,诱导应用解密出错,防御手段是 GCM 自带的认证校验。
这三类风险在我的方案里都有对应的防线。但有一类风险我没有做太多防御,那就是用户在已 root 环境下被记录键盘输入。这一类问题需要在系统层面解决,不是应用层一个密码管理器能防住的。边界想得越清楚,代码就不会乱。
5. 界面与交互落地:一个可用的密码列表页
5.1 列表页与加密数据加载
密码管理器的界面并不复杂,核心是一个列表页和编辑页。列表页加载时,我先从本地文件把所有密文读进来,然后验证主密码,成功后解密全部条目放入内存。你可能觉得一次解密全部条目太浪费性能,但我实测 100 条以下的账密数据在 OpenHarmony 上解密总耗时不超过半秒,体验完全 OK。数据量再大的话,建议优化成分页解密或懒加载。
列表页的 UI 我只展示标题和用户名,密码字段始终用圆点占位。点击某一条时,进入详情页,用户需要再次输入一次主密码才能看到明文密码。这个“二次验证”可能有点繁琐,但确实能防止“手机借给人看一眼、密码顺手被看到”的尴尬情况。如果你觉得每次都要输主密码太麻烦,可以改成指纹验证,但 OpenHarmony 的指纹能力在 Flutter 侧支持还不够成熟,我目前没有接入。
5.2 新增与编辑条目的交互闭环
新增条目时,用户填写用户名、密码、备注,点击保存之后,页面先把数据封装成 AccountEntry,再调用 CryptoEngine.encrypt 加密整个 JSON,最后把密文追加到本地文件。整个流程中,明文密码只在内存模型里存在,一旦加密完成,我立刻把临时变量置空。
var entry = AccountEntry( id: DateTime.now().microsecondsSinceEpoch.toString(), title: titleController.text, username: usernameController.text, password: passwordController.text, note: noteController.text, updatedAt: DateTime.now(), ); final jsonStr = jsonEncode(entry.toJson()); final cipherText = await _cryptoEngine.encrypt(jsonStr); await _entryRepository.appendEntry(cipherText); // 内存中的明文引用立即清除 entry = AccountEntry( id: entry.id, title: entry.title, username: entry.username, password: '', note: entry.note, updatedAt: entry.updatedAt, );编辑条目的逻辑和新增类似,只不过多了一步:先解密出旧数据,把修改后的字段回填进去,再重新加密覆盖保存。注意,修改后必须生成新的 IV,因为数据变了,再使用旧 IV 会造成安全隐患。
5.3 无感知解密:用户体验的关键细节
这里要强调一个容易被忽略的点:解密过程是异步的,如果你在 UI 层把 await 阻塞在 build 方法里,界面就会卡住。我的做法是在 FutureBuilder 里做异步解密,解密完成后才渲染详情内容。这样用户点击条目后会看到一个极短暂加载动画,但整体交互仍然是连贯的。
另外,Flutter 的垃圾回收并不保证对象立即被释放,所以我在详情页退出时手动把控制器、解密字符串等敏感变量的引用置空。虽然这个动作不能保证物理内存立刻被擦除,但至少降低了对象被 GC 保留的概率。做安全应用的人,心里要时刻装着“内存里少留一秒是一秒”的原则。
6. 常见问题与排查实录
6.1 OpenHarmony 模拟器上原生插件不可用怎么办
我一开始用的是模拟器调试,遇到的一个老问题是:模拟器上某些原生能力缺失,Flutter 应用启动后找不到插件,然后直接白屏。这个问题大概率是 OpenHarmony 模拟器对 Flutter 引擎的支持不完整导致的,尤其是在 JIT 模式下加载插件容易出现通道初始化失败。
解决方法有两个:一是换真机调试,OpenHarmony 的真机体验比模拟器好很多;二是确认插件注册代码有没有被正确执行,检查 MainActivity 里是否生成了插件注册表。我排查时发现,社区维护的 Flutter OpenHarmony SDK 在自动注册插件时会有遗漏,手动在启动代码里调用 ensureInitialized 可以解决。
注意:任何一个 Flutter 插件在 OpenHarmony 上都可能需要单独做适配,如果某个库多了原生代码,一定要提前查一下它是否支持 openharmony 分支,否则就会卡在编译阶段。
6.2 解密失败与密钥不一致问题
在开发过程中,我最常碰到的解密失败原因是:盐值或密钥没有保存好。由于 App 每次启动都会从配置里读取盐值,如果盐值丢失或者读取顺序错了,密钥就会不一致,所有数据都解不开。如果你遇到“之前存的数据突然解密失败”,优先检查配置项里的盐是否发生改变。
另外,我遇到过 SharedPreferences 的缓存问题。应用更新版本后,旧数据缓存可能还留在本地,新版本读取时因为格式不兼容直接报错。解决方法是给本地存储的键名加上版本前缀,比如 vault_data_v1、vault_data_v2,避免新旧版本之间串数据。
6.3 性能优化与数据迁移要点
密码管理器数据量一般不大,不过我在压测时发现一个问题:当账密条目达到 500 条时,一次性解密会导致短暂卡顿。这个问题的根源不是解密慢,而是 JSON 反序列化和列表重建开销大。我把内存缓存改成 LRU 结构,只保留最近查看的 30 条明文数据,其他条目保持密文状态,列表展示只渲染标题字段。这个优化让列表页滚动变得更加流畅。
关于数据迁移,最实用的建议是:在设置页提供“导出加密备份”功能,导出的文件以 JSON 格式存储,但整体仍然使用用户的公钥或主密码派生密钥加密。这样用户换机时可以直接拿备份文件恢复,不需要重新录入所有账密。我在实现时给备份文件加了完整性校验字段,恢复时会先验证 MAC,防止备份文件被篡改。
还有一个数据安全技巧:如果你打算清理用户数据,不要只删除可见文件,还要把可能残留的临时文件后缀一致删除。密文虽然不怕被读取,但清理干净是对用户隐私的基本尊重。
在我实际测试中,OpenHarmony 上的 Flutter 应用在调试模式下性能会明显下降,尤其是大量异步加密调用的时候。发布 release 版本后,性能会好很多。如果你在开发过程中感觉卡顿,先别急着优化算法,release 模式再测一遍往往就顺了。
最后再分享一个小技巧:开发这类安全应用时,我会在代码里埋一个“自毁开关”——当检测到应用签名发生过变化时,自动触发安全锁定,提示用户重新验明身份。这在 OpenHarmony 设备上尤其有用,因为它能有效对抗从非官方渠道安装的篡改包,避免用户数据被恶意版本读取。密码管理器这类工具,核心不是把每一个功能做到多炫,而是让你在本地数据真正暴露威胁时,每一层防御都能撑得住。整趟折腾下来,我对 cryptography_flutter 在 OpenHarmony 上的表现挺满意,也希望这些踩坑记录对你的项目有帮助。