原文地址:asim.ihsan.io/flutter-ffi…
原文作者:asim.ihsan.io/
发布时间:2020年6月21日
概要
Flutter是一个用于编写跨平台移动应用的Google UI工具包。通过使用目前处于测试版的dart:ffi库,现在使用Flutter的原生代码比以往任何时候都要容易。在这篇文章中,我们将演示为什么你要使用原生代码,以及一个使用libsodium加密库的实际例子。
在这篇文章的最后,你将能够。
- 为iOS和Android移动应用创建一个使用本地代码的Flutter插件。
- 使用Flutter移动应用中的libsodium原生加密库。
- 在后台运行昂贵的原生代码,避免阻塞移动应用的用户界面。
本博客中表达的观点是我自己的,不一定是我的雇主的观点。
- 介紹
- 现有技术、参考资料和其他资源
- 指南
- 先决条件
- 获得libsodium
- 为iOS构建libsodium
- 为Android构建libsodium
- 创建一个Flutter插件,并使用FFI绑定到libsodium。
- Flutter iOS插件设置
- Flutter Android插件设置
- Flutter Dart代码--琐碎的开始
- Flutter Dart代码--先封后拆。
- 参考编码
- 今后的工作和需要改进的领域
介紹
Flutter是一个软件工具箱,它可以让你一次写完代码,同时将应用部署到iOS和Android上。Dart是Flutter使用的编程语言,它的功能很强大,速度也足够快,可以满足大多数目的。然而,有时你想使用已经用其他编程语言编写并经过实战检验的代码的预先存在的原生库,例如因为。
- 你需要用一种不同的、更快的、更节省内存的语言来编写代码。例如,通过在Rust中编写一个可重用的库,你可以利用一个更强大的优化编译器,并在需要时避免垃圾收集器的开销。
- 你希望在多个领域重用相同的代码,比如你的Flutter移动应用、桌面应用和服务器。虽然Google正在开始为Flutter引入桌面和网络支持,但目前为了在多个域中共享逻辑,你可以用不同的语言在可重用的库中编写。
- 你想重用现有的代码,尤其是对安全敏感的应用,比如加密数据。你不想重写这样的代码,因为很可能引入错误,泄露信息或导致你的应用程序崩溃。相反,通过使用经过安全工程师审核的预写库,你会更有信心它能正常工作。
在这篇文章中,我们将使用一个由这三个原因驱动的例子。libsodium是一个加密库,可以让你例如加密、解密和散列数据。libsodium速度快,已经通过安全工程师的审核,并允许你在服务器上重复使用相同的代码,这样就可以更容易地在移动设备上解密加密数据。
本文将从零开始。我们将创建一个Flutter插件,为iOS和Android编译libsodium,最后演示如何从Flutter和服务器端使用libsodium。通过阅读本文,你将能够使用其他本地库的代码,而不仅仅是libsodium。
现有技术、参考资料和其他资源
Rust一次并与Android、iOS和Flutter共享是一篇精彩的文章,同样从零开始,告诉你如何将Rust编写的库与Android、iOS和Flutter共享。但是,由于本文没有使用目前处于测试阶段的新的dart:fi功能,所以有很多开销,因为你需要在Swift和Kotlin中分别编写自定义代码来与iOS和Android共享库。我将会写一篇后续文章,展示在Flutter中使用一个真实世界的Rust库例子是多么容易。
flutter_sodium是一个现有的Flutter插件,它使用新的dart:fi功能与libsodium进行绑定。通过阅读flutter_sodium的代码,我有了写这篇文章的动机,但我做了一些不同的实现选择。此外,flutter_sodium使用了预建的libsodium库,而本文将告诉你如何从头开始重新编译libsodium。
关于Flutter插件的背景知识请看。
指南
先决条件
我碰巧在$HOME/Programming中工作,但把它改成你喜欢的任何地方。
ROOT_DIR=$HOME/Programming
下载Android NDK,然后将ANDROID_NDK_HOME和NDK_HOME环境变量都设置为$HOME/Library/Android/sdk/ndk-bundle。
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk-bundle
export NDK_HOME=$HOME/Library/Android/sdk/ndk-bundle
确保你的Flutter安装没有任何高级问题,并确保你在Beta频道上获得新的Dart FFI功能。
flutter channel beta
flutter upgrade
flutter doctor -v
为了假装成服务端解密Flutter移动应用发送的数据,我们将使用pynacl Python模块。使用你的Python系统安装或安装Python与。
然后安装 pynacl 和 ipython (为了一个有用的 REPL shell)。
pip install pynacl ipython
获得libsodium
截至2020-06-14,v1.0.18是libsodium的最新稳定版本。
cd $ROOT_DIR
wget https://download.libsodium.org/libsodium/releases/libsodium-1.0.18-stable.tar.gz
tar xvf libsodium-1.0.18-stable.tar.gz
rm -f libsodium-1.0.18-stable.tar.gz
为iOS构建libsodium
libsodium提供了易于使用的编译脚本,用于编译iOS和Android的库。
这将把工件放在$ROOT_DIR/libsodium-stable/libsodium-ios中。我们指定了LIBSODIUM_FULL_BUILD,这样我们就可以公开所有的API,而不仅仅是高级API。
cd $ROOT_DIR/libsodium-stable
# Clean up from previous builds
test -d libsodium-ios || rm -rf libsodium-ios
./configure && make distclean
LIBSODIUM_FULL_BUILD=true ./dist-build/ios.sh
如果最后成功,你会看到所有架构的单一二进制文件的路径。
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-ios
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a: Mach-O universal binary with 5 architectures: [i386:current ar archive random library] [arm_v7:current ar archive random library] [arm_v7s] [x86_64] [arm64]
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a (for architecture i386): current ar archive random library
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a (for architecture armv7): current ar archive random library
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a (for architecture armv7s): current ar archive random library
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a (for architecture x86_64): current ar archive random library
/Users/asimi/Programming/libsodium-stable/libsodium-ios/lib/libsodium.a (for architecture arm64): current ar archive random library
为Android构建libsodium
同样,我们将使用现有的libsodium构建脚本为Android构建库。
cd $ROOT_DIR/libsodium-stable
# Clean up from previous builds
./configure && make distclean
LIBSODIUM_FULL_BUILD=true ./dist-build/android-arm.sh
LIBSODIUM_FULL_BUILD=true ./dist-build/android-armv7-a.sh
LIBSODIUM_FULL_BUILD=true ./dist-build/android-armv8-a.sh
LIBSODIUM_FULL_BUILD=true ./dist-build/android-x86.sh
LIBSODIUM_FULL_BUILD=true ./dist-build/android-x86_64.sh
输出将在这里(注意westmere是x86_64)。
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-android-armv6
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-android-armv7-a
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-android-armv8-a
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-android-i686
libsodium has been installed into /Users/asimi/Programming/libsodium-stable/libsodium-android-westmere
创建一个Flutter插件,并使用FFI绑定到libsodium。
让我们创建一个全新的空Flutter插件。
cd $ROOT_DIR
flutter create --template=plugin flutter_libsodium
Flutter iOS插件设置
将周围的库复制到正确的位置。
cp $ROOT_DIR/libsodium-stable/libsodium-ios/lib/libsodium.a $ROOT_DIR/flutter_libsodium/ios/
更新 iOS ios/flutter_libsodium.podspec 文件以包含二进制库。
diff --git a/ios/flutter_libsodium.podspec b/ios/flutter_libsodium.podspec
index 0ae9b0f..e4ad522 100644
--- a/ios/flutter_libsodium.podspec
+++ b/ios/flutter_libsodium.podspec
@@ -16,8 +16,10 @@ A new flutter plugin project.
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
+ s.vendored_libraries = 'libsodium.a'
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.swift_version = '5.0'
+ s.xcconfig = { 'OTHER_LDFLAGS' => '-force_load "${PODS_ROOT}/../.symlinks/plugins/flutter_libsodium/ios/libsodium.a"'}
end
Flutter Android插件设置
将库的周围复制到正确的位置。
mkdir -p $ROOT_DIR/flutter_libsodium/android/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86,x86_64}
cp $ROOT_DIR/libsodium-stable/libsodium-android-armv7-a/lib/libsodium.so \
$ROOT_DIR/flutter_libsodium/android/src/main/jniLibs/armeabi-v7a
cp $ROOT_DIR/libsodium-stable/libsodium-android-armv8-a/lib/libsodium.so \
$ROOT_DIR/flutter_libsodium/android/src/main/jniLibs/arm64-v8a
cp $ROOT_DIR/libsodium-stable/libsodium-android-i686/lib/libsodium.so \
$ROOT_DIR/flutter_libsodium/android/src/main/jniLibs/x86
cp $ROOT_DIR/libsodium-stable/libsodium-android-westmere/lib/libsodium.so \
$ROOT_DIR/flutter_libsodium/android/src/main/jniLibs/x86_64
Flutter Dart--琐碎的开始
下面是一些初始化libsodium库的代码。在使用libsodium的任何部分之前,你首先需要调用sodium_init()。之后让我们练习只获取版本字符串,应该返回 "1.0.18"。
首先将 ffi 作为一个新的依赖项添加到 pubspec.yaml 中。
diff --git a/pubspec.yaml b/pubspec.yaml
index 8c63764..8247ec0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,6 +9,7 @@ environment:
flutter: ">=1.10.0"
dependencies:
+ ffi: ^0.1.3
flutter:
sdk: flutter
然后创建一个新的文件lib/libsodium_bindings.dart,它将包含第一个使用FFI直接与原生库对话的层。
library bindings;
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
final libsodium = _load();
DynamicLibrary _load() {
if (Platform.isAndroid) {
return DynamicLibrary.open("libsodium.so");
} else {
return DynamicLibrary.process();
}
}
// https://doc.libsodium.org/quickstart#boilerplate
// https://github.com/jedisct1/libsodium/blob/2d5b954/src/libsodium/sodium/core.c#L27-L53
typedef NativeInit = Int32 Function();
typedef Init = int Function();
final Init sodiumInit = libsodium.lookupFunction<NativeInit, Init>('sodium_init');
// https://github.com/jedisct1/libsodium/blob/927dfe8/src/libsodium/sodium/version.c#L4-L8
typedef NativeVersionString = Pointer<Utf8> Function();
typedef VersionString = Pointer<Utf8> Function();
final VersionString sodiumVersionString =
libsodium.lookupFunction<NativeVersionString, VersionString>('sodium_version_string');
我从Dart SDK单元测试中借鉴了这种使用typedef's和lookupFunction进行绑定的风格。请注意这些绑定是多么机械和无聊。这是故意的--应该可以从libsodium中自动生成这些发现。
现在,我们在绑定的基础上创建一个lib/libsodium_wrapper.dart。它与我们的绑定层对话,创建方便的包装器,并最终代表我们管理内存。
import 'package:ffi/ffi.dart';
import 'package:flutter_libsodium/libsodium_bindings.dart' as bindings;
class LibsodiumError extends Error {}
class LibsodiumCouldNotInitError extends LibsodiumError {}
class LibsodiumWrapper {
LibsodiumWrapper() {
if (sodiumInit() < 0) {
throw LibsodiumCouldNotInitError();
}
}
int sodiumInit() {
return bindings.sodiumInit();
}
String sodiumVersionString() {
return Utf8.fromUtf8(bindings.sodiumVersionString());
}
}
String getSodiumVersionString(final LibsodiumWrapper wrapper) => wrapper.sodiumVersionString();
创建一个包装器似乎毫无意义,但当我们在下面介绍一个非平凡的例子时,你会明白为什么它很有用。至少它提醒我们调用钠_init()并检查它的返回值。
请注意,sodium_version_string不会在堆上malloc内存,所以我们不需要释放返回值。当我们下面介绍一个非平凡的例子时,我将会更多地谈论内存管理。
还要注意最后一行奇怪的函数定义,是因为为了使用compute进行异步调用,"回调参数必须是一个顶层函数,而不是一个闭包或一个类的实例或静态方法"。
在flutter_libsodium分支part1中看一下example子文件夹,了解如何使用该库并对其进行集成测试。
- example/lib/main.dart是使用方法。
- example\test_driver\app_test.dart 是集成测试。
要运行集成测试,像往常一样运行。
cd example
flutter drive --target=test_driver/app.dart --android-emulator
Flutter Dart代码--先封后拆。
这是一个比较复杂、现实的例子,你想在设备上加密一些东西,然后在服务器上解密。此外,让我们假设我们想在设备上加密数据,使设备或其他对手不可能解密它所加密的东西,也不可能在不被发现的情况下修改数据。
给我们提供这些基元的密码学基元叫做 "密封",libsodium把这些密封的盒子叫做 "密封盒",这个概念来源于Gifford(1981)的 "Cryptographic Sealing for Information Secrecy and Authentication"。
首先让我们打开一个新的终端窗口,在服务器端生成一个公钥/私钥对。启动一个Python shell。
ipython
然后生成服务器密钥对。
import base64
from nacl.public import PrivateKey
keypair = PrivateKey.generate()
print("Public key: " + base64.b64encode(keypair.public_key.encode()).decode('utf-8'))
print("Private key: " + base64.b64encode(keypair.encode()).decode('utf-8'))
结果:
Public key: lKSTP8K5YQoHMZOn2+mTLunP3yMgqN1O8GyaqRvHbQE=
Private key: +YownzrW+Bx2dmpQAjuQJr5SEAwd6Bg5NUDHVfKRIY4=
让我们使用Flutter中的服务器公钥来密封消息。这里面涉及到很多锅炉模板代码,所以一定要看flutter_libsodium分支part2,尤其是实现seal box的提交,了解所有的代码。不过这里有一些关于part1分支的重点需要注意。
从绑定的顶部开始,注意我们是如何绑定到 crypto_box_seal API 的。
// int crypto_box_seal(unsigned char *c, const unsigned char *m,
// unsigned long long mlen, const unsigned char *pk);
typedef CryptoBoxSeal = int Function(
Pointer<Uint8> c, Pointer<Uint8> m, int mlen, Pointer<Uint8> pk);
typedef NativeCryptoBoxSeal = Int32 Function(
Pointer<Uint8> c, Pointer<Uint8> m, Uint64 mlen, Pointer<Uint8> pk);
final CryptoBoxSeal cryptoBoxSeal =
libsodium.lookupFunction<NativeCryptoBoxSeal, CryptoBoxSeal>('crypto_box_seal');
unsigned char*是C的内存块,在Dart FFI中,它对应的是Pointer<Uint8>。这些指针必须是指向本地管理的内存。但是如果你从Dart字符串开始,如何在本机内存中得到一个Pointer<Uint8>呢?在这里,我们使用Dart的一个非常方便的特性来扩展String对象,并创建一个新的toUint8Pointer()方法,使用libsodium的安全内存分配sodium_malloc()方法,然后复制过来String的原始字节。
extension StringExtensions on String {
Pointer<Uint8> toUint8Pointer() {
if (this == null) {
return Pointer<Uint8>.fromAddress(0);
}
final units = utf8.encode(this);
final Pointer<Uint8> result = bindings.sodiumMalloc(units.length);
final Uint8List nativeString = result.asTypedList(units.length);
nativeString.setAll(0, units);
return result;
}
}
为什么我使用libsodium sodium_malloc分配内存,而不是使用Dart FFI分配API? sodium_malloc速度较慢,但提供了以下功能。
- 在分配内存之前和之后都会创建保护页;如果程序访问保护页,应用程序就会崩溃。这提供了对缓冲区溢出的深度防御。
- 分配的内存被
mlock()'d,以避免它被交换到磁盘或成为内存转储的一部分。
这些特性提供了深度防御,但最终你还需要避免为敏感数据(如明文)分配像String这样的Dart对象;详见下面的 "未来工作和需要改进的地方 "部分。
我们向其他类添加其他帮助扩展,因此可以围绕底层的 crypto_box_seal 原生调用提出一个包装器。请注意,crypto_box_sealbytes是libsodium添加到加密密文中的开销(32字节的外延公钥,16字节的HMAC)。
// https://doc.libsodium.org/public-key_cryptography/sealed_boxes
String cryptoBoxSeal(final String recipientPublicKeyBase64Encoded, final String plaintext) {
final int cryptoBoxSealBytes = bindings.crypto_box_SEALBYTES();
final cLength = plaintext.length + cryptoBoxSealBytes;
final c = bindings.sodiumMalloc(cLength);
final m = plaintext.toUint8Pointer();
final Uint8List recipientPublicKey = base64.decode(recipientPublicKeyBase64Encoded);
final pk = recipientPublicKey.toPointer();
try {
bindings.cryptoBoxSeal(c, m, plaintext.length, pk);
final Uint8List result = c.toList(cLength);
return base64.encode(result);
} finally {
bindings.sodiumFree(c);
bindings.sodiumFree(m);
bindings.sodiumFree(pk);
}
}
最后在实际的UI中,我们想在按钮被按下时加密一些文本。如果你在UI线程中执行这个计算量很大的调用,你会阻塞它并导致jank。jank意味着你阻塞UI线程的时间太长,以至于干扰了用户界面;可能用户输入被忽略,或者动画帧被跳过。因此我们需要在不同的线程上执行这个计算。Flutter提供了一个方便的函数compute,但有一个限制是只能传递一个参数给compute,因此我们创建一个方便类来封装参数。
Future<void> encryptData(final String plaintext) async {
final encryptedData = await compute(
cryptoBoxSeal, CryptoBoxSealCall(wrapper, serverPublicKeyBase64Encoded, plaintext));
setState(() {
_encryptedData = encryptedData;
});
}
如果你运行代码的part2分支,每次加密一些数据,你都会得到不同的密文,因为libsodium对每个密封盒的调用都使用一个全新的历时公钥/私钥对。在一个特定的运行中,当我加密foobar时,我得到了zZSCwppjzaneb4f6a1HEWo4GL8RiN8oGILzMaaM8Mz7/97J4+8EEEfbQHDBGp3A1juOFWv/Z。
一旦你有了base64编码的密封盒(即加密数据),想象一下你已经以某种方式将其传输到服务器上。然后你可以在服务器上解密它。
import base64
from nacl.public import PrivateKey, SealedBox
private_key_encoded = "+YownzrW+Bx2dmpQAjuQJr5SEAwd6Bg5NUDHVfKRIY4="
private_key = PrivateKey(base64.b64decode(private_key_encoded))
unseal_box = SealedBox(private_key)
ciphertext = "zZSCwppjzaneb4f6a1HEWo4GL8RiN8oGILzMaaM8Mz7/97J4+8EEEfbQHDBGp3A1juOFWv/Z"
new_plaintext = unseal_box.decrypt(base64.b64decode(ciphertext)).decode('utf-8')
print(new_plaintext)
这将如期返回foobar。
参考代码
请看一下 flutter_libsodium GitHub 仓库,特别是标签 part1 和 part2。
未来的工作和需要改进的地方
为了方便,我跳过了Flutter中的crypto_box_seal调用的错误处理,以及服务器上的crypto_box_unseal调用。当然,你要处理错误!
当与本地代码交互时,你需要与生活在内存中某个地方的数据交互。如果你从Dart运行时管理的内存开始,你需要把它复制到本地管理的内存中,以便让本地库访问它。这是很浪费的。让本地库访问数据的最有效的内存方式是仔细确保你分配本地内存,然后通过视图使用它。这样就不需要从Dart复制到native,而且FFI已经给出了简单的方式,所以从native到Dart。当你在自己的应用程序中工作时,可以考虑是否可以直接使用本地内存的Pointer<Uint8>指针。
与其有单独的libsodium和Flutter绑定的目录,不如为这两个目录设置一个单独的目录,把libsodium作为Git子模块来查看,然后创建一个构建脚本来自动构建libsodium,并把它的二进制文件复制过来,这样更容易维护。不过,我不知道Flutter插件是否支持这样定制自己的构建过程。
正如文章中所讨论的那样,应该可以自动解析libsodium的C头和代码,以便生成FFI。
通过www.DeepL.com/Translator(免费版)翻译