Android 更新和 TLS 连接

Android 设备供应商以其较短的支持期而闻名。他们只在有限的时间内提供更新,甚至提供带有旧 Android 版本的设备。然后,开发者必须处理分散的 Android 版本分布。Google Play 上大约 2% 的活跃设备仍在使用 Android 4.x。旧手机通常仍然具有不错的性能,并且使用自定义 ROM 对其进行更新使它们直到今天仍然非常好用。不幸的是,并非所有设备都支持自定义 ROM,也不是所有用户都想处理安装自定义 ROM 的麻烦。

AntennaPod 是我维护的 Android 开源播客管理器。AntennaPod 不依赖于集中式服务器,而是直接使用设备获取播客订阅。这对用户的隐私非常有利,因为没有人会获得你订阅的完整列表。不幸的是,越来越多使用旧 Android 版本的用户在使用某些播客源时遇到问题。原因是服务器已更新到这些设备上不可用的较新的 TLS 版本。升级服务器的 TLS 版本绝对是一件好事,因为它使通信更加安全。这就是为什么大多数浏览器都提供自己的堆栈,它们可以独立于设备供应商进行更新。不幸的是,所有其他依赖于连接到许多不受其控制的服务器的应用,例如 AntennaPod,仍然会遇到问题。然后用户会看到如下下载错误:Failure in SSL Library, usually a protocol error。这很糟糕,因为它会阻止用户收听播客。虽然这基本上是设备供应商的错,但最终会波及应用开发者。

从 Android 4.1 开始支持 TLSv1.1TLSv1.2,并在 Android 5.0 中默认启用。还有一些密码套件和证书在 Android 4.x 中不受支持。虽然此问题目前仅影响 Android 4.x 用户,但随着服务器升级到 TLSv1.3,未来会出现更多问题。TLSv1.3 最初是随 Android 10 一起提供的,它在 Google Play 上大约 8% 的活动设备上运行。你可以在 SSLSocket 参考页面上找到有关 Android 中 SSL 支持的更多详细信息。开发者现在必须处理供应商忽视软件更新的事实。

这是一个已知问题,因此 Google 发布了一个名为 ProviderInstaller 的库。它非常易于使用 (ProviderInstaller.installIfNeeded(context);) 并立即解决所有这些问题。由于提供程序独立于使用 Google Play 服务的应用保持最新状态,因此应用开发者不需要再担心这一点。

这听起来好得令人难以置信,不是吗?好吧,Google 的 ProviderInstaller 有一个很大的缺点。该库是闭源的,因此不能用于在 F-Droid 上发布的应用。AntennaPod 目前构建有两种不同的风格:一种是用于 F-Droid 的 100% 自由软件,另一种是用于 Google Play 商店的 ProviderInstaller 库。这意味着 F-Droid 用户被抛在了后面。他们仍然会遇到与某些服务器的连接问题,并认为这是 AntennaPod 中的错误。

与 F-Droid 一起使用的替代方法是包含开源库 Conscrypt。在 AntennaPod 中捆绑库最初是由 @Slinger 在对 AntennaPod 的拉取请求中提出的。虽然将 Conscrypt 与应用捆绑起来就像使用 Google 的 ProviderInstaller 一样简单,但它有两个主要缺点:

  • 应用开发者被迫定期更新提供程序。这对维护人员来说是额外的工作,并且可能会给开发周期缓慢的应用带来麻烦。
  • 捆绑 Conscrypt 会增加 apk 的大小。在 AntennaPod 的情况下,这意味着应用程变大 4 MB 左右,仅针对这种几乎不可见的变化就增加了 40%。现在想想每个应用都必须这样做才能获得更新的 TLS 堆栈。如果 F-Droid 上处理网络的每个应用都需要捆绑额外的 4 MB TLS 库,这将显着增加存储使用量。

幸运的是,有一个解决方案!这个想法是编写一个开源提供程序应用(就像 Google 的提供程序一样),它除了捆绑 Conscrypt 并提供允许其他应用加载它的稳定 API 之外什么都不做。然后可以独立于 AntennaPod 或 F-Droid 等应用进行更新。提供者应用可以简单地拥有一个类似这样的类:

public static void install() {
    Security.insertProviderAt(Conscrypt.newProvider(), 1);
    Log.d(TAG, "Provider installed.");
}

然后,我们可以开发一个开源库,使用 ClassLoader 来包含来自提供程序应用的 Conscrypt 库。

Context targetContext = context.createPackageContext("com.bytehamster.providerinstaller",
        Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = targetContext.getClassLoader();
Class installClass = classLoader.loadClass("com.bytehamster.providerinstaller.ProviderInstallerImpl");
Method installMethod = installClass.getMethod("install", new Class[]{ });
installMethod.invoke(null);
installed = true;

GitHub Gist 上也提供了代码示例(并且更详细一些)。

显然,直接包含来自另一个应用的代码是一种安全风险。通过包含提供程序,你在应用的上下文中执行外部代码。你如何验证提供者没有恶意?有一些方法可以验证提供者是否可信:

  • 让受信任的应用成为提供者。F-Droid 应用本身可以捆绑提供程序,保持更新并提供稳定的 API。然后应用可以对 F-Droid 包名称进行硬编码并验证其签名。这种方法的问题在于,自己构建 F-Droid 或使用 G-Droid 等替代品的用户无法使用更新后的库。
  • 该库可以向 Android 系统查询以获取兼容的 Conscrypt 提供程序应用并包含其中之一。为确保这是安全的,可以仅在它是系统应用时包含提供程序。然后 F-Droid 特权扩展可以捆绑并提供 Conscrypt。许多 F-Droid 用户可能不使用特权扩展,因此我们仍然无法覆盖所有用户。
  • 对我来说,最好的解决方案似乎有一个提供 Conscrypt 的独立应用(如 org.fdroid.securityprovider)。在 F-Droid 论坛上也对此进行了讨论并且最初由 @bubu 建议。像 F-Droid 这样的受信任实体可以构建提供程序并使用他们的密钥对其进行签名。然后可以由想要包含 Conscrypt 的调用应用进行验证。

我很想看到这个方向的一些变化。我们绝对应该探索完全开源的安全提供程序应用的可能性。不幸的是,我正忙于维护 AntennaPod。因此,我无法维护库和安装程序应用。GitHub Gist 上有一个概念验证,但它缺少安全检查。此外,添加一些简单的方法来提示用户在 TLS 失败时安装提供程序可能会很好。如果你有兴趣使开源提供程序成为可能,请查看 F-Droid 论坛

一些说明:

  • 这篇文章是从应用开发者的角度编写的。我试图让 AntennaPod 适合所有用户,包括那些使用 Android 4.x 的用户。从用户的角度来看,你应该考虑是否仍要使用旧的 Android 版本,例如 4.x。Android 4.4 发布于 2014 年,也就是 6 年前。从那时起,许多关键的安全问题已得到修复。将这样的旧设备暴露在互联网上可能会带来相当大的安全风险,这与应用中包含的 TLS 库无关。
  • 在研究这个之前,我不知道你可以使用 ClassLoader 来执行其他应用的代码。这非常好,可以用于插件之类的东西。我很想看看其他开发者用它构建了什么。