起因和目的
在最近开发基于 Flutter 框架的应用时,需要向应用中添加一些素材文件(比如名片、二维码图片等)。
在编译项目后,发现这些素材文件是明文储存在 assets 文件夹中的,非常容易提取和篡改。
因此,我打算使用加密方法对素材文件进行加密,这样一来:
- 即使提取了素材文件,缺少密码也无法读取素材内容;
- 即使篡改了素材文件,应用按照原有的解密方式读取文件,将无法读取篡改后的内容。
加密素材
构建项目
可以使用 Dart 构建 CLI 程序项目,使用 encrypt 包进行加密和解密:
1 2 3 4 5
| dart create assets_encryption
dart pub add encrypt
|
生成密码和 IV
encrypt 默认使用 AES CBC 模式对数据进行加密,对于加密和解密,需要提供密码 key 和初始向量 IV。
虽然 encrypt 提供了随机生成的方法,不过也可以使用 python 生成 key 和 IV:
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 { final key = Key.fromBase64(keyBase64); final iv = IV.fromBase64(ivBase64); final encrypter = Encrypter(AES(key)); 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), ); } 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 { final decryptedBytes = (await file.readAsBytes()).buffer.asUint8List(); final decryptedBase64 = convert.base64Encode(decryptedBytes); final encryptedBase64 = encrypter.encrypt(decryptedBase64, iv: iv).base64; 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 { 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); 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 文件夹中。
运行项目代码:
会发现 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 应用素材的加密,从而:
- 从素材文件夹中获取的文件被加密,不能直接读取;
- 如果篡改素材文件夹中的文件,在应用中将无法正常读取;
- 加密和解密使用的密码不会暴露给使用者。