iOS系统
我们尝试用iOS的本地桥接机制进行初始化。但Olm.h和Olm.mm包含模块实现起来会更简单,其中jsiadapter::install在setBridge方法里被调用,暴露host函数。
暴露host函数
如上所述,Android和iOS的专有代码最终都会调用跨平台的jsiadapter::install方法。C++方法也在这里被暴露。即JS对象被设置在jsiRuntime.global上,其方法直接调用到了C++代码中。
Object module = Object(jsiRuntime);
//…add methods to module
jsiRuntime.global().setProperty(jsiRuntime, "_olm", move(module));
通过一个全局变量,上述代码中的对象就可以在JS端被访问。就我们的用例来说,一个对象就足够了。但如有必要,无论有多少对象都可以在这里暴露,且不用改变平台的专有代码。
向暴露对象添加方法
auto createOlmAccount = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "createOlmAccount"), 0,
[](Runtime &runtime, const Value &thisValue, const Value *arguments,
size_t count) -> Value {
auto acountHostObject = AccountHostObject(&runtime);
auto accountJsiObject = acountHostObject.asJsiObject();
return move(accountJsiObject);
});
module.setProperty(jsiRuntime, "createOlmAccount", move(createOlmAccount));
auto createOlmSession = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "createOlmSession"), 0,
[](Runtime &runtime, const Value &thisValue, const Value *arguments,
size_t count) -> Value {
auto sessionHostObject = SessionHostObject(&runtime);
auto sessionJsiObject = sessionHostObject.asJsiObject();
return move(sessionJsiObject);
});
module.setProperty(jsiRuntime, "createOlmSession", move(createOlmSession));
此处暴露了两个方法:createOlmAccount和createOlmSession,都返回HostObjects。
HostObject
HostObject是一个可以在JS运行时注册的C++对象。即暴露的方法可以从JS代码中调用。但它也可以在JS和C++之间来回传递,同时仍是一个完全可操作的C++对象。
就我们的用例来说,AccountHostObject和SessionHostObject是对本地特定olm对象OlmAccount和OlmSession的封装。它们包含了可以被JS代码调用的方法(AccountHostObject对应identity_keys、generate_one_time_keys、one_time_keys等; SessionHostObject对应create_outbound、create_inbound、encrypt、decrypt等)。
上述方法从C++到JS的暴露方式还是通过host函数,在HostObject::get方法中:
Value SessionHostObject::get(Runtime &rt, const PropNameID &sym) {
if (methodName == "create_outbound") {
return Function::createFromHostFunction(
*runtime, PropNameID::forAscii(*runtime, "create_outbound"), 0,
[](Runtime &runtime, const Value &thisValue, const Value *arguments,
size_t count) -> Value {
auto sessionJsiObject = thisValue.asObject(runtime);
auto sessionHostObject =
sessionJsiObject.getHostObject<SessionHostObject>(runtime).get();
auto accountJsiObject = arguments[0].asObject(runtime);
auto accountHostObject =
accountJsiObject.getHostObject<AccountHostObject>(runtime).get();
auto identityKey = arguments[1].asString(runtime).utf8(runtime);
auto oneTimeKey = arguments[2].asString(runtime).utf8(runtime);
sessionHostObject->createOutbound(accountHostObject->getOlmAccount(),
identityKey, oneTimeKey);
return Value(true);
});
}
}
例如:
const olmAccount = global._olm.createOlmAccount();
const olmSession = global._olm.createOlmSession();
olmSession.create_outbound(olmAccount, “someIdentityKey”, “someOneTimeKey”);
如图所示,global._olm.createOlmAccount()和global._olm.createOlmSession()会返回一个HostObject。当对其调用任何方法时(比如示例中的create_outbound),HostObject::get方法将被调用,并带有适当参数,即Runtime和方法名称。然后我们就能用这个方法名称来暴露所需行为。
需要注意:调用的HostObject可以在C++端完全重构。
auto sessionJsiObject = thisValue.asObject(runtime);
auto sessionHostObject =
sessionJsiObject.getHostObject<SessionHostObject>(runtime).get();
参数也可以从JS传递到C++,包括其他HostObject。
auto accountJsiObject = arguments[0].asObject(runtime);
auto accountHostObject =
accountJsiObject.getHostObject<AccountHostObject>(runtime).get();
auto identityKey = arguments[1].asString(runtime).utf8(runtime);
auto oneTimeKey = arguments[2].asString(runtime).utf8(runtime);
完全一致的接口
就像一开始提到的,保持web和移动端界面一致是主要目标。因此,在实现了所有必要的JSI功能后,所有的功能都被封装成了美观的TypeScript类——帐户和会话。
其用法如下,包含在SDK附带的集成示例中:
const olmAccount = new Olm.Account();
olmAccount.create();
const identityKeys = olmAccount.identity_keys();
const olmSession = new Olm.Session();
olmSession.create();
olmSession.create_outbound(olmAccount, idKey, otKey);
这里的API和olm JS包暴露的API完全相同。大功告成了!
下一步
实现暴露libolm功能的RN库是移动E2EE项目中的一步。接下里它会被集成到Jitsi Meet应用程序中,用于实现每个参与者之间的E2EE通信通道,即用于交换密钥。
密钥生成
由于WebCrypto API在RN中不可用,我们必须暴露密钥生成方法的一个子集(导入、导出、生成随机字节)。再一次,我们计划通过JSI来实现这一步。
之后我们发现olm库包含这些方法,所以我们应该可以在react-native-olm库中暴露它们。
实际执行媒体加密/解密
WebRTC提供了一个简单的API,使我们能够获得与web上“insertable streams”相同的结果。在C++层中的FrameEncryptorInterface和FrameDecryptorInterface。
加密器会设置在RTPSender上,解密器在RTPReceiver上。它们只会作为每个被发送/接收的帧的代理,添加逻辑来从每个被发送/接收的帧里,构建/解构SFrame。
这段代码是否能在本地端运行非常重要。因为在我们的用例中,是否能解决JS和本地间的通信引起的性能问题很关键。每一帧的每一秒里,这些操作都必须做很多次,可能会使音视频流不连贯。
唯一要从JS端进行的操作是启用E2EE和密钥交换步骤。我们不得不把设置AES-GCM密钥的方法从JS暴露给本地的FrameEncryptors和FrameDecryptors,很可能会使用JSI路径。
Vodozemac诞生
在我们焦头烂额时,Matrix的同事们创建了* vodozemac——一种新的Rust libolm实现,它强烈建议我们迁移到这个SDK中去。目前,它只限JS和Python绑定,C++绑定还在努力开发中。我们会密切关注其进展,在我们整理好后更新到vodozemac。
上链接
GitHub repo在此,可以自己试着操作了。
文章地址:https://jitsi.org/blog/a-stepping-stone-towards-end-to-end-encryption-on-mobile/
原文作者:Titus Moldovan