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

Atualizar seu provedor de segurança para se proteger contra explorações de SSL

O Android depende de um Provider de segurança para oferecer comunicações de rede seguras. No entanto, de tempos em tempos, são encontradas vulnerabilidades no provedor de segurança padrão. Para se proteger contra essas vulnerabilidades, o Google Play Services oferece uma maneira de atualizar automaticamente o provedor de segurança de um dispositivo, proporcionando proteção contra explorações conhecidas. Ao chamar os métodos do Google Play Services, seu app pode garantir que ele seja executado em um dispositivo com as atualizações mais recentes para que tenha proteção contra explorações conhecidas.

Por exemplo, foi descoberta uma vulnerabilidade no OpenSSL (CVE-2014-0224, link em inglês) que pode deixar os apps vulneráveis a um ataque de intermediário (também conhecido pelo nome em inglês "man-in-the-middle") que descriptografa tráfego seguro sem o conhecimento de nenhum dos lados. Com o Google Play Services versão 5.0, uma correção é disponibilizada, mas é necessário que os apps exijam a instalação dessa correção. Usando os métodos do Google Play Services, é possível garantir que seu app seja executado em um dispositivo protegido contra esse ataque.

Cuidado: a atualização do Provider de segurança de um dispositivo não atualiza android.net.SSLCertificateSocketFactory. Em vez de usar essa classe, recomendamos que os desenvolvedores de apps utilizem métodos de alto nível para interagir com criptografia. É possível, na maioria dos apps, usar APIs como HttpsURLConnection sem precisar definir um TrustManager personalizado ou criar um SSLCertificateSocketFactory.

Aplicar um patch no provedor de segurança com ProviderInstaller

Para atualizar o provedor de segurança de um dispositivo, use a classe ProviderInstaller. Você pode verificar se o provedor de segurança está atualizado (e, se necessário, atualizá-lo), chamando o método installIfNeeded() (ou installIfNeededAsync()) dessa classe.

Quando você chama installIfNeeded(), o ProviderInstaller faz o seguinte:

  • Se a atualização do Provider do dispositivo for concluída (ou se ele já estiver atualizado), o método será retornado normalmente.
  • Se a biblioteca do Google Play Services do dispositivo estiver desatualizada, o método gerará uma GooglePlayServicesRepairableException. O app poderá, então, capturar essa exceção e mostrar ao usuário uma caixa de diálogo apropriada para atualizar o Google Play Services.
  • Se um erro não recuperável ocorrer, o método gerará GooglePlayServicesNotAvailableException para indicar que não é possível atualizar o Provider. Em seguida, o app poderá capturar a exceção e escolher uma ação apropriada, como exibir o diagrama de fluxo de correção padrão.

O método installIfNeededAsync() se comporta de forma semelhante, mas, em vez de gerar exceções, ele chama o método de callback apropriado para indicar êxito ou falha.

Se installIfNeeded() precisar instalar um novo Provider, esse processo poderá levar de 30 a 50 milésimos de segundo (em dispositivos mais recentes) até 350 ms (em dispositivos mais antigos). Se o provedor de segurança já estiver atualizado, o método levará um tempo insignificante. Para evitar impactos na experiência do usuário:

  • Chame installIfNeeded() a partir das linhas de execução de rede em segundo plano imediatamente quando elas forem carregadas, em vez de esperar que a linha de execução tente usar a rede. Não há problema em chamar o método várias vezes, já que ele é retornado imediatamente se o provedor de segurança não precisar ser atualizado.
  • Se a experiência do usuário for afetada pelo bloqueio da linha de execução, por exemplo, se a chamada for originada de uma atividade na linha de execução de IU, chame a versão assíncrona do método, installIfNeededAsync(). Naturalmente, se você fizer isso, será preciso esperar a conclusão da operação antes de tentar realizar comunicações seguras. O ProviderInstaller chama o método onProviderInstalled() do seu listener para sinalizar que o processo foi bem-sucedido.

Aviso: se o ProviderInstaller não puder instalar um Provider atualizado, o provedor de segurança do seu dispositivo poderá ficar vulnerável a explorações conhecidas. Seu app se comportará como se todas as comunicações HTTP estivessem descriptografadas.

Quando o Provider estiver atualizado, todas as chamadas para APIs de segurança (incluindo APIs SSL) serão roteadas por ele. No entanto, isso não é válido para android.net.SSLCertificateSocketFactory, que permanece vulnerável a tais explorações, como a CVE-2014-0224 (em inglês).

Aplicar patch de forma síncrona

A maneira mais simples de aplicar um patch no provedor de segurança é chamar o método síncrono installIfNeeded(). Essa ação é apropriada se a experiência do usuário não for afetada pelo bloqueio de linhas de execução enquanto a operação é concluída.

Por exemplo, veja aqui uma implementação de um adaptador de sincronização que atualiza o provedor de segurança. Como um adaptador de sincronização é executado em segundo plano, não há problema se a linha de execução for bloqueada enquanto a atualização do provedor de segurança é realizada. O adaptador de sincronização chama installIfNeeded() para atualizar o provedor de segurança. Se o método for retornado normalmente, o adaptador de sincronização saberá que o provedor de segurança está atualizado. Se o método acionar uma exceção, o adaptador de sincronização poderá realizar a ação apropriada, como solicitar que o usuário atualize o Google Play Services.

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.
      }
    }
    

Aplicar patch de forma assíncrona

A atualização do provedor de segurança pode levar até 350 milissegundos (em dispositivos mais antigos). Se você estiver realizando a atualização em uma linha de execução que afeta diretamente a experiência do usuário, como a linha de execução de IU, não é recomendável realizar uma chamada síncrona para atualizar o provedor, já que isso poderá fazer com que o app ou o dispositivo congele até que a operação seja concluída. Em vez disso, use o método assíncrono installIfNeededAsync(). Esse método indica seu sucesso ou falha chamando callbacks.

Por exemplo, apresentamos aqui um código que atualiza o provedor de segurança em uma atividade na linha de execução de IU. A atividade chama installIfNeededAsync() para atualizar o provedor e se designa como listener para receber notificações de êxito ou falha. Se o provedor de segurança estiver atualizado ou se a atualização dele for concluída, o método onProviderInstalled() da atividade será chamado, e a atividade saberá que as comunicações estão seguras. Se não for possível atualizar o provedor, o método onProviderInstallFailed() da atividade será chamado, e a atividade poderá realizar a ação apropriada (como solicitar que o usuário atualize o Google Play Services).

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.
      }
    }