Mengupdate penyedia keamanan agar terlindung dari eksploitasi 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 eksploitasi umum. Dengan memanggil metode layanan Google Play, Anda dapat membantu memastikan aplikasi Anda berjalan di perangkat yang memiliki update terbaru untuk melindungi dari eksploitasi umum.

Misalnya, ditemukan kerentanan di OpenSSL (CVE-2014-0224) yang dapat membuat aplikasi rentan terhadap serangan di jalur yang mendekripsi traffic aman tanpa diketahui kedua belah pihak. Layanan Google Play versi 5.0 menawarkan perbaikan, tetapi aplikasi harus memeriksa apakah perbaikan ini sudah diinstal atau belum. Dengan metode layanan Google Play, Anda dapat membantu memastikan aplikasi Anda berjalan di perangkat yang aman dari serangan tersebut.

Perhatian: Mengupdate Provider keamanan perangkat tidak akan mengupdate android.net.SSLCertificateSocketFactory sehingga tetap rentan. Daripada menggunakan class ini, yang tidak digunakan lagi, sebaiknya developer aplikasi menggunakan metode tingkat tinggi untuk berinteraksi dengan kriptografi, seperti HttpsURLConnection.

Menambahkan patch di penyedia keamanan menggunakan ProviderInstaller

Untuk mengupdate penyedia keamanan perangkat, gunakan class ProviderInstaller. Anda dapat memastikan bahwa penyedia keamanan adalah versi terbaru (dan mengupdatenya, jika perlu) dengan memanggil metode installIfNeeded() (atau installIfNeededAsync()) class tersebut. Bagian ini menjelaskan opsi tersebut di level tinggi. Bagian berikutnya memberikan langkah-langkah dan contoh yang lebih detail.

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

  • Jika Provider perangkat berhasil diupdate (atau sudah yang terbaru), metode akan ditampilkan tanpa menampilkan pengecualian.
  • Jika library layanan Google Play perangkat telah habis masa berlakunya, metode ini akan menampilkan GooglePlayServicesRepairableException. Selanjutnya, aplikasi dapat memahami 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 memahami 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 metode callback yang tepat untuk menunjukkan keberhasilan atau kegagalan.

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

  • Panggil installIfNeeded() dari thread jaringan latar belakang segera setelah thread dimuat, bukan menunggu sampai thread mencoba menggunakan jaringan. (Tidak ada salahnya memanggil metode beberapa kali, karena metode ini akan langsung ditampilkan jika penyedia keamanan tidak perlu diupdate.)
  • Panggil versi asinkron metode tersebut, installIfNeededAsync(), jika pengalaman pengguna dapat terpengaruh oleh pemblokiran thread—misalnya, jika panggilan berasal dari aktivitas di UI thread. (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 diupdate, penyedia keamanan perangkat Anda mungkin akan rentan terhadap eksploitasi umum. 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, hal ini tidak berlaku untuk android.net.SSLCertificateSocketFactory, yang tetap rentan terhadap eksploitasi seperti CVE-2014-0224.)

Menambahkan patch secara sinkron

Cara paling mudah untuk menambahkan patch di 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 penerapan worker yang mengupdate penyedia keamanan. Karena worker berjalan di latar belakang, tidak masalah jika thread diblokir selagi menunggu penyedia keamanan diupdate. Worker memanggil installIfNeeded() untuk mengupdate penyedia keamanan. Jika metode ini ditampilkan secara normal, worker akan mengetahui bahwa penyedia keamanan adalah versi terbaru. Jika metode ini menampilkan pengecualian, worker dapat mengambil tindakan yang sesuai (seperti memperingatkan pengguna untuk mengupdate layanan 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();
  }
}

Menambahkan patch secara asinkron

Proses update penyedia keamanan dapat memerlukan waktu hingga 350 milidetik (di perangkat lama). Jika update dilakukan pada thread yang langsung memengaruhi pengalaman pengguna, seperti UI thread, sebaiknya Anda tidak membuat panggilan sinkron untuk mengupdate penyedia, karena hal tersebut dapat mengakibatkan aplikasi atau perangkat berhenti berfungsi 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 UI thread. Aktivitas ini memanggil installIfNeededAsync() untuk mengupdate penyedia, dan menetapkan dirinya sebagai pemroses untuk menerima notifikasi keberhasilan atau kegagalan. Jika penyedia keamanan adalah versi terbaru 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 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.
  }
}