SSL の脆弱性攻撃から保護するためにセキュリティ プロバイダをアップデートする

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 サービスをアップデートするための適切なダイアログ ボックスをユーザーに表示します。
  • 修復不可能なエラーが発生した場合、このメソッドは GooglePlayServicesNotAvailableException をスローし、Provider をアップデートできないことを示します。次に、アプリはこの例外をキャッチし、標準の修正フロー図を表示するなどの適切なアクションを実行します。

例外をスローせずに、適切なコールバック メソッドを呼び出して成功または失敗を示すことを除いて、installIfNeededAsync() メソッドは同じように機能します。

installIfNeeded() で新しい Provider をインストールする必要がある場合、インストールには 30~50 ミリ秒(新しいデバイスの場合)から 350 ミリ秒(以前のデバイスの場合)程度かかります。セキュリティ プロバイダがすでに最新の状態である場合、このメソッドは瞬時に実行されます。ユーザー エクスペリエンスに影響を与えないようにするには、次の処理を実行します。

  • スレッドがネットワークの使用を試みるのを待つ代わりに、バックグラウンド ネットワーク スレッドが読み込まれると即座に、これらのスレッドから installIfNeeded() を呼び出します(セキュリティ プロバイダをアップデートする必要がない場合、このメソッドは結果を即座に返すため、このメソッドを複数回呼び出しても悪影響を及ぼすことはありません)。
  • ユーザー エクスペリエンスがスレッド ブロックの影響を受ける場合、たとえば、UI スレッドのアクティビティからの呼び出しの場合、このメソッドの非同期バージョン installIfNeededAsync() を呼び出します(当然ながら、この非同期バージョンを呼び出す場合は、安全な通信を試行する前に、この操作が完了するのを待つ必要があります。ProviderInstaller はリスナーの onProviderInstalled() メソッドを呼び出し、成功したことを示します)。

警告: ProviderInstaller がアップデート済みの Provider をインストールできない場合、デバイスのセキュリティ プロバイダが既知の脆弱性攻撃に対して無防備になる可能性があります。アプリは、すべての HTTP 通信が暗号化されていないかのように動作します。

Provider がアップデートされると、Security API(SSL API を含む)呼び出しはすべて Provider を経由します(ただし、これにより、android.net.SSLCertificateSocketFactory が適用されることはありません。つまり、CVE-2014-0224 に記載された脆弱性攻撃などに対しては無防備のままです)。

同期的にパッチを適用する

セキュリティ プロバイダにパッチを適用する最も簡単な方法は、同期メソッド 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.
        }
    }
    

Java

    /**
     * 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.
        }
    }
    

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 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.
      }
    }