更新安全性提供者以防範安全資料傳輸層 (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(),不必等待執行緒嘗試使用網路。(如果安全性提供者不需更新,這個方法會立即傳回,因此不會造成多次呼叫造成的傷害)。
  • 如果使用者體驗受到執行緒封鎖的影響 (例如呼叫來自 UI 執行緒中的活動),請呼叫 installIfNeededAsync() 方法的非同步版本。(如果執行這項操作,您就必須等待作業完成之後,才能嘗試任何安全的通訊;ProviderInstaller 會呼叫事件監聽器的 onProviderInstalled() 方法以傳送成功訊息)。

警告:如果 ProviderInstaller 無法安裝更新的 Provider,則裝置的安全性提供者可能會容易受到已知的攻擊。您的應用程式會視為所有 HTTP 通訊都未加密。

更新 Provider 後,所有對安全性 API (包括 SSL API) 的呼叫都會透過該 API 轉送。(不過,這不適用於 android.net.SSLCertificateSocketFactory 安全漏洞,這類漏洞很容易遭受攻擊,例如 CVE-2014-0224)。

同步修補

如要修補安全性提供者,最簡單的方法是呼叫同步方法 installIfNeeded()。在等待作業完成期間,如果使用者體驗不會受到執行緒封鎖影響,則適合使用這種做法。

例如,以下是會更新安全性提供者的 worker 實作。由於工作站在背景執行,因此即使在等待安全性提供者更新時,執行緒會遭到封鎖,也是正常情況。工作站會呼叫 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.
  }
}