セキュリティ プロバイダを更新して 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 がアップデートされると、Security 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.
  }
}