实现移动端到端加密的基础(二)

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++层中的FrameEncryptorInterfaceFrameDecryptorInterface

加密器会设置在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

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营