使用 Dart 加密 Flutter 应用的素材

起因和目的

在最近开发基于 Flutter 框架的应用时,需要向应用中添加一些素材文件(比如名片、二维码图片等)。

在编译项目后,发现这些素材文件是明文储存在 assets 文件夹中的,非常容易提取和篡改。

因此,我打算使用加密方法对素材文件进行加密,这样一来:

  1. 即使提取了素材文件,缺少密码也无法读取素材内容;
  2. 即使篡改了素材文件,应用按照原有的解密方式读取文件,将无法读取篡改后的内容。

加密素材

构建项目

可以使用 Dart 构建 CLI 程序项目,使用 encrypt 包进行加密和解密:

1
2
3
4
5
# 创建项目
dart create assets_encryption

# 添加依赖
dart pub add encrypt

生成密码和 IV

encrypt 默认使用 AES CBC 模式对数据进行加密,对于加密和解密,需要提供密码 key 和初始向量 IV

虽然 encrypt 提供了随机生成的方法,不过也可以使用 python 生成 keyIV

1
2
3
4
5
import base64
import random

print(base64.b64encode(random.randbytes(32)))
print(base64.b64encode(random.randbytes(16)))

这样就生成了需要的 256-bits 的 key 和 128-bits 的 IV

编写加密/解密代码

在 Dart 项目中使用前面生成的配置并编写加密和解密的代码:

bin/assets_encryption.dart 中编写主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'dart:io';
import 'package:encrypt/encrypt.dart';

import 'package:assets_encryption/assets_encryption.dart';

const originalAssetsPath = 'assets';
const encryptedAssetsPath = 'assets-encrypted';
const decryptedAssetsPath = 'assets-decrypted';
const keyBase64 = 'WL68UwjjhzzZIUaBTl/RwqE8luB58OQnC6Ui9G5skjY=';
const ivBase64 = 'pztzXcoy9XK++8wde3xBXw==';

void main(List<String> arguments) async {
// Initialize encryption info
final key = Key.fromBase64(keyBase64);
final iv = IV.fromBase64(ivBase64);
final encrypter = Encrypter(AES(key));
// Read original file and encrypt
final assetsDirectory = Directory(originalAssetsPath);
List<FileSystemEntity> assetsEntities = assetsDirectory.listSync();
for (final fileSystemEntity in assetsEntities) {
File file = File(fileSystemEntity.path);
await encrypt(
encrypter: encrypter,
iv: iv,
file: file,
outputDir: Directory(encryptedAssetsPath),
);
}
// Read encrypted files and decrypt
final encryptedDirectory = Directory(encryptedAssetsPath);
List<FileSystemEntity> encryptedEntities = encryptedDirectory.listSync();
for (final fileSystemEntity in encryptedEntities) {
File file = File(fileSystemEntity.path);
await decrypt(
encrypter: encrypter,
iv: iv,
file: file,
outputDir: Directory(decryptedAssetsPath),
);
}
}

lib/assets_encryption.dart 中编写加密和解密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'dart:io';
import 'dart:convert' as convert;

import 'package:encrypt/encrypt.dart';

Future<void> encrypt({
required Encrypter encrypter,
required IV iv,
required File file,
required Directory outputDir,
}) async {
// Encrypt data
final decryptedBytes = (await file.readAsBytes()).buffer.asUint8List();
final decryptedBase64 = convert.base64Encode(decryptedBytes);
final encryptedBase64 = encrypter.encrypt(decryptedBase64, iv: iv).base64;
// Save as file
await outputDir.create();
final fileName = file.path.split(Platform.pathSeparator).last;
await File([outputDir.path, fileName].join(Platform.pathSeparator))
.writeAsBytes(convert.utf8.encoder.convert(encryptedBase64));
print('File ${file.path} encrypted and saved!');
}

Future<void> decrypt({
required Encrypter encrypter,
required IV iv,
required File file,
required Directory outputDir,
}) async {
// Decrypt data
final encryptedBytes = await file.readAsBytes();
final encryptedBase64 = convert.utf8.decoder.convert(encryptedBytes);
final decryptedBase64 =
encrypter.decrypt(Encrypted.fromBase64(encryptedBase64), iv: iv);
final decryptedBytes = convert.base64Decode(decryptedBase64);
// Save as file
await outputDir.create();
final fileName = file.path.split(Platform.pathSeparator).last;
await File([outputDir.path, fileName].join(Platform.pathSeparator))
.writeAsBytes(decryptedBytes);
print('File ${file.path} decrypted and saved!');
}

上述的代码将项目目录中 assets 文件夹中的所有文件加密后存放到 assets-encrypted 文件夹中,再将这些文件解密后存放到 assets-decrypted 文件夹中。

运行项目代码:

1
dart run

会发现 assets-encrypted 文件夹中文件无法读取;assets-decrypted 文件夹中文件和原文件相同。

在 Flutter 中使用加密素材

解密素材

将加密后的文件移动到 Flutter 应用的 assets 文件夹中,在解密后读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Future<Uint8List?> decryptAsset(String assetPath) async {
const keyBase64 = "WL68UwjjhzzZIUaBTl/RwqE8luB58OQnC6Ui9G5skjY=";
const ivBase64 = "pztzXcoy9XK++8wde3xBXw==";
final key = encrypt.Key.fromBase64(keyBase64);
final iv = encrypt.IV.fromBase64(ivBase64);
final encrypter = encrypt.Encrypter(encrypt.AES(key));
try {
final assetByteData = await rootBundle.load(assetPath);
final encryptedBase64 = assetByteData.buffer.asUint8List();
final decryptedBase64 =
encrypter.decrypt(encrypt.Encrypted(encryptedBase64), iv: iv);
final decryptedBytes = base64Decode(decryptedBase64);
return decryptedBytes;
} catch (exception) {
return null;
}
}

这样一来,对于指定的 assetPath,将读取解密后的内容作为 Uint8List 返回。

即使素材文件被篡改,如果没有使用相同的方法加密,必然会解密失败并返回 null,从而防止了素材被篡改的问题。

存放密码和 IV

将密码硬编码在代码中不是非常安全(尤其是代码开源时!);另外,硬编码的方法也不利于不同配置的切换。

因此,可以使用编译时的环境变量设置,将密码存放在配置文件中,在编译时作为编译时环境变量添加到应用中。

在应用中读取变量:

1
2
const keyBase64 = String.fromEnvironment("ASSET_KEY_BASE64");
const ivBase64 = String.fromEnvironment("ASSET_IV_BASE64");

编写定义环境变量的文件 secrets.json

1
2
3
4
{
"ASSET_KEY_BASE64": "WL68UwjjhzzZIUaBTl/RwqE8luB58OQnC6Ui9G5skjY=",
"ASSET_IV_BASE64": "pztzXcoy9XK++8wde3xBXw==",
}

最终,在编译时添加定义在文件中的环境变量:

1
flutter build --dart-define-from-file=secrets.json

另外,别忘记在 .gitignore 添加环境变量文件,免得包含密码的文件被上传到远端存储库中!

总结

由此,实现了对 Flutter 应用素材的加密,从而:

  1. 从素材文件夹中获取的文件被加密,不能直接读取;
  2. 如果篡改素材文件夹中的文件,在应用中将无法正常读取;
  3. 加密和解密使用的密码不会暴露给使用者。