セキュリティ プロバイダを更新して SSL エクスプロイトから保護する

Android は、セキュリティ Provider を使用して、セキュアなネットワーク通信を実現しています。ただし、時間の経過とともに、デフォルト セキュリティ プロバイダに脆弱性が見つかることがあります。このような脆弱性から保護するために、Google Play 開発者サービスは、デバイスのセキュリティ プロバイダを自動的にアップデートして既知のエクスプロイトから保護する機能を備えています。アプリから Google Play 開発者サービスのメソッドを呼び出すことにより、アプリが稼働するデバイスを常に最新の状態にアップデートし、既知のエクスプロイトから保護することができます。

たとえば、OpenSSL で見つかった脆弱性(CVE-2014-0224)により、アプリは中間者攻撃にさらされるリスクがあります。この攻撃では、サーバー側とクライアント側の両方に知られることなく、セキュア トラフィックが復号されます。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 を含む)の呼び出しはすべて、このプロバイダを経由するようになります(ただし、これは、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.
      }
    }