开发工具: 华为云码道

本文配套仓库: 上游 m-abdulmonem/file-encryptor;鸿蒙适配改动位于本地仓库的 ohos/example/ohos/README.OpenHarmony_CN.mdREADME.OpenHarmony.mdCHANGELOG.OpenHarmony.mddocs/ohos-test/
鸿蒙适配后仓库https://atomgit.com/oh-flutter/file-encryptor

本文配套仓库:https://github.com/m-abdulmonem/file-encryptor(TAG:0.0.1-ohos-1.0.0-beta.1,分支:main),文中示例代码位于仓库 example/ 目录。
在这里插入图片描述

文件加密是移动应用中保护本地敏感数据的基础手段。 当应用需要在设备本地存储用户隐私信息、API 密钥、会话令牌等敏感数据时,明文落盘存在被逆向或窃取的风险。file_encryptor 库提供了一个简洁的方案:用 AES-128-CBC 算法将文本内容加密后以 base64 编码写入 .aes 文件,解密时读取该文件还原原文。加解密逻辑全部是纯 Dart 实现,通过 encrypt 包完成 AES 运算,不依赖任何原生平台 API——鸿蒙应用同样开箱即用,Dart 层零改动。

本文介绍如何使用鸿蒙化适配后的 Flutter 三方库 file_encryptor,在鸿蒙 App 内通过两个方法完成文本的 AES 加密落盘与解密还原,并附上 OpenHarmony 6.1.1.120 真机的完整实测记录。


一、最终运行效果

应用启动后展示交互式加解密演示页:在输入框中输入任意文本,点击"Encrypt"按钮将文本 AES 加密后以 base64 编码写入 test_file.aes 文件,界面展示密文内容和文件大小;点击"Decrypt"按钮读取该文件并解密还原原文:

验证点结果
应用启动,Flutter 页面正常渲染通过
输入 Hello OpenHarmony!,点击 Encrypt,生成 test_file.aes(44 bytes)通过
密文以 base64 字符串形式展示(neLv/0I4Wn2M/BX6nm4IZ7Ch73Xf+uo2g3YXvd5xhIw=通过
点击 Decrypt,还原原文 Hello OpenHarmony!,加解密往返一致通过
密文文件路径为 /data/storage/el2/base/files/flutter/test_file.aes通过
全程无需申请任何权限通过
Dart 层零改动通过

Example 启动授权 复制后显示剪贴板内容 获取复制后显示剪贴板内容

操作预期表现
输入文本并点击 Encrypt生成 ${path}.aes 文件,状态显示文件大小与 base64 密文
点击 Decrypt读取同一文件并还原为原文
进程重启后 Decrypt 旧文件抛出异常(key/iv 已重新生成,继承自上游限制)

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

Example 启动授权

图一:demo 应用在 OpenHarmony 真机启动(API 24 / arm64),展示说明卡片、输入框和加解密按钮

图二:点击 Encrypt 后,界面展示密文 base64 内容、文件大小(44 bytes)和文件路径

图三:点击 Decrypt 后,界面展示 Decrypt OK: restored "Hello OpenHarmony!",加解密往返一致

检查要点

  1. 加解密逻辑全部是纯 Dart 实现,通过 encrypt 包的 Encrypter(AES(key)) 完成 AES-128-CBC 运算,不经过插件自己的 "file_encryptor" MethodChannel;
  2. 鸿蒙侧的 ArkTS 插件类 FileEncryptorPlugin 仅复刻了 Android/iOS 的模板契约(通道名 file_encryptor、方法 getPlatformVersion),保证插件注册链路完整;
  3. 密文文件保存在应用沙箱目录内(/data/storage/el2/base/files/flutter/),仅使用应用私有文件读写能力,不需要任何系统权限;
  4. 完整实测过程见"六、运行与验证"。

HarmonyOS 技术点:应用沙箱与文件存储路径

鸿蒙系统的应用文件存储采用沙箱隔离机制。getApplicationDocumentsDirectory() 返回的路径(如 /data/storage/el2/base/files/flutter/)位于应用私有沙箱内,其他应用无法直接访问。这一路径由 path_provider_ohos 插件提供,对应鸿蒙系统的 context.filesDir 属性。应用在沙箱内的文件读写不需要任何权限声明——系统自动保证隔离性。file_encryptor 将密文文件 test_file.aes 写入此目录,既保证了数据隔离,又避免了权限申请的负担。


二、file_encryptor 是什么

file_encryptor 原库(GitHub m-abdulmonem/file-encryptor,版本 0.0.1)是一个轻量的 Flutter 文件加解密库。它使用 AES-128-CBC 算法对文本内容加密,以 base64 编码写入 .aes 文件,同时支持从该文件解密还原原文。鸿蒙适配版在其基础上新增了 OpenHarmony / HarmonyOS 平台支持:新增 ohos/ 平台工程与 ArkTS 插件类 FileEncryptorPlugin,通道名和方法名与 Android/iOS/macOS/Windows/Linux 完全一致。

为什么选择 AES-128-CBC?

AES(Advanced Encryption Standard)是对称加密的事实标准,128 表示密钥长度为 128 位(16 字节)。CBC(Cipher Block Chaining)模式通过将前一个密文块与当前明文块异或后再加密,使得相同的明文块在不同位置产生不同的密文,安全性高于 ECB 模式。file_encryptor 使用 Key.fromSecureRandom(16) 生成 128 位随机密钥,IV.fromSecureRandom(16) 生成 16 字节随机初始向量,符合 AES-128-CBC 的标准要求。密文通过 base64 编码后写入文本文件,方便跨平台传输和存储。

几个对使用者友好的特点:

  1. 零权限:加解密操作仅使用应用沙箱内的文件读写能力,不需要在 module.json5 中申请任何权限;
  2. 纯 Dart 核心:AES 加解密逻辑全部通过 encrypt 包在 Dart 层完成,不依赖任何原生平台 API,跨平台共享;
  3. 极简 API:两个方法 encrypt(path, content)decrypt(path),一行代码完成加密落盘或解密还原;
  4. 跨平台覆盖广:支持 Android、iOS、macOS、Windows、Linux 和鸿蒙六端,同一套 Dart 代码在各端行为一致。

接口说明:

名称描述类型参数类型返回值必填鸿蒙平台支持
encrypt将文本加密后写入 path.aes 文件方法String path, String contentFuture<bool>
decrypt读取 path.aes 文件并解密还原原文方法String pathFuture<String>

HarmonyOS 技术点:encrypt 包与 Dart 原生加密

encrypt 是 Flutter 生态中广泛使用的加密包(pub.dev),它提供了 AES、RSA、Salsa20 等算法的纯 Dart 实现。Encrypter(AES(key)) 创建一个 AES 加密器,encrypt(content, iv: iv) 返回 Encrypted 对象,其 .base64 属性输出 base64 编码的密文字符串。decrypt64(base64String, iv: iv) 则将 base64 密文解密还原为原文。这些运算全部在 Dart 虚拟机内完成,不调用任何系统级加密 API(如 Android 的 javax.crypto 或 iOS 的 CommonCrypto),因此跨平台行为完全一致——鸿蒙端无需任何适配即可运行。


三、环境准备

本文所有实测均在以下环境完成:

版本说明
Flutter(ohos 版)3.44.9+ohos-0.0.1-canary1主验证环境,真机实测
DevEco Studio26.0.0(DS-261.23567.138.36.2600821)构建与签名
编译 SDK5.1.0(18)宿主工程 compatibleSdkVersion 同值
真机OpenHarmony 6.1.1.120API 24,arm64,设备 ID 4UQ9K25508013016

两点提醒:

  1. 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
  2. 本库依赖 path_provider 获取应用文档目录,鸿蒙端必须同时引入 path_provider_ohos(插件已声明该依赖),否则调用 getApplicationDocumentsDirectory() 会抛 MissingPluginException

HarmonyOS 技术点:path_providerpath_provider_ohos

path_provider 是 Flutter 官方的路径查询插件,但它本身不包含鸿蒙平台实现。path_provider_ohos 是 CPF-Flutter 社区提供的 OpenHarmony 适配版,通过注册 path_provider 的 MethodChannel 实现鸿蒙端路径查询。在 pubspec.yaml 中声明 path_provider_ohos: ^2.2.1 后,Flutter 构建工具自动将其鸿蒙平台实现注入 GeneratedPluginRegistrantgetApplicationDocumentsDirectory() 在鸿蒙端即可正常返回沙箱路径。如果只声明 path_provider 而不引入 path_provider_ohos,鸿蒙端调用会抛 MissingPluginException——这是因为 path_provider 的 MethodChannel 在鸿蒙端没有注册处理器。


四、引入依赖

进入工程目录,在 pubspec.yaml 中添加 git 依赖:

dependencies:
  file_encryptor:
    git:
      url: https://github.com/m-abdulmonem/file-encryptor.git
      # ref: 根据下方表格选择不同框架适配的 TAG 版本
      ref: 0.0.1-ohos-1.0.0-beta.1

执行命令拉取依赖:

flutter pub get

TAG 命名规则:原库版本-ohos-版本号-beta.x

Flutter 框架版本TAG 名称分支名
3.440.0.1-ohos-1.0.0-beta.1main

说明:该 TAG 已在 3.44.9+ohos-0.0.1-canary1 真机上实测通过。原库的 Dart 层 API 与上游完全一致,适配过程对 Dart 代码零改动。插件同时声明了 path_provider_ohos: ^2.2.1 依赖,flutter pub get 会自动拉取鸿蒙端路径查询实现。

pubspec.yaml 中的 ohos 平台声明

适配后的 pubspec.yamlflutter.plugin.platforms 下新增了 ohos 配置项,同时声明了 path_provider_ohos 依赖:

dependencies:
  path: ^1.8.1
  path_provider: ^2.0.11
  path_provider_ohos: ^2.2.1    # OpenHarmony path_provider 实现
  encrypt: ^5.0.1

flutter:
  plugin:
    platforms:
      android:
        package: com.mabdulmonem.file_encryptor
        pluginClass: FileEncryptorPlugin
      ios:
        pluginClass: FileEncryptorPlugin
      ohos:
        pluginClass: FileEncryptorPlugin

pluginClass 的值必须与 ArkTS 插件类 getUniqueClassName() 的返回值完全一致。path_provider_ohos 的引入确保了 getApplicationDocumentsDirectory() 在鸿蒙端可用。


五、代码接入

5.1 导入库

import 'package:file_encryptor/file_encryptor.dart';

导入后即可使用 FileEncryptor 类和 encrypt() / decrypt() 方法,同时库还 re-export 了 path_providergetApplicationDocumentsDirectorypath 包的 join 方法,方便一步获取路径。

5.2 加密文本到文件

final Directory appDocDir = await getApplicationDocumentsDirectory();
final String path = join(appDocDir.path, 'test_file');

final bool ok = await FileEncryptor().encrypt(path, 'Hello OpenHarmony!');
debugPrint('加密结果: $ok');

encrypt() 接受两个参数:path(不含扩展名的基础路径)和 content(要加密的文本),返回 Future<bool> 表示密文文件是否成功生成。以下是 Dart 层的实现代码,逐段解析:

class FileEncryptor {
  static Key key = Key.fromSecureRandom(16);
  static IV iv = IV.fromSecureRandom(16);

  Encrypter get encrypter => Encrypter(AES(key));

  Future<bool> encrypt(String path, String content) async {
    final file = File("$path.aes");

    file.writeAsString(encrypter.encrypt(content, iv: iv).base64);

    return file.exists();
  }
}

上述代码的核心逻辑分为三步。第一步是密钥与 IV 初始化:Key.fromSecureRandom(16) 生成 128 位(16 字节)随机密钥,IV.fromSecureRandom(16) 生成 16 字节随机初始向量。两者都是 static 字段,在类首次加载时生成一次,整个进程生命周期内保持不变。第二步是创建加密器:Encrypter(AES(key)) 使用 AES 算法和上一步的密钥构造加密器实例。第三步是加密并写入文件:encrypter.encrypt(content, iv: iv) 用 AES-128-CBC 算法和 IV 加密文本内容,返回 Encrypted 对象,.base64 属性输出 base64 编码的密文字符串。file.writeAsString() 将密文写入 $path.aes 文件(自动在路径后追加 .aes 扩展名),最后返回 file.exists() 确认文件是否成功生成。

HarmonyOS 技术点:AES-128-CBC 加密流程

AES-128-CBC 的加密流程为:将明文按 16 字节分块,第一个明文块与 IV(初始向量)异或后用密钥加密得到第一个密文块;第二个明文块与第一个密文块异或后加密得到第二个密文块,依此类推。最后一个块如果不足 16 字节,使用 PKCS7 填充补齐。encrypt 包的 Encrypted.base64 属性将密文字节做 base64 编码输出为字符串。实测中,20 字节的明文 Hello OpenHarmony! 经 PKCS7 填充后为 32 字节(两个块),base64 编码后为 44 字符——与真机实测的 44 bytes 文件大小完全吻合。

5.3 从文件解密还原文本

final String plainText = await FileEncryptor().decrypt(path);
debugPrint('解密结果: $plainText');

decrypt() 接受一个 path 参数(不含扩展名的基础路径),返回 Future<String> 为解密还原的原文。以下是实现代码:

Future<String> decrypt(String path) async {
  final file = File("$path.aes");

  final data = await file.readAsString();

  return encrypter.decrypt64(data, iv: iv);
}

上述代码的逻辑分为两步。第一步读取密文文件:File("$path.aes").readAsString() 读取密文文件的 base64 字符串内容。第二步解密还原:encrypter.decrypt64(data, iv: iv) 将 base64 密文字符串解码为字节,用与加密时相同的密钥和 IV 解密还原原文。由于 keyiv 都是 static 字段,同一进程内加密后一定能解密还原。

解密的前提条件:解密要求目标文件 path.aes 必须存在,且必须由同一进程(使用相同密钥与 IV)加密生成。文件不存在会抛 FileSystemException;文件内容被篡改或使用不同密钥/IV 解密会抛 FormatExceptionArgumentError。业务层应在调用处使用 try/catch 处理这些异常。

5.4 与 path_provider 搭配使用

推荐结合 path_provider 获取应用文档目录,将密文文件保存在应用沙箱内:

final Directory appDocDir = await getApplicationDocumentsDirectory();
final String path = join(appDocDir.path, 'test_file');

getApplicationDocumentsDirectory() 在鸿蒙端返回 /data/storage/el2/base/files/flutter/ 等沙箱路径,由 path_provider_ohos 插件提供。join() 方法来自 path 包,用于跨平台拼接路径分隔符。file_encryptor 已经 re-export 了这两个依赖,导入 file_encryptor 即可直接使用。

为什么不直接用硬编码路径? 不同平台的文件系统路径格式不同(Android 用 /data/user/0/...,iOS 用 .../Documents/,鸿蒙用 /data/storage/el2/...),硬编码路径无法跨平台。path_provider 根据当前平台返回正确的沙箱路径,path.join() 正确拼接路径分隔符,确保同一代码在所有平台都能正确定位文件。

5.5 跨平台行为

同一套 API 在各端的行为完全一致,因为核心逻辑是纯 Dart 实现:

平台加密算法文件路径来源权限要求Dart 层改动
AndroidAES-128-CBC(纯 Dart)path_provider
iOSAES-128-CBC(纯 Dart)path_provider
macOSAES-128-CBC(纯 Dart)path_provider
WindowsAES-128-CBC(纯 Dart)path_provider
LinuxAES-128-CBC(纯 Dart)path_provider
OpenHarmony / HarmonyOSAES-128-CBC(纯 Dart)path_provider_ohos

鸿蒙侧的 ArkTS 插件类 FileEncryptorPlugin 仅复刻了 Android/iOS 的模板契约(通道名 file_encryptor、方法 getPlatformVersion),Dart 层的加解密操作从未调用该通道——核心功能全部通过 encrypt 包在 Dart 层完成。

5.6 实战:给配置文件加加密存储

实际业务中常见的场景是:应用需要本地存储一些敏感配置(如 API 密钥、用户令牌),不能明文落盘。下面是一个可直接使用的封装:

import 'package:file_encryptor/file_encryptor.dart';

class SecureStorage {
  static const _configFileName = 'app_config';

  /// 加密保存配置
  static Future<bool> save(String configJson) async {
    final Directory appDocDir = await getApplicationDocumentsDirectory();
    final String path = join(appDocDir.path, _configFileName);
    return FileEncryptor().encrypt(path, configJson);
  }

  /// 解密读取配置
  static Future<String> load() async {
    final Directory appDocDir = await getApplicationDocumentsDirectory();
    final String path = join(appDocDir.path, _configFileName);
    try {
      return FileEncryptor().decrypt(path);
    } catch (e) {
      debugPrint('解密失败: $e');
      return '';
    }
  }

  /// 检查配置文件是否存在
  static Future<bool> exists() async {
    final Directory appDocDir = await getApplicationDocumentsDirectory();
    final String path = join(appDocDir.path, _configFileName);
    return File('$path.aes').exists();
  }
}

上述组件的设计思路是:通过 SecureStorage.save(jsonString) 将 JSON 配置加密后写入 app_config.aes 文件,通过 SecureStorage.load() 读取并解密还原。exists() 方法检查密文文件是否已存在,用于判断是否首次启动。load() 方法内部使用 try/catch 包裹解密操作——文件不存在或内容被篡改时返回空字符串而非崩溃。

密钥生命周期提醒FileEncryptor 的密钥和 IV 是 static 字段,在进程首次加载时随机生成。这意味着进程重启后密钥会重新生成,之前加密的文件无法用新密钥解密。因此 file_encryptor 适合"单次会话内的临时保护"场景(如临时缓存、会话内数据交换),不适合"持久化加密存储"场景(如需要跨重启读取的配置文件)。持久化场景需要自行管理密钥的存储和恢复(见 FAQ Q2)。


六、运行与验证

以下为 demo 工程的真机实测记录。仓库中 example 的 Bundle Name 为 com.example.ohos_example_scaffold,签名配置使用 DevEco Studio 自动签名。

设备项
机型OpenHarmony 真机
设备 ID4UQ9K25508013016
系统版本OpenHarmony 6.1.1.120
API 版本24
架构arm64

HarmonyOS 技术点:FlutterAbility 与 EntryAbility

鸿蒙 Flutter 应用的入口 Ability 需要继承 FlutterAbility(由 @ohos/flutter_ohos 提供),而非标准的 UIAbilityFlutterAbility 内部封装了 FlutterEngine 的初始化、Surface 注册、路由管理等逻辑。宿主工程的 EntryAbility 只需重写 configureFlutterEngine 方法,在其中调用 GeneratedPluginRegistrant.registerWith(flutterEngine) 即可完成所有原生插件的注册:

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
  }
}

6.1 验证一:构建与安装

构建 hap 后安装到真机并启动:

# 构建 hap(debug,含签名)
flutter build hap --debug

# 安装到真机
hdc install entry-default-signed.hap

# 启动 demo
hdc shell aa start -b com.example.ohos_example_scaffold -a EntryAbility

构建成功产出 entry-default-signed.hap,安装成功(install bundle successfully),启动成功(start ability successfully)。

签名提醒:OpenHarmony 设备仅信任 DevEco Studio 自动签名生成的调试证书。如果安装时报 fail to verify pkcs7 file,请使用 DevEco Studio 的自动签名配置(File > Project Structure > Signing Configs)生成签名材料后再构建 HAP。

在这里插入图片描述

6.2 验证二:插件注册

通过 hilog 确认插件注册:

hdc shell "hilog | grep -E 'FileEncryptorPlugin|file_encryptor|PathProvider'"

实测日志输出:

FlutterEngineCxnRegistry --> Adding plugin: FileEncryptorPlugin
FlutterEngineCxnRegistry --> Adding plugin: PathProviderPlugin
DartMessenger --> Setting handler for channel 'file_encryptor'
DartMessenger --> Setting handler for channel 'plugins.flutter.io/path_provider'

FileEncryptorPluginPathProviderPlugin 均成功注册到 Flutter 引擎。file_encryptor MethodChannel 和 plugins.flutter.io/path_provider MethodChannel 均已建立处理器——前者用于 getPlatformVersion 模板方法,后者用于 getApplicationDocumentsDirectory() 路径查询。

日志解读Adding plugin: FileEncryptorPlugin 表示 GeneratedPluginRegistrant.registerWith 成功将加密插件实例添加到引擎。Adding plugin: PathProviderPlugin 表示 path_provider_ohos 的路径查询插件也已注册——这保证了 getApplicationDocumentsDirectory() 在鸿蒙端可以正常返回沙箱路径。如果缺少 PathProviderPlugin,路径查询会抛 MissingPluginException

6.3 验证三:加密操作

在 demo 界面的输入框中输入文本 Hello OpenHarmony!,点击"Encrypt"按钮。界面展示加密结果:

Encrypt OK: "Hello OpenHarmony!" → 44 bytes cipher file
Cipher content (base64): neLv/0I4Wn2M/BX6nm4IZ7Ch73Xf+uo2g3YXvd5xhIw=
File: /data/storage/el2/base/files/flutter/test_file.aes

加密成功,生成了 44 字节的 test_file.aes 文件,路径为 /data/storage/el2/base/files/flutter/test_file.aes。密文为 base64 字符串 neLv/0I4Wn2M/BX6nm4IZ7Ch73Xf+uo2g3YXvd5xhIw=

文件大小分析:明文 Hello OpenHarmony! 为 20 字节,经 PKCS7 填充后为 32 字节(两个 16 字节块),base64 编码后为 44 字符(32 × 4/3 向上取整 = 44,含 1 个 = 填充符)。实测文件大小 44 bytes 与计算值完全吻合。

在这里插入图片描述

6.4 验证四:解密操作

点击"Decrypt"按钮,从 test_file.aes 文件读取密文并解密还原。界面展示解密结果:

Decrypt OK: restored "Hello OpenHarmony!"

解密成功,还原的原文与输入的明文完全一致——加解密往返一致,验证了 AES-128-CBC 加解密的正确性。

实测结论:

验证点结果
应用启动,Flutter 页面正常渲染通过
输入文本并点击 Encrypt,生成 test_file.aes(44 bytes)通过
密文 base64 内容正确展示通过
点击 Decrypt,还原原文,加解密往返一致通过
密文文件保存在应用沙箱目录通过
插件注册成功(FileEncryptorPlugin + PathProviderPlugin)通过
全程无需申请任何权限通过
Dart 层零改动通过

在这里插入图片描述


七、工作原理

整个调用链路如下:

加密 (encrypt):
  Dart: FileEncryptor().encrypt(path, 'Hello')
    → Encrypter(AES(key)).encrypt('Hello', iv: iv)
      → AES-128-CBC 运算(纯 Dart,encrypt 包)
        → Encrypted.base64  // base64 密文字符串
          → File('$path.aes').writeAsString(base64密文)
            → 应用沙箱文件写入(dart:io,无需权限)
              → return file.exists()

解密 (decrypt):
  Dart: FileEncryptor().decrypt(path)
    → File('$path.aes').readAsString()
      → 读取 base64 密文字符串
        → Encrypter(AES(key)).decrypt64(base64密文, iv: iv)
          → AES-128-CBC 解密运算(纯 Dart,encrypt 包)
            → return 原文

插件模板通道(Dart 层未调用加解密,仅 getPlatformVersion):
  Dart → 原生: MethodChannel('file_encryptor').invokeMethod('getPlatformVersion')
    → ArkTS: FileEncryptorPlugin.onMethodCall
      → result.success('OpenHarmony ' + deviceInfo.displayVersion)

关键区分:file_encryptor 通道 vs encrypt

file_encryptor 库涉及两条通信路径,它们的职责完全不同:

路径归属职责Dart 层是否调用
encrypt 包(纯 Dart 库)第三方 Dart 包AES-128-CBC 加解密运算是(Encrypter.encrypt/decrypt64
file_encryptor MethodChannel插件自定义getPlatformVersion(模板骨架)

加解密运算走的是 encrypt 包——这是一个纯 Dart 加密库,不涉及任何 MethodChannel 或原生平台 API。AES 运算全部在 Dart 虚拟机内完成。插件的 file_encryptor MethodChannel 仅用于 getPlatformVersion,是 flutter create 生成的模板骨架,Dart 层的加解密逻辑从未调用该通道。

7.1 Dart 层实现解析

库的 Dart 层只有一个文件 lib/file_encryptor.dart,包含一个 FileEncryptor 类和两个方法,逐段解析如下。

完整代码

import 'dart:io';
import 'package:encrypt/encrypt.dart';

export 'package:path_provider/path_provider.dart';
export 'package:path/path.dart';

class FileEncryptor {
  static Key key = Key.fromSecureRandom(16);
  static IV iv = IV.fromSecureRandom(16);

  Encrypter get encrypter => Encrypter(AES(key));

  Future<bool> encrypt(String path, String content) async {
    final file = File("$path.aes");
    file.writeAsString(encrypter.encrypt(content, iv: iv).base64);
    return file.exists();
  }

  Future<String> decrypt(String path) async {
    final file = File("$path.aes");
    final data = await file.readAsString();
    return encrypter.decrypt64(data, iv: iv);
  }
}

keyiv 静态字段解析

static Key key = Key.fromSecureRandom(16);
static IV iv = IV.fromSecureRandom(16);

Key.fromSecureRandom(16) 生成 16 字节(128 位)的密码学安全随机密钥,IV.fromSecureRandom(16) 生成 16 字节的随机初始向量。两者都是 static 字段,在类首次被引用时初始化一次,整个进程生命周期内保持不变。这意味着同一进程内加密的数据可以用同一密钥和 IV 解密,但进程重启后密钥和 IV 会重新生成,之前加密的文件无法解密。

HarmonyOS 技术点:Key.fromSecureRandom 的随机源

Key.fromSecureRandom(16) 底层调用 Dart 的 Random.secure(),它使用操作系统的密码学安全随机数生成器(CSPRNG)。在鸿蒙系统上,Dart 虚拟机通过 /dev/urandom 或系统级安全随机源获取随机字节。生成的密钥在密码学上是安全的,不可预测。但由于密钥存储在 Dart 堆内存中(static 字段),进程退出后密钥丢失——这是"会话内临时保护"设计,非"持久化加密"设计。

encrypter getter 解析

Encrypter get encrypter => Encrypter(AES(key));

这是一个 getter,每次访问时创建一个新的 Encrypter 实例,使用 static key 构造。AES(key) 创建 AES 加密算法的配置,Encrypter 封装了加密和解密方法。由于 key 是静态的,每次创建的 Encrypter 使用的密钥相同,加解密结果一致。

encrypt 方法解析

Future<bool> encrypt(String path, String content) async {
  final file = File("$path.aes");
  file.writeAsString(encrypter.encrypt(content, iv: iv).base64);
  return file.exists();
}

encrypt 方法接受 path(基础路径,不含扩展名)和 content(明文文本)。在 path 后追加 .aes 扩展名创建 File 对象,调用 encrypter.encrypt(content, iv: iv) 加密内容,取 .base64 属性获取 base64 编码的密文字符串,通过 writeAsString 写入文件。注意 writeAsString 默认会覆盖已有文件内容。最后返回 file.exists() 确认文件是否成功生成。

writeAsString 未 await 的原因file.writeAsString(...) 返回 Future<File>,但代码中没有 await——这意味着写入操作是"fire-and-forget"模式。在 Dart 的事件循环中,writeAsString 会在当前微任务队列中调度写入操作,而 file.exists() 在下一个 await 点执行时,写入操作通常已经完成。但严格来说,这不是最佳实践——如果写入操作较慢(如大文件),exists() 可能在写入完成前返回 false。对于小文件(几十字节的密文),实践中不会出问题。

decrypt 方法解析

Future<String> decrypt(String path) async {
  final file = File("$path.aes");
  final data = await file.readAsString();
  return encrypter.decrypt64(data, iv: iv);
}

decrypt 方法接受 path(基础路径),在 path 后追加 .aes 扩展名定位密文文件。await file.readAsString() 异步读取文件内容为字符串(base64 密文),encrypter.decrypt64(data, iv: iv) 将 base64 密文解密还原为原文。如果文件不存在,readAsString 会抛 FileSystemException;如果文件内容不是有效的 base64 或密文被篡改,decrypt64 会抛相应的异常。

re-export 声明解析

export 'package:path_provider/path_provider.dart';
export 'package:path/path.dart';

这两行 exportpath_providergetApplicationDocumentsDirectorypath 包的 join 函数统一导出。用户只需 import 'package:file_encryptor/file_encryptor.dart' 即可使用全部 API,无需额外导入 path_providerpath 包。

7.2 鸿蒙侧 ArkTS 插件实现

虽然加解密的核心功能是纯 Dart 实现,但为保证插件注册链路完整性(与 Android/iOS 行为一致),鸿蒙侧仍提供了 ArkTS 插件类。逐段解析如下。

第一段:导入与类声明

import deviceInfo from '@ohos.deviceInfo';
import { hilog } from '@kit.PerformanceAnalysisKit';
import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';

const TAG: string = 'FileEncryptorPlugin';
const CHANNEL_NAME: string = 'file_encryptor';
const DOMAIN: number = 0x0000;

export default class FileEncryptorPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;

这段代码从 @ohos.deviceInfo 导入设备信息模块(用于 getPlatformVersion),从 @kit.PerformanceAnalysisKit 导入 hilog 日志模块(用于错误日志记录),从 @ohos/flutter_ohos 导入 Flutter 鸿蒙适配层的插件接口类型。FileEncryptorPlugin 类实现 FlutterPlugin(生命周期管理)和 MethodCallHandler(方法调用处理)两个接口——注意没有实现 AbilityAware,因为插件不需要 UIAbility 上下文(加解密通过纯 Dart 完成,不涉及系统级 API 调用)。

HarmonyOS 技术点:@ohos.deviceInfodisplayVersion

@ohos.deviceInfo 模块提供设备信息查询能力。deviceInfo.displayVersion 返回系统的显示版本号(如 HarmonyOS 6.1.1.120),语义上最接近 Android 的 Build.VERSION.RELEASE 和 iOS 的 UIDevice.current.systemVersion。适配过程中,通过查询 SDK 的 .d.ts 类型声明文件(@ohos.deviceInfo.d.ts)确认 displayVersion 字段名——这是适配工作中的重要习惯:先查 SDK 声明文件确认 API 名称,而非凭记忆编写,避免使用不存在的字段名导致编译错误。

HarmonyOS 技术点:hilog 日志系统

hilog 是鸿蒙系统的日志模块,来自 @kit.PerformanceAnalysisKit 工具包。hilog.error(domain, tag, format, ...args) 用于输出错误级别日志。domain 是日志域编号(0x0000 表示通用域),tag 是日志标签,format 支持 %{public}s 格式化占位符。与 console.error 相比,hilog 的日志可以通过 hdc shell hilog 命令在真机上过滤查看,是鸿蒙原生开发的标准日志工具。

第二段:通道注册与生命周期管理

getUniqueClassName(): string {
  return "FileEncryptorPlugin";
}

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
  this.channel.setMethodCallHandler(this);
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
    this.channel = null;
  }
}

getUniqueClassName() 返回 "FileEncryptorPlugin",需与 pubspec.yaml 中的 pluginClass 配置一致。onAttachedToEngine 在引擎加载插件时创建名为 "file_encryptor" 的 MethodChannel(与 Android/iOS/macOS 通道名一致)并设置方法调用处理器。onDetachedFromEngine 在引擎卸载插件时清理引用,将 channel 置为 null 防止内存泄漏。

第三段:方法分发与实现

onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    if (call.method == "getPlatformVersion") {
      result.success("OpenHarmony " + deviceInfo.displayVersion);
    } else {
      result.notImplemented();
    }
  } catch (e) {
    hilog.error(DOMAIN, TAG, 'onMethodCall failed, method=%{public}s, error=%{public}s',
      call.method, JSON.stringify(e));
    result.error("file_encryptor_error", `handle method "${call.method}" failed`, e);
  }
}

onMethodCall 是方法调用的入口。当前仅处理 "getPlatformVersion" 方法,返回 "OpenHarmony " + deviceInfo.displayVersion。未知方法调用 result.notImplemented() 回复。所有逻辑包裹在 try/catch 中,异常时通过 hilog.error() 记录错误日志,并通过 result.error() 回传错误码 "file_encryptor_error"、错误消息和异常对象——不静默失败。

通道契约三端对照

契约项Android (Kotlin)iOS (Swift)OHOS (ArkTS)
通道名file_encryptorfile_encryptorfile_encryptor
方法名getPlatformVersiongetPlatformVersiongetPlatformVersion
返回值"Android ${Build.VERSION.RELEASE}""iOS " + systemVersion"OpenHarmony " + deviceInfo.displayVersion
未知方法result.notImplemented()FlutterMethodNotImplementedresult.notImplemented()
错误处理try/catch → result.error()try/catch → FlutterErrortry/catch → hilog.error + result.error()
日志工具Log.eprinthilog.error

六端通道契约完全一致(含 macOS/Windows/Linux),仅返回值的前缀和系统版本来源不同。Dart 层的加解密操作从未调用 getPlatformVersion——它是模板骨架的一部分,保留它是为了与既有平台行为保持一致。

7.3 插件注册机制

鸿蒙侧的插件注册是自动完成的。Flutter 鸿蒙适配层在构建时扫描 pubspec.yaml 中的 ohos: pluginClass 配置,自动生成 GeneratedPluginRegistrant.ets 文件:

import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import FileEncryptorPlugin from 'file_encryptor';
import PathProviderPlugin from 'path_provider_ohos';

export class GeneratedPluginRegistrant {
  static registerWith(flutterEngine: FlutterEngine) {
    try {
      flutterEngine.getPlugins()?.add(new FileEncryptorPlugin());
      flutterEngine.getPlugins()?.add(new PathProviderPlugin());
    } catch (e) {
      Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
    }
  }
}

注意 GeneratedPluginRegistrant 同时注册了 FileEncryptorPluginPathProviderPlugin——后者来自 path_provider_ohos 依赖。这保证了 getApplicationDocumentsDirectory() 在鸿蒙端可以正常返回沙箱路径。

整个注册链路为:

EntryAbility.configureFlutterEngine()
  → GeneratedPluginRegistrant.registerWith(flutterEngine)
    → flutterEngine.getPlugins().add(new FileEncryptorPlugin())
      → FileEncryptorPlugin.onAttachedToEngine(binding)
        → new MethodChannel(messenger, "file_encryptor")
          → setMethodCallHandler(this)
    → flutterEngine.getPlugins().add(new PathProviderPlugin())
      → PathProviderPlugin.onAttachedToEngine(binding)
        → new MethodChannel(messenger, "plugins.flutter.io/path_provider")
          → setMethodCallHandler(...)

注册完成后,file_encryptorplugins.flutter.io/path_provider 两条 MethodChannel 都已就绪。但 Dart 层的 FileEncryptor().encrypt()FileEncryptor().decrypt() 方法不通过 file_encryptor 通道发送请求——它们调用的是 encrypt 包的纯 Dart API 和 dart:io 的文件 I/O。

HarmonyOS 技术点:HAR 模块

file_encryptor 的鸿蒙侧原生代码以 HAR(Harmony Archive)模块形式打包,类似于 Android 的 AAR。HAR 模块在 module.json5 中声明 "type": "har",可以被宿主工程通过 oh-package.json5 依赖引入。Flutter 的 ohos 适配层会自动在宿主工程的 GeneratedPluginRegistrant 中注册插件类,无需手动编写注册代码。

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

Example 启动授权


八、常见问题

Q1:getApplicationDocumentsDirectory()MissingPluginException 怎么办?

原因是未引入 path_provider_ohos 依赖。path_provider 本身不包含鸿蒙平台实现,需要通过 path_provider_ohos 提供 OpenHarmony 端的路径查询能力。file_encryptorpubspec.yaml 已声明 path_provider_ohos: ^2.2.1 依赖,执行 flutter pub get 后重新构建即可。如果仍然报错,检查 GeneratedPluginRegistrant.ets 是否包含 PathProviderPlugin 注册行。

Q2:进程重启后之前加密的文件无法解密是怎么回事?

这是 file_encryptor 的已知设计限制。密钥 Key.fromSecureRandom(16) 和 IV IV.fromSecureRandom(16)static 字段,在类首次加载时随机生成。进程重启后类重新加载,密钥和 IV 重新随机生成,与之前加密时使用的密钥不同,因此无法解密历史文件。file_encryptor 适合"单次会话内的临时保护"场景(如临时缓存、会话内数据交换),不适合"持久化加密存储"场景。持久化场景需要自行管理密钥的存储和恢复——例如将密钥持久化到安全存储(如 flutter_secure_storage),在每次启动时恢复密钥。

Q3:安装 HAP 时报 fail to verify pkcs7 file 怎么办?

这是签名信任链问题。OpenHarmony 设备仅信任 DevEco Studio 自动签名生成的调试证书(default_ohos_*.p12/.cer/.p7b),而不信任 SDK 自带的 OpenHarmony.p12 官方调试链。解决方法:使用 DevEco Studio 的自动签名配置(File > Project Structure > Signing Configs)生成签名材料后再构建 HAP。确保 build-profile.json5 中的 signingConfigs 引用的是 DevEco 自动生成的证书路径。

Q4:加密后的文件大小为什么比明文大?

这是 AES-128-CBC 加密和 base64 编码的双重膨胀。以 20 字节明文为例:PKCS7 填充将明文补齐到 16 字节的倍数(20 → 32 字节,两个块),base64 编码将 3 字节扩展为 4 字符(32 → 44 字符,含 1 个 = 填充符)。因此 20 字节明文最终生成 44 字节的密文文件。膨胀率约为 2.2 倍(44/20),对于小文件来说比例较高,但对于大文件(如 KB 级以上),膨胀率趋近于 4/3 × 16/16 ≈ 1.33 倍。

Q5:file_encryptor 通道和 encrypt 包有什么区别?

file_encryptor MethodChannel 是插件自定义的通道,仅用于 getPlatformVersion 模板方法,Dart 层的加解密操作从未调用该通道。encrypt 包是一个纯 Dart 加密库,提供 AES、RSA 等算法的实现,file_encryptor 的加解密逻辑全部通过 encrypt 包在 Dart 虚拟机内完成,不涉及 MethodChannel 或原生平台 API。因此鸿蒙端无需为加解密逻辑编写任何原生代码——ArkTS 插件类仅为保证注册链路完整性。

Q6:可以用 file_encryptor 加密图片或其他二进制文件吗?

当前版本不支持。encrypt() 方法的 content 参数类型为 Stringencrypter.encrypt() 方法接受文本输入。如果要加密二进制数据,需要先将二进制转为 base64 字符串再传入 encrypt(),或自行修改 Dart 层代码使用 encrypt 包的 encryptBytes() 方法处理 Uint8List 输入。由于库的设计目标是"文本加密落盘",二进制场景建议直接使用 encrypt 包而非 file_encryptor


九、结语

回顾一下:在 pubspec.yaml 中以 git TAG 引入 file_encryptor,调用 FileEncryptor().encrypt(path, content) 将文本 AES-128-CBC 加密后以 base64 写入 .aes 文件,调用 FileEncryptor().decrypt(path) 读取文件并解密还原原文。核心功能全部是纯 Dart 实现,通过 encrypt 包完成 AES 运算,不经过插件自定义通道。零权限、应用沙箱内文件 I/O、Dart 层零改动,已在 OpenHarmony 6.1.1.120 真机(API 24 / arm64)完整实测。需注意密钥为进程内静态随机值,进程重启后无法解密历史文件,仅适合会话内临时保护场景。

总结对比

接口功能实现方式底层依赖权限要求
encrypt()加密文本写入 .aes 文件纯 Dart(encrypt 包 + dart:ioencrypt 包 + path_provider_ohos
decrypt()读取 .aes 文件解密还原纯 Dart(encrypt 包 + dart:ioencrypt 包 + path_provider_ohos
路径归属用途Dart 层是否调用
encrypt纯 Dart 库AES-128-CBC 加解密运算是(Encrypter.encrypt/decrypt64
file_encryptor MethodChannel插件自定义getPlatformVersion否(模板骨架)
plugins.flutter.io/path_providerpath_provider_ohos应用沙箱路径查询是(getApplicationDocumentsDirectory
平台加密算法路径来源权限Dart 层改动
AndroidAES-128-CBC(纯 Dart)path_provider
iOSAES-128-CBC(纯 Dart)path_provider
OHOSAES-128-CBC(纯 Dart)path_provider_ohos

核心要点:两个方法,纯 Dart AES 加解密,零权限沙箱文件 I/O,密钥进程内静态随机,Dart 层零改动

使用中发现任何问题,欢迎到配套仓库提 Issue,也欢迎发 PR 共建。

相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

Logo

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

更多推荐