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) 的呼叫都會透過此提供者轉送 (但不適用於 android.net.SSLCertificateSocketFactory
,因為它仍然容易受到 CVE-2014-0224 這類漏洞攻擊)。
同步修補
如要修補安全性提供者,最簡單的方法是呼叫同步方法 installIfNeeded()
。如果在等待作業完成期間,使用者體驗不會受到執行緒封鎖影響,則適合採用這種做法。
舉例來說,以下是用於更新安全性提供者的 worker 實作項目。由於 worker 是在背景執行,因此在等待安全性提供者更新的同時,讓執行緒進行封鎖作業也沒問題。worker 會呼叫 installIfNeeded()
來更新安全性提供者。如果方法正常傳回,worker 就會知道安全性提供者為最新版本。若方法擲回例外狀況,worker 便可採取適當動作,例如提示使用者更新 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. } }