Android에서는 보안 네트워크 통신을 제공하기 위해 보안 Provider
가 필요합니다. 하지만, 기본 보안 프로바이더에서 취약점이 발견되는 경우가 있습니다. 이러한 취약점으로부터 기기를 보호하기 위해 Google Play 서비스에서는 기기의 보안 프로바이더를 자동으로 업데이트하여 알려진 악용으로부터 기기를 보호하는 방법을 제공합니다. Google Play 서비스 메서드를 호출하면 최신 업데이트가 있는 기기에서 앱이 실행되도록 하여 알려진 악용으로부터 기기를 보호할 수 있습니다.
예를 들어, OpenSSL(CVE-2014-0224)에서는 어느 쪽도 모르게 보안 트래픽을 복호화하는 '중간자(man-in-the-middle)' 공격에 앱을 노출하는 취약점이 발견되었습니다. Google Play 서비스 버전 5.0에서는 이에 대한 수정사항을 제공하지만, 앱에서 이 수정사항이 설치되도록 해야 합니다. Google Play 서비스 메서드를 사용하면 이러한 공격으로부터 안전한 기기에서 앱이 실행되도록 할 수 있습니다.
주의: 기기의 보안 Provider
업데이트는 android.net.SSLCertificateSocketFactory
를 업데이트하지 않습니다. 앱 개발자는 이 클래스를 사용하는 대신 암호화와 상호작용하는 상위 수준 메서드를 사용하는 것이 좋습니다. 대부분의 앱은 맞춤 TrustManager
를 설정하거나 SSLCertificateSocketFactory
를 만들지 않고도 HttpsURLConnection
과 같은 API를 사용할 수 있습니다.
ProviderInstaller를 통해 보안 프로바이더에 패치 사용
기기의 보안 프로바이더를 업데이트하려면 ProviderInstaller
클래스를 사용합니다. 이 클래스의 installIfNeeded()
(또는 installIfNeededAsync()
) 메서드를 호출하여 보안 프로바이더가 최신인지 확인(필요한 경우 업데이트)할 수 있습니다.
installIfNeeded()
를 호출하면 ProviderInstaller
에서 다음을 실행합니다.
- 기기의
Provider
가 성공적으로 업데이트되면(또는 이미 최신이면), 메서드가 정상적으로 반환됩니다. - 기기의 Google Play 서비스 라이브러리가 최신이 아닌 경우, 메서드에서
GooglePlayServicesRepairableException
이 발생합니다. 그러면 앱이 이 예외를 포착하고 사용자에게 Google Play 서비스를 업데이트하라는 대화상자를 표시합니다. - 복구 불가능한 오류가 발생한 경우, 메서드에서
Provider
의 업데이트가 불가능하다고 알리는GooglePlayServicesNotAvailableException
이 발생합니다. 그러면 앱이 이 예외를 포착하고 표준 문제 해결 흐름 다이어그램 표시 같은 일련의 적절한 작업을 선택할 수 있습니다.
installIfNeededAsync()
메서드도 동작은 비슷하지만, 예외를 발생시키는 대신 적절한 콜백 메서드를 호출하여 성공 또는 실패 여부를 나타내는 점이 다릅니다.
installIfNeeded()
에서 새 Provider
를 설치해야 하는 경우, 이 작업은 30-50밀리초(최신 기기의 경우)에서 350밀리초(구형 기기의 경우) 가량이 걸릴 수 있습니다. 보안 프로바이더가 이미 최신 상태인 경우 메서드가 작업하는 데 걸리는 시간은 미미합니다. 사용자 환경에 영향을 미치지 않도록 하려면 다음과 같이 합니다.
- 스레드가 네트워크 사용을 시도하기를 기다리는 대신, 스레드가 로드될 때 즉시 백그라운드 네트워킹 스레드에서
installIfNeeded()
를 호출합니다. (보안 프로바이더에 업데이트가 필요하지 않으면 이 메서드는 즉시 반환되므로 메서드를 여러 번 호출하더라도 피해가 없습니다.) - 스레드 차단에 의해 사용자 환경이 영향을 받는 경우(예: UI 스레드에 있는 한 활동에서 호출하는 경우), 해당 메서드의 비동기 버전인
installIfNeededAsync()
를 호출합니다. (물론 이렇게 하는 경우, 보안 통신을 시도하기 전에 작업이 완료되기를 기다려야 합니다.ProviderInstaller
는 성공 신호를 보내기 위해 리스너의onProviderInstalled()
메서드를 호출합니다.)
경고: ProviderInstaller
에서 업데이트된 Provider
를 설치할 수 없다면 기기의 보안 프로바이더는 알려진 악용에 취약할 수 있습니다. 앱은 모든 HTTP 통신이 암호화되지 않은 것처럼 작동해야 합니다.
Provider
가 업데이트되면, 보안 API(SSL API 포함)의 모든 호출이 이를 통해 라우팅됩니다.
(그러나, 이것은 CVE-2014-0224와 같은 악용에 취약한 android.net.SSLCertificateSocketFactory
에는 적용되지 않습니다.)
동기 패치 사용
보안 프로바이더에 패치를 사용하는 가장 간단한 방법은 동기 메서드 installIfNeeded()
를 호출하는 것입니다.
이 방법은 작업이 완료되기를 기다리는 동안 사용자 환경이 스레드 차단으로 영향을 받지 않는 경우에 적합합니다.
예를 들어, 다음은 보안 프로바이더를 업데이트하는 동기화 어댑터의 구현입니다. 동기화 어댑터는 백그라운드로 실행되므로 보안 프로바이더가 업데이트되기를 기다리는 동안 스레드가 차단되어도 문제가 없습니다. 동기화 어댑터는 보안 프로바이더를 업데이트하기 위해 installIfNeeded()
를 호출합니다. 메서드가 정상적으로 반환되면 보안 프로바이더가 최신 상태임을 동기화 어댑터가 알게 됩니다. 메서드가 예외를 발생시키면 동기화 어댑터가 적절한 작업을 수행할 수 있습니다(예: 사용자에게 Google Play 서비스를 업데이트하라는 메시지 표시).
Kotlin
/** * Sample sync adapter using {@link ProviderInstaller}. */ class SyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { override fun onPerformSync( account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult ) { 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 SyncManager that a soft error occurred. syncResult.stats.numIoExceptions++ return } catch (e: GooglePlayServicesNotAvailableException) { // 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. } }
자바
/** * 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. GoogleApiAvailability.getInstance() .showErrorNotification(context, e.connectionStatusCode) // 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 스레드의 활동에서 보안 프로바이더를 업데이트하는 코드입니다. 이 활동은 보안 프로바이더를 업데이트하기 위해 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 will cause the fragment to delay until // onPostResume. retryProviderInstall = true } } /** * On resume, check to see if we flagged that we need to reinstall the * provider. */ override fun onPostResume() { super.onPostResume() if (retryProviderInstall) { // We can now safely retry installation. ProviderInstaller.installIfNeededAsync(this, this) } retryProviderInstall = false } private fun 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. } }
자바
/** * 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 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 GoogleApiAvailability.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. retryProviderInstall = true; } } /** * On resume, check to see if we flagged that we need to reinstall the * provider. */ @Override protected void onPostResume() { super.onPostResume(); if (retryProviderInstall) { // We can now safely retry installation. ProviderInstaller.installIfNeededAsync(this, this); } retryProviderInstall = 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. } }