更新您的安全提供程序以防范 SSL 攻击

Android 依靠安全 Provider 提供安全的网络通信。不过,我们会不时地在默认安全提供程序中发现漏洞。为了这些漏洞引发问题,您可以通过 Google Play 服务自动更新设备的安全提供程序,从而防范已知攻击。通过调用 Google Play 服务方法,您的应用可以确保其在具有最新更新的设备上运行以防范已知攻击。

例如,在 OpenSSL (CVE-2014-0224) 中发现了一个漏洞,该漏洞会让应用受到可以解密安全流量的“中间人”攻击,而通信双方对此却毫不知情。Google Play 服务版本 5.0 提供了该漏洞的修复,但应用必须确保已安装此修复。通过使用 Google Play 服务方法,您的应用可以确保其在可以防范该攻击的设备上运行。

注意:更新设备的安全 Provider 不会更新 android.net.SSLCertificateSocketFactory我们建议应用开发者使用高级方法与加密进行交互,而不是使用此类。大多数应用都可以使用 HttpsURLConnection 之类的 API,无需设置自定义 TrustManager 或创建 SSLCertificateSocketFactory

使用 ProviderInstaller 为安全提供程序打补丁

要更新设备的安全提供程序,请使用 ProviderInstaller 类。通过调用该类的 installIfNeeded()(或 installIfNeededAsync())方法,您可以验证安全提供程序是否处于最新状态(如果需要,请对其进行更新)。

当您调用 installIfNeeded() 时,ProviderInstaller 将执行以下操作:

  • 如果设备的 Provider 已成功更新(或者已经处于最新状态),此方法将正常返回。
  • 如果设备的 Google Play 服务库已过期,此方法会引发 GooglePlayServicesRepairableException。然后,应用可以捕获此异常,并向用户显示相应的对话框以更新 Google Play 服务。
  • 如果出现不可恢复的错误,此方法将引发 GooglePlayServicesNotAvailableException 以表明它无法更新 Provider。然后,应用可以捕获异常,并选择相应的操作,如显示标准的修复流程图

installIfNeededAsync() 方法的行为方式相似,只是不会引发异常,而是调用相应的回调方法以表明更新成功还是失败。

如果 installIfNeeded() 需要安装新的 Provider,所需的时间从 30-50 毫秒(在最新的设备上)到 350 毫秒(在较旧的设备上)不等。如果安全提供程序已经处于最新状态,此方法需要的时间微不足道。为避免影响用户体验,请执行以下操作:

  • 在加载线程时立即从后台网络线程调用 installIfNeeded(),而不是等待线程尝试使用网络。(多次调用此方法没有任何坏处,因为如果安全提供程序不需要更新,它会立即返回。)
  • 如果用户体验受线程拦截影响(例如,如果调用来自 UI 线程中的某个 Activity),请调用此方法的异步版本,即 installIfNeededAsync()。(当然,如果执行此操作,您需要等待操作完成后再尝试任何安全的通信。ProviderInstaller 会调用侦听器的 onProviderInstalled() 方法来指示成功。)

警告:如果 ProviderInstaller 无法安装更新的 Provider,您的设备安全提供程序可能容易受到已知攻击。您的应用应表现为好像所有 HTTP 通信都未加密。

在更新 Provider 后,对安全 API(包括 SSL API)的所有调用均通过它来路由。(不过,这不适用于 android.net.SSLCertificateSocketFactory,其仍然容易受到类似 CVE-2014-0224 的攻击。)

同步打补丁

为安全提供程序打补丁的最简单方法是调用同步方法 installIfNeeded()。如果在等待操作完成时用户体验不会受到线程拦截影响,则该方法非常适用。

例如,下面是一个可以更新安全提供程序的同步适配器的实现。由于同步适配器在后台运行,因此,如果正在等待安全提供程序更新,则线程可以实施拦截。同步适配器将调用 installIfNeeded() 来更新安全提供程序。如果此方法正常返回,则同步适配器知道安全提供程序处于最新状态。如果此方法引发异常,则同步适配器可以进行适当的操作(如提示用户更新 Google Play 服务)。

/**
 * Sample sync adapter using {@link ProviderInstaller}.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

  ...

  // This is called each time a sync is attempted; this is okay, since the
  // overhead is negligible if the security provider is up-to-date.
  @Override
  public void onPerformSync(Account account, Bundle extras, String authority,
      ContentProviderClient provider, SyncResult syncResult) {
    try {
      ProviderInstaller.installIfNeeded(getContext());
    } catch (GooglePlayServicesRepairableException e) {

      // Indicates that Google Play services is out of date, disabled, etc.

      // Prompt the user to install/update/enable Google Play services.
      GooglePlayServicesUtil.showErrorNotification(
          e.getConnectionStatusCode(), getContext());

      // Notify the SyncManager that a soft error occurred.
      syncResult.stats.numIOExceptions++;
      return;

    } catch (GooglePlayServicesNotAvailableException e) {
      // Indicates a non-recoverable error; the ProviderInstaller is not able
      // to install an up-to-date Provider.

      // Notify the SyncManager that a hard error occurred.
      syncResult.stats.numAuthExceptions++;
      return;
    }

    // If this is reached, you know that the provider was already up-to-date,
    // or was successfully updated.
  }
}

异步打补丁

更新安全提供程序需要 350 毫秒的时间(在较旧的设备上)。如果在直接影响用户体验的线程(如 UI 线程)上进行更新,您一定不希望进行同步调用以更新提供程序,因为这会导致应用或设备冻结,直至操作完成。相反,您应该使用异步方法 installIfNeededAsync()。该方法通过调用回调指示其成功还是失败。

例如,下面是可以在 UI 线程的某个 Activity 中更新安全提供程序的一些代码。此 Activity 会调用 installIfNeededAsync() 来更新提供程序,并将自身指定为接收成功或失败通知的侦听器。如果安全提供程序为最新或已成功更新,将调用此 Activity 的 onProviderInstalled() 方法,且 Activity 知道通信是安全的。如果提供程序无法更新,将调用此 Activity 的 onProviderInstallFailed() 方法,且 Activity 可以进行适当的操作(如提示用户更新 Google Play 服务)。

/**
 * Sample activity using {@link ProviderInstaller}.
 */
public class MainActivity extends Activity
    implements ProviderInstaller.ProviderInstallListener {

  private static final int ERROR_DIALOG_REQUEST_CODE = 1;

  private boolean mRetryProviderInstall;

  //Update the security provider when the activity is created.
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ProviderInstaller.installIfNeededAsync(this, this);
  }

  /**
   * This method is only called if the provider is successfully updated
   * (or is already up-to-date).
   */
  @Override
  protected void onProviderInstalled() {
    // Provider is up-to-date, app can make secure network calls.
  }

  /**
   * This method is called if updating fails; the error code indicates
   * whether the error is recoverable.
   */
  @Override
  protected void onProviderInstallFailed(int errorCode, Intent recoveryIntent) {
    if (GooglePlayServicesUtil.isUserRecoverableError(errorCode)) {
      // Recoverable error. Show a dialog prompting the user to
      // install/update/enable Google Play services.
      GooglePlayServicesUtil.showErrorDialogFragment(
          errorCode,
          this,
          ERROR_DIALOG_REQUEST_CODE,
          new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
              // The user chose not to take the recovery action
              onProviderInstallerNotAvailable();
            }
          });
    } else {
      // Google Play services is not available.
      onProviderInstallerNotAvailable();
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
      Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
      // Adding a fragment via GooglePlayServicesUtil.showErrorDialogFragment
      // before the instance state is restored throws an error. So instead,
      // set a flag here, which will cause the fragment to delay until
      // onPostResume.
      mRetryProviderInstall = true;
    }
  }

  /**
   * On resume, check to see if we flagged that we need to reinstall the
   * provider.
   */
  @Override
  protected void onPostResume() {
    super.onPostResult();
    if (mRetryProviderInstall) {
      // We can now safely retry installation.
      ProviderInstall.installIfNeededAsync(this, this);
    }
    mRetryProviderInstall = false;
  }

  private void onProviderInstallerNotAvailable() {
    // This is reached if the provider cannot be updated for some reason.
    // App should consider all HTTP communication to be vulnerable, and take
    // appropriate action.
  }
}