更新您的安全提供程序以防范 SSL 漏洞

Android 依靠安全 Provider 提供安全的网络通信。但是,默认安全提供程序偶尔会出现漏洞。为了防范这些漏洞,Google Play 服务 提供了一种方法来自动更新设备的安全提供程序以防范已知的漏洞利用。通过调用 Google Play 服务方法,您可以帮助确保您的应用在安装了最新更新以防范已知漏洞利用的设备上运行。

例如,在 OpenSSL 中发现了一个漏洞(CVE-2014-0224),该漏洞可能使应用容易受到路径攻击,该攻击可以在任何一方不知情的情况下解密安全流量。Google Play 服务版本 5.0 提供了一个修复程序,但应用必须检查此修复程序是否已安装。通过使用 Google Play 服务方法,您可以帮助确保您的应用在针对该攻击的安全设备上运行。

注意:更新设备的安全 Provider 不会更新 android.net.SSLCertificateSocketFactory,后者仍然存在漏洞。我们建议应用开发者不要使用此已弃用的类,而是使用与加密交互的高级方法,例如 HttpsURLConnection

使用 ProviderInstaller 修补安全提供程序

要更新设备的安全提供程序,请使用 ProviderInstaller 类。您可以通过调用该类的 installIfNeeded()(或 installIfNeededAsync())方法来验证安全提供程序是否为最新版本(并在必要时更新)。本节在高级别描述了这些选项。后续部分将提供更详细的步骤和示例。

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

  • 如果设备的 Provider 成功更新(或已经是最新版本),则该方法在不引发异常的情况下返回。
  • 如果设备的 Google Play 服务库已过时,则该方法会引发 GooglePlayServicesRepairableException。然后,应用可以捕获此异常并向用户显示适当的对话框以更新 Google Play 服务。
  • 如果发生不可恢复的错误,则该方法会引发 GooglePlayServicesNotAvailableException 以指示它无法更新 Provider。然后,应用可以捕获异常并选择适当的操作,例如显示标准的 修复流程图

installIfNeededAsync() 方法的行为类似,只是它不会引发异常,而是调用相应的回调方法来指示成功或失败。

如果安全提供程序已经是最新版本,则installIfNeeded()花费的时间可以忽略不计。如果该方法需要安装新的Provider,则可能需要30-50毫秒(在较新的设备上)到350毫秒(在较旧的设备上)。为了避免影响用户体验

  • 在后台网络线程加载后立即从这些线程调用installIfNeeded(),而不是等到线程尝试使用网络时再调用。(多次调用该方法没有坏处,因为它在安全提供程序不需要更新时会立即返回。)
  • 如果用户体验可能会受到线程阻塞的影响,请调用该方法的异步版本installIfNeededAsync(),例如,如果调用来自 UI 线程中的活动。(如果这样做,您需要等待操作完成,然后再尝试任何安全通信。 ProviderInstaller调用您的侦听器的onProviderInstalled()方法来发出成功信号。)

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

更新Provider后,对安全 API(包括 SSL API)的所有调用都将通过它进行路由。(但是,这并不适用于android.net.SSLCertificateSocketFactory,它仍然容易受到CVE-2014-0224等漏洞的攻击。)

同步修补

修补安全提供程序最简单的方法是调用同步方法installIfNeeded()。如果用户体验不会受到线程在等待操作完成时阻塞的影响,则此方法适用。

例如,以下是一个更新安全提供程序的工作器的实现。由于工作器在后台运行,因此如果线程在等待安全提供程序更新时阻塞,也没有问题。工作器调用installIfNeeded()来更新安全提供程序。如果该方法正常返回,则工作器知道安全提供程序是最新的。如果该方法抛出异常,则工作器可以采取适当的操作(例如,提示用户更新 Google Play 服务)。

Kotlin

/**
 * Sample patch Worker using {@link ProviderInstaller}.
 */
class PatchWorker(appContext: Context, workerParams: WorkerParameters): Worker(appContext, workerParams) {

  override fun doWork(): Result {
        try {
            ProviderInstaller.installIfNeeded(context)
        } catch (e: GooglePlayServicesRepairableException) {

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

            // Prompt the user to install/update/enable Google Play services.
            GoogleApiAvailability.getInstance()
                    .showErrorNotification(context, e.connectionStatusCode)

            // Notify the WorkManager that a soft error occurred.
            return Result.failure()

        } catch (e: GooglePlayServicesNotAvailableException) {
            // Indicates a non-recoverable error; the ProviderInstaller can't
            // install an up-to-date Provider.

            // Notify the WorkManager that a hard error occurred.
            return Result.failure()
        }


        // If this is reached, you know that the provider was already up to date
        // or was successfully updated.
        return Result.success()
    }
}

Java

/**
 * Sample patch Worker using {@link ProviderInstaller}.
 */
public class PatchWorker extends Worker {

  ...

  @Override
  public Result doWork() {
    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.
      GoogleApiAvailability.getInstance()
              .showErrorNotification(context, e.connectionStatusCode)

      // Notify the WorkManager that a soft error occurred.
      return Result.failure();

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

      // Notify the WorkManager that a hard error occurred.
      return Result.failure();
    }

    // If this is reached, you know that the provider was already up to date
    // or was successfully updated.
    return Result.success();
  }
}

异步修补

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

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

Kotlin

private const val ERROR_DIALOG_REQUEST_CODE = 1

/**
 * Sample activity using {@link ProviderInstaller}.
 */
class MainActivity : Activity(), ProviderInstaller.ProviderInstallListener {

    private var retryProviderInstall: Boolean = false

    // Update the security provider when the activity is created.
    override fun onCreate(savedInstanceState: Bundle?) {
        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 fun 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 fun onProviderInstallFailed(errorCode: Int, recoveryIntent: Intent) {
        GoogleApiAvailability.getInstance().apply {
            if (isUserResolvableError(errorCode)) {
                // Recoverable error. Show a dialog prompting the user to
                // install/update/enable Google Play services.
                showErrorDialogFragment(this@MainActivity, errorCode, ERROR_DIALOG_REQUEST_CODE) {
                    // The user chose not to take the recovery action.
                    onProviderInstallerNotAvailable()
                }
            } else {
                onProviderInstallerNotAvailable()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int,
                                  data: Intent) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
            // Adding a fragment via GoogleApiAvailability.showErrorDialogFragment
            // before the instance state is restored throws an error. So instead,
            // set a flag here, which causes the fragment to delay until
            // onPostResume.
            retryProviderInstall = true
        }
    }

    /**
     * On resume, check whether a flag indicates that the provider needs to be
     * reinstalled.
     */
    override fun onPostResume() {
        super.onPostResume()
        if (retryProviderInstall) {
            // It's safe to retry installation.
            ProviderInstaller.installIfNeededAsync(this, this)
        }
        retryProviderInstall = false
    }

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

Java

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

  private static final int ERROR_DIALOG_REQUEST_CODE = 1;

  private boolean retryProviderInstall;

  // 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) {
    GoogleApiAvailability availability = GoogleApiAvailability.getInstance();
    if (availability.isUserRecoverableError(errorCode)) {
      // Recoverable error. Show a dialog prompting the user to
      // install/update/enable Google Play services.
      availability.showErrorDialogFragment(
          this,
          errorCode,
          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 isn't 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 GoogleApiAvailability.showErrorDialogFragment
      // before the instance state is restored throws an error. So instead,
      // set a flag here, which causes the fragment to delay until
      // onPostResume.
      retryProviderInstall = true;
    }
  }

  /**
  * On resume, check whether a flag indicates that the provider needs to be
  * reinstalled.
  */
  @Override
  protected void onPostResume() {
    super.onPostResume();
    if (retryProviderInstall) {
      // It's safe to retry installation.
      ProviderInstaller.installIfNeededAsync(this, this);
    }
    retryProviderInstall = false;
  }

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