Mettre à jour le fournisseur de solutions de sécurité pour vous protéger contre les failles SSL

Android s'appuie sur un Provider de solutions de sécurité pour sécuriser les communications réseau. Toutefois, de temps en temps, des failles sont détectées chez le fournisseur de solutions de sécurité par défaut. Pour vous prémunir contre ces failles, les services Google Play permettent de mettre à jour automatiquement le fournisseur de solutions de sécurité d'un appareil afin de vous protéger contre les failles connues. En appelant les méthodes des services Google Play, vous pouvez vous assurer que votre application s'exécute sur un appareil disposant des dernières mises à jour afin de vous protéger contre les failles connues.

Par exemple, une faille a été découverte dans OpenSSL (CVE-2014-0224). Cette faille peut exposer les applications à une attaque sur le chemin qui déchiffre le trafic sécurisé sans qu'aucune partie n'en ait conscience. La version 5.0 des services Google Play propose un correctif, mais les applications doivent vérifier que ce correctif est installé. En utilisant les méthodes des services Google Play, vous pouvez vous assurer que votre application s'exécute sur un appareil protégé contre cette attaque.

Attention : La mise à jour du Provider de solutions de sécurité d'un appareil ne met pas à jour android.net.SSLCertificateSocketFactory, qui reste vulnérable. Plutôt que d'utiliser cette classe obsolète, nous encourageons les développeurs d'applications à utiliser des méthodes de haut niveau pour interagir avec la cryptographie, par exemple HttpsURLConnection.

Appliquer un correctif au fournisseur de solutions de sécurité avec ProviderInstaller

Pour mettre à jour le fournisseur de solutions de sécurité d'un appareil, utilisez la classe ProviderInstaller. Vous pouvez vérifier que le fournisseur de solutions de sécurité est à jour (et le mettre à jour, si nécessaire) en appelant la méthode installIfNeeded() de cette classe (ou installIfNeededAsync()). Cette section décrit ces options dans les grandes lignes. Les sections suivantes fournissent des étapes plus détaillées et des exemples.

Lorsque vous appelez installIfNeeded(), ProviderInstaller effectue les opérations suivantes :

  • Si le Provider de l'appareil est correctement mis à jour (ou s'il est déjà à jour), la méthode est renvoyée sans générer d'exception.
  • Si la bibliothèque de services Google Play de l'appareil est obsolète, la méthode génère une exception GooglePlayServicesRepairableException. L'application peut ensuite intercepter cette exception et présenter à l'utilisateur une boîte de dialogue appropriée pour mettre à jour les services Google Play.
  • Si une erreur non récupérable se produit, la méthode génère une exception GooglePlayServicesNotAvailableException pour indiquer qu'elle ne peut pas mettre à jour le Provider. L'application peut ensuite intercepter l'exception et choisir une action appropriée, par exemple afficher le schéma de flux de correction standard.

La méthode installIfNeededAsync() se comporte de la même manière, sauf qu'au lieu de générer des exceptions, elle appelle la méthode de rappel appropriée pour indiquer la réussite ou l'échec.

Si le fournisseur de solutions de sécurité est déjà à jour, installIfNeeded() prend un temps négligeable. Si la méthode doit installer un nouveau Provider, l'opération peut prendre entre 30 et 50 ms (sur les appareils plus récents) et 350 ms (sur les appareils plus anciens). Pour éviter de perturber l'expérience utilisateur :

  • Appelez installIfNeeded() immédiatement à partir de threads réseau en arrière-plan lors de leur chargement, au lieu d'attendre que le thread essaie d'utiliser le réseau. (Appeler la méthode plusieurs fois ne présente aucun danger, car elle est immédiatement renvoyée si le fournisseur de solutions de sécurité n'a pas besoin d'être mis à jour.)
  • Appelez la version asynchrone de la méthode, installIfNeededAsync(), si l'expérience utilisateur peut être affectée par le blocage du thread, par exemple si l'appel provient d'une activité du thread UI. (Dans ce cas, vous devez attendre la fin de l'opération avant de tenter de communiquer de manière sécurisée.) ProviderInstaller appelle la méthode onProviderInstalled() de votre écouteur pour signaler la réussite de l'opération.)

Avertissement : Si ProviderInstaller ne parvient pas à installer un Provider mis à jour, le fournisseur de solutions de sécurité de votre appareil peut être vulnérable à des failles connues. Votre application doit se comporter comme si toutes les communications HTTP n'étaient pas chiffrées.

Une fois le Provider mis à jour, tous les appels aux API de sécurité (y compris les API SSL) sont acheminés par son intermédiaire. (Toutefois, cela ne s'applique pas à android.net.SSLCertificateSocketFactory, qui reste vulnérable aux failles telles que CVE-2014-0224.)

Appliquer un correctif de manière synchrone

Le moyen le plus simple d'appliquer un correctif au fournisseur de solutions de sécurité consiste à appeler la méthode synchrone installIfNeeded(). Cela est approprié si l'expérience utilisateur n'est pas affectée par le blocage du thread en attendant la fin de l'opération.

Voici un exemple d'implémentation d'un nœud de calcul qui met à jour le fournisseur de solutions de sécurité. Puisqu'un nœud de calcul s'exécute en arrière-plan, ce n'est pas grave si le thread se bloque en attendant que le fournisseur de solutions de sécurité soit mis à jour. Le nœud de calcul appelle installIfNeeded() pour mettre à jour le fournisseur de solutions de sécurité. Si la méthode est renvoyée normalement, le nœud de calcul sait que le fournisseur de solutions de sécurité est à jour. Si la méthode génère une exception, le nœud de calcul peut prendre les mesures appropriées (par exemple, inviter l'utilisateur à mettre à jour les services 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();
  }
}

Appliquer un correctif de manière asynchrone

La mise à jour du fournisseur de solutions de sécurité peut prendre jusqu'à 350 ms (sur les appareils plus anciens). Si vous procédez à la mise à jour sur un thread qui affecte directement l'expérience utilisateur, tel que le thread UI, vous ne devez pas effectuer d'appel synchrone pour mettre à jour le fournisseur, car cela pourrait entraîner le blocage de l'application ou de l'appareil jusqu'à la fin de l'opération. Utilisez plutôt la méthode asynchrone installIfNeededAsync(). Cette méthode indique sa réussite ou son échec en appelant des rappels.

Par exemple, voici du code qui met à jour le fournisseur de solutions de sécurité dans une activité dans le thread UI. L'activité appelle installIfNeededAsync() pour mettre à jour le fournisseur et se désigne comme écouteur pour recevoir les notifications de réussite ou d'échec. Si le fournisseur de solutions de sécurité est à jour ou mis à jour correctement, la méthode onProviderInstalled() de l'activité est appelée, et l'activité sait que la communication est sécurisée. Si le fournisseur ne peut pas être mis à jour, la méthode onProviderInstallFailed() de l'activité est appelée, et l'activité peut prendre les mesures appropriées (par exemple, inviter l'utilisateur à mettre à jour les services 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.
  }
}