提出標準 API 要求

本頁將說明如何提出標準 API 要求來取得完整性判定結果,您只要使用 Android 5.0 (API 級別 21) 以上版本,即可執行這項操作。每當應用程式發出伺服器呼叫,藉此檢查互動情形是否確實時,您都可以提出標準 API 要求,請求系統提供完整性判定結果。

總覽

顯示高階 Play Integrity API 設計的流程圖

提出標準要求的程序可分為兩個部分:

  • 備妥完整性權杖供應工具 (一次性):您需要先呼叫 Integrity API,備妥完整性權杖供應工具,之後才須取得完整性判定結果。舉例來說,您可以在應用程式啟動前或於背景中執行這項操作,之後再要求系統提供完整性判定結果。
  • 要求系統提供完整性權杖 (隨選):每當應用程式發出伺服器要求,讓您檢查是否正規時,您都應該要求系統提供完整性權杖,然後再將其傳送至應用程式的後端伺服器,以便進行解密及驗證。之後,後端伺服器即可決定要採取何種方式因應。

備妥完整性權杖供應工具 (一次性):

  1. 您的應用程式會使用 Google Cloud 專案編號呼叫完整性權杖供應工具。
  2. 應用程式會將完整性權杖供應工具儲存在記憶體中,以便進一步認證檢查呼叫。

要求系統提供完整性權杖 (隨選):

  1. 針對需要保護的使用者操作,應用程式會計算所提要求的雜湊 (使用任一適用的雜湊演算法,例如 SHA256)。
  2. 應用程式會要求系統提供完整性憑證,傳遞要求雜湊。
  3. 應用程式會從 Play Integrity API 接收已簽署及加密的完整性權杖。
  4. 應用程式會將完整性權杖傳遞至應用程式的後端。
  5. 應用程式的後端會將權杖傳送至 Google Play 伺服器,然後 Google Play 伺服器會解密並驗證判定結果,並將結果傳回應用程式的後端。
  6. 應用程式的後端會根據權杖酬載中的信號決定要如何繼續操作。
  7. 應用程式的後端會將決定結果傳送至您的應用程式。

備妥完整性權杖供應工具 (一次性)

提出標準 API 要求,請求 Google Play 提供完整性判定結果之前,您必須先備妥 (或稱執行「暖機」程序) 完整性權杖供應工具。如此一來,Google Play 就能靈敏地在裝置上快取部分認證資訊,進而在您索取完整性判定結果時,縮短關鍵路徑上的延遲時間。再次準備權杖供應工具的做法,有助於重複執行完整性檢查而不大量耗用資源,這樣當您下次提出要求時,就能取得更為即時的完整性判定結果。

建議您在以下情況準備完整性權杖供應工具:

  • 應用程式啟動時 (即冷啟動時)。準備權杖供應工具並非同步操作,因此不會影響啟動時間。如果您打算在應用程式啟動後不久 (例如使用者登入或玩家加入遊戲時) 索取完整性判定結果,這個方法就非常實用。
  • 應用程式開啟時 (即暖啟動時)。但請注意,每個應用程式執行個體每分鐘最多只能準備完整性權杖 5 次。
  • 如果您想在索取完整性判定結果前備妥權杖,則可隨時在背景進行操作。

如要準備完整性權杖供應工具,請按照下列指示操作:

  1. 依照以下範例建立 StandardIntegrityManager
  2. 建構 PrepareIntegrityTokenRequest,透過 setCloudProjectNumber() 方法提供 Google Cloud 專案編號。
  3. 使用管理工具呼叫 prepareIntegrityToken(),提供 PrepareIntegrityTokenRequest

Java

import com.google.android.gms.tasks.Task;

// Create an instance of a manager.
StandardIntegrityManager standardIntegrityManager =
    IntegrityManagerFactory.createStandard(applicationContext);

StandardIntegrityTokenProvider integrityTokenProvider;
long cloudProjectNumber = ...;

// Prepare integrity token. Can be called once in a while to keep internal
// state fresh.
standardIntegrityManager.prepareIntegrityToken(
    PrepareIntegrityTokenRequest.builder()
        .setCloudProjectNumber(cloudProjectNumber)
        .build())
    .addOnSuccessListener(tokenProvider -> {
        integrityTokenProvider = tokenProvider;
    })
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator PrepareIntegrityTokenCoroutine() {
    long cloudProjectNumber = ...;

    // Create an instance of a standard integrity manager.
    var standardIntegrityManager = new StandardIntegrityManager();

    // Request the token provider.
    var integrityTokenProviderOperation =
      standardIntegrityManager.PrepareIntegrityToken(
        new PrepareIntegrityTokenRequest(cloudProjectNumber));

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenProviderOperation;

    // Check the resulting error code.
    if (integrityTokenProviderOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenProviderOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityTokenProvider = integrityTokenProviderOperation.GetResult();
}

原生

/// Initialize StandardIntegrityManager
StandardIntegrityManager_init(/* app's java vm */, /* an android context */);
/// Create a PrepareIntegrityTokenRequest opaque object.
int64_t cloudProjectNumber = ...;
PrepareIntegrityTokenRequest* tokenProviderRequest;
PrepareIntegrityTokenRequest_create(&tokenProviderRequest);
PrepareIntegrityTokenRequest_setCloudProjectNumber(tokenProviderRequest, cloudProjectNumber);

/// Prepare a StandardIntegrityTokenProvider opaque type pointer and call
/// StandardIntegrityManager_prepareIntegrityToken().
StandardIntegrityTokenProvider* tokenProvider;
StandardIntegrityErrorCode error_code =
        StandardIntegrityManager_prepareIntegrityToken(tokenProviderRequest, &tokenProvider);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_provider_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_getStatus(tokenProvider, &token_provider_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_provider_status == INTEGRITY_RESPONSE_COMPLETED)
{
    /// continue to request token from the token provider
}
/// ...
/// Remember to free up resources.
PrepareIntegrityTokenRequest_destroy(tokenProviderRequest);

防範有心人士竄改要求 (建議做法)

使用 Play Integrity API 檢查應用程式中的使用者操作時,您可以利用 requestHash 欄位來因應竄改攻擊。例如,遊戲可能想向遊戲的後端伺服器回報玩家得分,而您的伺服器想要確保 Proxy 伺服器未曾竄改該分數,Play Integrity API 會在已簽署的完整性回應內,傳回您在 requestHash 欄位中設定的值。少了 requestHash,完整性權杖就只會繫結至裝置,而不會繫結至特定要求,導致可能發生攻擊。以下指示說明如何有效使用 requestHash 欄位:

當您要求系統提供完整性判定結果時,請完成以下操作:

  • 從正在發生的使用者操作或伺服器要求中,計算所有相關要求參數的摘要 (例如穩定要求序列化的 SHA256)。requestHash 欄位中設定值的長度上限為 500 個位元組。針對要檢查或保護的操作,請在 requestHash 中加入任何重要或相關的應用程式要求資料。由於完整性權杖文字涵蓋 requestHash 欄位,因此過長的值可能會增加要求大小。
  • 將摘要做為 requestHash 欄位提供給 Play Integrity API,並取得完整性權杖。

當您收到完整性判定結果時,請完成以下操作:

  • 將完整性權杖解碼,然後擷取 requestHash 欄位。
  • 利用與應用程式相同的方式,計算要求摘要 (例如,穩定要求序列化的 SHA256)。
  • 比較應用程式端和伺服器端摘要。如果兩者不相符,則代表要求可信度低。

要求系統提供完整性判定結果 (隨選)

備妥完整性權杖供應工具後,即可開始要求 Google Play 提供完整性判定結果。若要這樣做,請完成下列步驟:

  1. 按照上文取得 StandardIntegrityTokenProvider
  2. 建構 StandardIntegrityTokenRequest,透過 setRequestHash 方法為您要保護的使用者操作提供要求雜湊。
  3. 使用完整性權杖供應工具呼叫 request(),提供 StandardIntegrityTokenRequest

Java

import com.google.android.gms.tasks.Task;

StandardIntegrityTokenProvider integrityTokenProvider;

// See above how to prepare integrityTokenProvider.

// Request integrity token by providing a user action request hash. Can be called
// several times for different user actions.
String requestHash = "2cp24z...";
Task<StandardIntegrityToken> integrityTokenResponse =
    integrityTokenProvider.request(
        StandardIntegrityTokenRequest.builder()
            .setRequestHash(requestHash)
            .build());
integrityTokenResponse
    .addOnSuccessListener(response -> sendToServer(response.token()))
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    StandardIntegrityTokenProvider integrityTokenProvider;

    // See above how to prepare integrityTokenProvider.

    // Request integrity token by providing a user action request hash. Can be called
    // several times for different user actions.
    String requestHash = "2cp24z...";
    var integrityTokenOperation = integrityTokenProvider.Request(
      new StandardIntegrityTokenRequest(requestHash)
    );

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenOperation;

    // Check the resulting error code.
    if (integrityTokenOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityToken = integrityTokenOperation.GetResult();
}

原生

/// Create a StandardIntegrityTokenRequest opaque object.
const char* requestHash = ...;
StandardIntegrityTokenRequest* tokenRequest;
StandardIntegrityTokenRequest_create(&tokenRequest);
StandardIntegrityTokenRequest_setRequestHash(tokenRequest, requestHash);

/// Prepare a StandardIntegrityToken opaque type pointer and call
/// StandardIntegrityTokenProvider_request(). Can be called several times for
/// different user actions. See above how to prepare token provider.
StandardIntegrityToken* token;
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_request(tokenProvider, tokenRequest, &token);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityToken_getStatus(token, &token_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrityToken = StandardIntegrityToken_getToken(token);
}
/// ...
/// Remember to free up resources.
StandardIntegrityTokenRequest_destroy(tokenRequest);
StandardIntegrityToken_destroy(token);
StandardIntegrityTokenProvider_destroy(tokenProvider);
StandardIntegrityManager_destroy();

解密並驗證完整性判定結果

您向系統索取完整性判定結果後,Play Integrity API 會提供已加密的回應權杖。您必須在 Google 伺服器上解密完整性權杖,才能取得裝置完整性判定結果。如要啟用這項功能,請完成下列步驟:

  1. 在連結至應用程式的 Google Cloud 專案中建立服務帳戶。在此流程中,您必須將服務帳戶使用者服務使用情形個人使用者角色授予服務帳戶。
  2. 在應用程式伺服器上,使用 playintegrity 範圍從服務帳戶的憑證中擷取存取權杖,然後提出下列要求:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. 讀取 JSON 回應。

產生的酬載是包含完整性判定結果的純文字權杖。

重播攻擊自動防護措施

為防範重播攻擊,Google Play 會自動確保每個完整性權杖都無法重複使用。若嘗試重複解密相同權杖,會產生空白的判定結果。