Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Actualiza tu proveedor de seguridad para protegerte contra vulnerabilidades de SSL

Android se basa en un objeto Provider de seguridad para proporcionar comunicaciones de red seguras. No obstante, de vez en cuando, se detectan vulnerabilidades en el proveedor de seguridad predeterminado. Para brindar protección contra estas vulnerabilidades, los Servicios de Google Play proporcionan un método para actualizar automáticamente el proveedor de seguridad de un dispositivo, a fin de brindar protección contra vulnerabilidades conocidas. Si llamas a los métodos de los Servicios de Google Play, podrás garantizar que tu app se ejecute en un dispositivo que cuente con las actualizaciones de protección más recientes contra vulnerabilidades conocidas.

Por ejemplo, se detectó una vulnerabilidad en OpenSSL (CVE-2014-0224) que puede dejar las apps expuestas a ataques de intermediarios que desencriptan tráfico seguro sin que ninguna de las partes correspondientes lo sepa. En la versión 5.0 de los Servicios de Google Play, se ofrece una solución, pero las apps deben controlar que realmente se instale este parche. Al usar los métodos de los Servicios de Google Play, podrás garantizar que tu app se ejecute en un dispositivo protegido contra ese ataque.

Precaución: Si actualizas el objeto Provider de seguridad de un dispositivo, no se actualizará el parámetro android.net.SSLCertificateSocketFactory. En lugar de usar esta clase, recomendamos a los desarrolladores de apps que utilicen métodos de alto nivel para interactuar con la criptografía. La mayoría de las apps pueden usar API como la de HttpsURLConnection sin necesidad de establecer un objeto TrustManager personalizado ni de crear un elemento SSLCertificateSocketFactory.

Aplícale un parche al proveedor de seguridad con un objeto ProviderInstaller

Para actualizar el proveedor de seguridad de un dispositivo, usa la clase ProviderInstaller. Puedes verificar que el proveedor de seguridad esté actualizado (o actualizarlo, si fuera necesario) llamando al método installIfNeeded() (o installIfNeededAsync()) de esa clase.

Cuando llamas a installIfNeeded(), ProviderInstaller realiza lo siguiente:

  • Si el objeto Provider del dispositivo se actualiza correctamente (o si ya está actualizado), el método mostrará una respuesta como lo hace habitualmente.
  • Si está desactualizada la biblioteca de los Servicios de Google Play del dispositivo, el método generará una excepción del tipo GooglePlayServicesRepairableException. Luego, la app podrá usarla para mostrarle al usuario el cuadro de diálogo correspondiente para actualizar los Servicios de Google Play.
  • Si se produce un error irrecuperable, el método generará una excepción GooglePlayServicesNotAvailableException para indicar que no puede actualizar el objeto Provider. Luego, la app podrá obtener la excepción y elegir un procedimiento adecuado, como mostrar el diagrama de flujo de fix-it estándar.

El método installIfNeededAsync() se comporta de forma similar, pero en lugar de generar excepciones, llama al método de devolución de llamada correspondiente para indicar el éxito o fracaso de la operación.

Si installIfNeeded() necesita instalar un nuevo objeto Provider, este proceso puede demorar entre 30 y 50 milisegundos en dispositivos más recientes, y hasta 350 en dispositivos más antiguos. Si ya está actualizado el proveedor de seguridad, el tiempo que requerirá el método será insignificante. Para no afectar negativamente la experiencia del usuario, haz lo siguiente:

  • Llama de inmediato a installIfNeeded() desde subprocesos de red en segundo plano cuando estos se carguen, en lugar de esperar a que el subproceso intente usar la red. (Se puede llamar al método varias veces, ya que muestra una respuesta al instante si el proveedor de seguridad no necesita actualización).
  • Si se prevé que la experiencia del usuario se verá afectada por el bloqueo del subproceso (por ejemplo, si la llamada proviene de una actividad en el procesamiento de la IU), llama a la versión asincrónica del método: installIfNeededAsync(). (Por supuesto, si haces esto, deberás esperar a que finalice la operación para intentar establecer comunicaciones seguras. El objeto ProviderInstaller llamará al método onProviderInstalled() de tu objeto de escucha para indicar que la operación fue exitosa).

Advertencia: Si el elemento ProviderInstaller no puede instalar un objeto Provider actualizado, el proveedor de seguridad del dispositivo podría ser vulnerable a ataques conocidos. Tu app debe comportarse como si todas las comunicaciones HTTP no estuvieran encriptadas.

Una vez que se actualice el objeto Provider, todas las llamadas a las API de seguridad (incluidas las API de SSL) se direccionarán a través de él. (No obstante, esto no se aplica a android.net.SSLCertificateSocketFactory, que continúa siendo vulnerable a ataques como CVE-2014-0224).

Aplica parches de forma sincrónica

La forma más simple de aplicarle un parche al proveedor de seguridad es llamar al método sincrónico installIfNeeded(). Es correcto hacer esto cuando la experiencia del usuario no se vea afectada por el bloqueo del subproceso mientras se espera a que finalice la operación.

Por ejemplo, a continuación, te mostramos la implementación de un adaptador de sincronización que actualiza el proveedor de seguridad. Dado que un adaptador de sincronización se ejecuta en segundo plano, no hay problema si el subproceso se bloquea mientras se espera a que se actualice el proveedor de seguridad. El adaptador de sincronización llama a installIfNeeded() para actualizar el proveedor de seguridad. Si el método muestra una respuesta como lo hace habitualmente, el adaptador de sincronización reconocerá que el proveedor está actualizado. Si el método genera una excepción, el adaptador podrá tomar una medida necesaria (como solicitar al usuario que actualice los Servicios de 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.
      }
    }
    

Aplica parches de forma asincrónica

La actualización del proveedor de seguridad puede tardar hasta 350 milisegundos en dispositivos más antiguos. Si realizas la actualización en un subproceso que afecta directamente la experiencia del usuario, como el de la IU, no te convendrá realizar una llamada sincrónica para actualizar el proveedor, ya que podría hacer que la app o el dispositivo se bloqueen hasta que finalice la operación. Como alternativa, debes usar el método asincrónico installIfNeededAsync(), que indica el éxito o fracaso realizando devoluciones de llamada.

Por ejemplo, a continuación, te mostramos parte de un código con el que se actualiza el proveedor de seguridad en una actividad del procesamiento de IU. La actividad llama a installIfNeededAsync() para actualizar el proveedor y se designa a sí misma como el objeto de escucha para recibir notificaciones de éxito o fracaso. Si el proveedor de seguridad está actualizado o se actualizó correctamente, se llama al método onProviderInstalled() de la actividad y esta reconoce que la comunicación es segura. Si no se puede actualizar el proveedor, se llama al método onProviderInstallFailed() de la actividad y esta puede tomar una medida adecuada (como indicar al usuario que actualice los Servicios de 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.
      }
    }