Cập nhật trình cung cấp dịch vụ bảo mật để chống khai thác SSL

Android dựa vào một Provider bảo mật để cung cấp hoạt động giao tiếp an toàn qua mạng. Tuy nhiên, thỉnh thoảng, các lỗ hổng vẫn được phát hiện thấy trong trình cung cấp dịch vụ bảo mật mặc định. Để bảo vệ khỏi các lỗ hổng này, Dịch vụ Google Play cung cấp một phương thức tự động cập nhật trình cung cấp dịch vụ bảo mật của thiết bị để chống lại các trường hợp khai thác đã biết. Bằng cách gọi các phương thức của Dịch vụ Google Play, bạn có thể giúp đảm bảo rằng ứng dụng đang chạy trên một thiết bị có bản cập nhật mới nhất để chống lại các trường hợp khai thác đã biết.

Ví dụ: một lỗ hổng bảo mật đã được phát hiện trong OpenSSL (CVE-2014-0224) có thể khiến ứng dụng dễ bị tấn công trên đường dẫn (giải mã lưu lượng truy cập bảo mật mà không có bên nào biết). Dịch vụ Google Play phiên bản 5.0 cung cấp một bản sửa lỗi nhưng các ứng dụng phải kiểm tra để đảm bảo rằng bạn đã cài đặt bản sửa lỗi này. Bằng cách sử dụng các phương thức của Dịch vụ Google Play, bạn có thể giúp đảm bảo ứng dụng của mình đang chạy trên một thiết bị được bảo mật chống lại cuộc tấn công đó.

Thận trọng: Việc cập nhật Provider bảo mật của thiết bị sẽ không cập nhật android.net.SSLCertificateSocketFactory, do đó ứng dụng vẫn dễ bị tấn công. Thay vì sử dụng lớp không dùng nữa này, nhà phát triển ứng dụng nên dùng các phương thức cấp cao để tương tác với quy trình mã hoá, chẳng hạn như HttpsURLConnection.

Vá trình cung cấp dịch vụ bảo mật bằng ProviderInstaller

Để cập nhật trình cung cấp dịch vụ bảo mật của thiết bị, hãy sử dụng lớp ProviderInstaller. Bạn có thể xác minh rằng trình cung cấp dịch vụ bảo mật đã được cập nhật (và cập nhật trình cung cấp này, nếu cần) bằng cách gọi phương thức installIfNeeded() (hoặc installIfNeededAsync()) của lớp đó. Phần này mô tả các lựa chọn dưới đây ở cấp độ tổng quát. Các phần tiếp theo sẽ cung cấp thêm các bước và ví dụ chi tiết.

Khi bạn gọi installIfNeeded(), ProviderInstaller sẽ thực hiện những thao tác sau:

  • Nếu Provider của thiết bị được cập nhật thành công (hoặc đã được cập nhật), thì phương thức này sẽ trả về mà không gửi một trường hợp ngoại lệ.
  • Nếu thư viện Dịch vụ Google Play của thiết bị đã cũ, thì phương thức này sẽ gửi GooglePlayServicesRepairableException. Sau đó, ứng dụng có thể phát hiện trường hợp ngoại lệ này và cho người dùng thấy một hộp thoại phù hợp để cập nhật Dịch vụ Google Play.
  • Nếu xảy ra lỗi không thể khôi phục, phương thức này sẽ gửi GooglePlayServicesNotAvailableException để cho biết rằng không thể cập nhật Provider. Sau đó, ứng dụng có thể phát hiện trường hợp ngoại lệ và chọn một chuỗi hành động thích hợp, chẳng hạn như hiển thị biểu đồ quy trình sửa lỗi tiêu chuẩn.

Phương thức installIfNeededAsync() hoạt động tương tự như vậy, ngoại trừ việc phương thức này sẽ gọi phương thức gọi lại thích hợp để cho biết có thành công hay không, thay vì gửi các trường hợp ngoại lệ.

Nếu trình cung cấp dịch vụ bảo mật đã được cập nhật, thì installIfNeeded() sẽ mất một khoảng thời gian không đáng kể. Nếu phương thức này cần cài đặt một Provider mới, thì có thể mất từ 30 đến 50 mili giây (trên các thiết bị mới) cho tới 350 mili giây (trên các thiết bị cũ). Cách tránh ảnh hưởng đến trải nghiệm người dùng:

  • Gọi installIfNeeded() từ các luồng kết nối mạng ở chế độ nền ngay khi các luồng này được tải, thay vì đợi luồng để cố gắng sử dụng mạng. (Việc gọi phương thức này nhiều lần không gây ra vấn đề gì vì phương thức này sẽ trả về giá trị ngay lập tức nếu trình cung cấp dịch vụ bảo mật không cần cập nhật.)
  • Gọi phiên bản không đồng bộ installIfNeededAsync() của phương thức này, nếu việc chặn luồng có thể làm ảnh hưởng đến trải nghiệm người dùng, ví dụ: nếu lệnh gọi đến từ một hoạt động trong luồng giao diện người dùng. (Nếu làm việc này, bạn cần phải đợi thao tác này kết thúc trước khi thử bất kỳ phương thức giao tiếp bảo mật nào. ProviderInstaller gọi phương thức onProviderInstalled() của trình nghe để cho biết thành công.)

Cảnh báo: Nếu ProviderInstaller không thể cài đặt một Provider đã cập nhật, thì trình cung cấp dịch vụ bảo mật của thiết bị có thể không được bảo vệ trước các trường hợp khai thác đã biết. Ứng dụng của bạn phải hoạt động như thể mọi hoạt động giao tiếp qua HTTP đều không được mã hoá.

Sau khi Provider được cập nhật, mọi lệnh gọi đến các API bảo mật (bao gồm cả API SSL) sẽ được định tuyến thông qua trình cung cấp này. (Tuy nhiên, điều này không áp dụng cho android.net.SSLCertificateSocketFactory, vì miền vẫn không được bảo vệ trước các trường hợp khai thác như CVE-2014-0224.)

Vá đồng bộ

Cách đơn giản nhất để vá trình cung cấp dịch vụ bảo mật là gọi phương thức đồng bộ installIfNeeded(). Đây là cách phù hợp nếu trải nghiệm người dùng không chịu ảnh hưởng của việc chặn luồng trong khi đợi thao tác hoàn tất.

Ví dụ: dưới đây là cách triển khai một worker cập nhật trình cung cấp dịch vụ bảo mật. Vì một worker sẽ chạy trong chế độ nền nên bạn có thể chấp nhận luồng này bị chặn trong khi chờ trình cung cấp dịch vụ bảo mật được cập nhật. Worker này gọi installIfNeeded() để cập nhật trình cung cấp dịch vụ bảo mật. Nếu phương thức này trả về giá trị như bình thường, thì worker biết là trình cung cấp dịch vụ bảo mật đã được cập nhật. Nếu phương thức này gửi một trường hợp ngoại lệ, thì worker có thể tiến hành một biện pháp thích hợp (chẳng hạn như nhắc người dùng cập nhật Dịch vụ 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();
  }
}

Vá không đồng bộ

Quá trình cập nhật trình cung cấp dịch vụ bảo mật có thể mất tới 350 mili giây (trên các thiết bị cũ). Nếu đang cập nhật trên một luồng ảnh hưởng trực tiếp đến trải nghiệm người dùng, chẳng hạn như luồng giao diện người dùng, thì bạn không nên thực hiện một lệnh gọi đồng bộ để cập nhật trình cung cấp, vì việc này có thể làm treo ứng dụng hoặc thiết bị cho đến khi thao tác kết thúc. Thay vào đó, hãy sử dụng phương thức không đồng bộ installIfNeededAsync(). Phương thức không đồng bộ cho biết quá trình cập nhật có thành công hay không bằng cách gọi lệnh gọi lại.

Ví dụ: dưới đây là mã cập nhật trình cung cấp dịch vụ bảo mật trong một hoạt động trong luồng giao diện người dùng. Hoạt động này sẽ gọi installIfNeededAsync() để cập nhật trình cung cấp và tự chỉ định mình làm trình nghe để nhận thông báo cho biết thành công hay không. Nếu trình cung cấp dịch vụ bảo mật là mới nhất hoặc đã được cập nhật thành công, thì phương thức onProviderInstalled() của hoạt động này sẽ được gọi và hoạt động này sẽ biết rằng hoạt động giao tiếp là an toàn. Nếu không thể cập nhật trình cung cấp, phương thức onProviderInstallFailed() của hoạt động này sẽ được gọi và hoạt động này có thể tiến hành một biện pháp thích hợp (chẳng hạn như nhắc người dùng cập nhật Dịch vụ 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.
  }
}