Mengupdate penyedia keamanan agar terlindung dari exploit SSL

Android mengandalkan Provider keamanan untuk menyediakan komunikasi jaringan yang aman. Namun, dari waktu ke waktu, kerentanan ditemukan di penyedia keamanan default. Untuk melindungi dari kerentanan ini, layanan Google Play menyediakan cara untuk otomatis mengupdate penyedia keamanan perangkat agar terlindung dari exploit yang dikenal. Dengan memanggil metode layanan Google Play, dapat dipastikan aplikasi Anda dijalankan pada perangkat yang memiliki update terbaru sebagai perlindungan dari exploit yang dikenal.

Misalkan kerentanan ditemukan di OpenSSL (CVE-2014-0224) yang dapat membuat aplikasi rentan terhadap serangan "man-in-the-middle" yang mendekripsi traffic aman tanpa diketahui kedua belah pihak. Layanan Google Play versi 5.0 menyediakan perbaikan, tetapi aplikasi harus memastikan bahwa perbaikan ini diinstal. Dengan menggunakan metode layanan Google Play, aplikasi Anda dapat memastikan bahwa ia berjalan pada perangkat yang aman dari serangan tersebut.

Perhatian: Mengupdate Provider keamanan perangkat tidak berarti mengupdate android.net.SSLCertificateSocketFactory. Daripada menggunakan class ini, sebaiknya developer aplikasi menggunakan metode tingkat tinggi untuk berinteraksi dengan kriptografi. Sebagian besar aplikasi dapat menggunakan API seperti HttpsURLConnection tanpa perlu menetapkan TrustManager kustom atau membuat SSLCertificateSocketFactory.

Mem-patch penyedia keamanan dengan ProviderInstaller

Untuk mengupdate penyedia keamanan perangkat, gunakan class ProviderInstaller. Anda dapat memverifikasi bahwa penyedia keamanan sudah menggunakan versi terupdate (dan mengupdatenya, jika perlu) dengan memanggil metode installIfNeeded() (atau installIfNeededAsync()) class tersebut.

Saat Anda memanggil installIfNeeded(), ProviderInstaller melakukan hal berikut:

  • Jika Provider perangkat berhasil diupdate (atau sudah menggunakan versi terupdate), metode ini kembali secara normal.
  • Jika library layanan Google Play perangkat telah habis masa berlakunya, metode ini akan menampilkan GooglePlayServicesRepairableException. Selanjutnya, aplikasi dapat menangkap pengecualian ini dan menunjukkan kotak dialog yang tepat kepada pengguna untuk mengupdate layanan Google Play.
  • Jika terjadi error yang tidak dapat dipulihkan, metode ini akan menampilkan GooglePlayServicesNotAvailableException yang menunjukkan update Provider tidak dapat dilakukan. Selanjutnya, aplikasi dapat menangkap pengecualian dan memilih tindakan yang tepat, seperti menampilkan diagram alur perbaikan standar.

Metode installIfNeededAsync() menunjukkan perilaku yang mirip, tetapi bukannya menampilkan pengecualian, metode ini akan memanggil callback yang tepat untuk menunjukkan keberhasilan atau kegagalan.

Jika installIfNeeded() perlu menginstal Provider baru, proses ini dapat memerlukan waktu dari 30-50 milidetik (pada perangkat yang lebih baru) hingga 350 milidetik (pada perangkat yang lebih lama). Jika penyedia keamanan sudah menggunakan versi terupdate, metode ini akan membutuhkan waktu yang sangat singkat. Agar tidak memengaruhi pengalaman pengguna:

  • Panggil installIfNeeded() dari thread jaringan latar belakang segera setelah thread dimuat, bukan menunggu sampai thread mencoba menggunakan jaringan. (Tidak apa-apa melakukan panggilan berkali-kali, karena metode ini akan langsung kembali jika penyedia keamanan tidak perlu diupdate.)
  • Jika pengalaman pengguna akan terpengaruh oleh pemblokiran thread--misalnya, jika panggilan berasal dari aktivitas dalam thread UI--panggil versi asinkron metode tersebut, installIfNeededAsync(). (Tentu saja, jika menggunakan opsi ini, Anda harus menunggu hingga operasi selesai sebelum mencoba komunikasi aman apa pun. ProviderInstaller memanggil metode onProviderInstalled() pemroses Anda untuk menandakan keberhasilan.)

Peringatan: Jika ProviderInstaller tidak dapat menginstal Provider yang telah diperbarui, penyedia keamanan perangkat Anda mungkin akan rentan terhadap exploit yang dikenal. Aplikasi Anda akan berperilaku seolah-olah semua komunikasi HTTP tidak dienkripsi.

Setelah Provider diupdate, semua panggilan ke API keamanan (termasuk API SSL) akan dirutekan melalui penyedia tersebut. (Namun, ini tidak berlaku untuk android.net.SSLCertificateSocketFactory, yang tetap rentan terhadap exploit seperti CVE-2014-0224.)

Melakukan patch secara sinkron

Cara paling mudah untuk mem-patch penyedia keamanan adalah dengan memanggil metode sinkron installIfNeeded(). Hal ini sesuai jika pengalaman pengguna tidak akan terpengaruh oleh pemblokiran thread selagi menunggu operasi diselesaikan.

Misalnya, berikut adalah implementasi adaptor sinkronisasi yang mengupdate penyedia keamanan. Karena adaptor sinkronisasi berjalan di latar belakang, tidak apa-apa jika thread diblokir selagi menunggu penyedia keamanan diupdate. Adaptor sinkronisasi akan memanggil installIfNeeded() untuk mengupdate penyedia keamanan. Jika metode ini kembali secara normal, berarti adaptor sinkronisasi mengetahui bahwa versi penyedia keamanan sudah terupdate. Jika metode ini menampilkan pengecualian, adaptor sinkronisasi dapat mengambil tindakan yang sesuai (seperti memperingatkan pengguna untuk mengupdate layanan 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.
      }
    }
    

Melakukan patch secara asinkron

Proses update penyedia keamanan dapat memerlukan waktu hingga 350 milidetik (pada perangkat lama). Jika update dilakukan pada thread yang langsung memengaruhi pengalaman pengguna, seperti thread UI, sebaiknya Anda tidak membuat panggilan sinkron untuk mengupdate penyedia, karena hal tersebut dapat mengakibatkan aplikasi atau perangkat macet sampai operasi selesai. Sebagai gantinya, gunakan metode asinkron installIfNeededAsync(). Metode tersebut menunjukkan keberhasilan atau kegagalan dengan memanggil callback.

Misalnya, berikut adalah kode yang mengupdate penyedia keamanan dalam aktivitas di thread UI. Aktivitas ini memanggil installIfNeededAsync() untuk mengupdate penyedia, dan menetapkan dirinya sebagai pemroses untuk menerima notifikasi keberhasilan atau kegagalan. Jika penyedia keamanan sudah menggunakan versi terupdate atau berhasil diupdate, metode onProviderInstalled() aktivitas akan dipanggil, dan aktivitas akan mengetahui bahwa komunikasi tersebut aman. Jika penyedia tidak dapat diupdate, metode onProviderInstallFailed() aktivitas akan dipanggil, dan aktivitas dapat mengambil tindakan yang sesuai (seperti memperingatkan pengguna untuk mengupdate layanan 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.
      }
    }