Cómo actualizar tu proveedor de seguridad para protegerte contra exploits de SSL

Android se basa en un objeto Provider de seguridad para proporcionar comunicaciones de red seguras. No obstante, ocasionalmente, se detectan vulnerabilidades en el proveedor de seguridad predeterminado. Con el objetivo de 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 para brindar protección contra exploits conocidos. Si llamas a los métodos de los Servicios de Google Play, podrás asegurarte de que tu app se ejecute en un dispositivo que tenga las actualizaciones más recientes para brindar protección contra exploits conocidos.

Por ejemplo, se descubrió una vulnerabilidad en OpenSSL (CVE-2014-0224) que puede dejar las apps expuestas a un ataque en la ruta que desencripta el tráfico seguro sin que ninguna de las partes correspondientes lo sepa. La versión 5.0 de los Servicios de Google Play ofrece una solución, pero las apps deben verificar que esté instalada. Si usas los métodos de los Servicios de Google Play, podrás asegurarte de que tu app se ejecute en un dispositivo protegido contra ese ataque.

Precaución: La actualización del objeto Provider de seguridad del dispositivo no actualiza android.net.SSLCertificateSocketFactory, por lo que permanece vulnerable. En lugar de usar esta clase obsoleta, recomendamos a los desarrolladores de apps que utilicen métodos de alto nivel para interactuar con la criptografía, como HttpsURLConnection.

Cómo aplicarle un parche al proveedor de seguridad con ProviderInstaller

Para actualizar el proveedor de seguridad de un dispositivo, usa la clase ProviderInstaller. Puedes verificar si el proveedor de seguridad está actualizado (y, de ser necesario, actualizarlo) llamando al método installIfNeeded() (o installIfNeededAsync()) de esa clase. En esta sección, se describen estas opciones en términos generales. En las siguientes secciones, se proporcionan pasos y ejemplos más detallados.

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

  • Si el Provider del dispositivo se actualiza correctamente (o ya está actualizado), el método devuelve un resultado sin arrojar una excepción.
  • Si la biblioteca de los Servicios de Google Play del dispositivo está desactualizada, el método arrojará 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 arroja una GooglePlayServicesNotAvailableException para indicar que no puede actualizar el 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 arrojar excepciones, llama al método de devolución de llamada correspondiente para indicar el éxito o el fracaso de la operación.

Si el proveedor de seguridad ya está actualizado, installIfNeeded() tarda una cantidad de tiempo insignificante. Si el método necesita instalar un Provider nuevo, puede tardar entre 30 y 50 ms (en dispositivos más recientes) y hasta 350 ms (en dispositivos más antiguos). 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 devuelve una respuesta al instante si el proveedor de seguridad no necesita actualización).
  • Llama a la versión asíncrona del método, installIfNeededAsync(), si la experiencia del usuario puede verse afectada por el bloqueo del subproceso (por ejemplo, si la llamada proviene de una actividad en el subproceso de IU). Por supuesto, si haces esto, deberás esperar a que finalice la operación para intentar establecer comunicaciones seguras. El ProviderInstaller llama al método onProviderInstalled() de tu objeto de escucha para indicar que la operación fue exitosa.

Advertencia: Si el ProviderInstaller no puede instalar un Provider actualizado, el proveedor de seguridad del dispositivo puede ser vulnerable a exploits 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 APIs de seguridad (incluidas las APIs de SSL) se transmitirán a través de él. Sin embargo, esto no se aplica a android.net.SSLCertificateSocketFactory, que sigue siendo vulnerable a exploits como CVE-2014-0224.

Cómo aplicar parches de forma sincrónica

La forma más simple de aplicar un parche al proveedor de seguridad es llamar al método síncrono 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, esta es la implementación de un trabajador que actualiza el proveedor de seguridad. Dado que un trabajador 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 trabajador llama a installIfNeeded() para actualizar el proveedor de seguridad. Si el método devuelve resultados normalmente, el trabajador sabrá que el proveedor de seguridad está actualizado. Si el método arroja una excepción, el trabajador podrá tomar una medida necesaria (como solicitar al usuario que actualice los Servicios de 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();
  }
}

Cómo aplicar parches de forma asíncrona

La actualización del proveedor de seguridad puede tardar hasta 350 ms 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 síncrona para actualizar el proveedor, ya que podría hacer que la app o el dispositivo se bloqueen hasta que finalice la operación. En su lugar, usa el método asíncrono 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 subproceso 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 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.
  }
}