完整性判定結果

本頁面說明如何解讀及使用系統傳回的完整性判定結果。無論您是提出標準要求還是傳統 API 要求,完整性判定結果都會以同一形式傳回相似的內容。透過這份判定結果,您可以瞭解裝置、應用程式和帳戶的有效性資訊。如果判定結果經過解密及驗證,應用程式的伺服器便可使用其中產生的酬載,決定如何以最佳方式處理應用程式中的特定操作或要求。

傳回的完整性判定結果格式

酬載是純文字格式的 JSON,其中包含完整性信號和開發人員提供的資訊。

一般酬載的結構如下:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

您必須先檢查 requestDetails 欄位中的值是否與原始要求的值相符,再逐一檢查完整性判定結果。以下章節會詳細介紹各個欄位。

要求詳情欄位

requestDetails 欄位包含要求的相關資訊,包括開發人員在標準要求的 requestHash 和傳統要求的 nonce 中提供的資訊

標準 API 要求:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the request.
  requestPackageName: "com.package.name"
  // Request hash provided by the developer.
  requestHash: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the integrity token
  // was prepared (computed on the server).
  timestampMillis: "1675655009345"
}

這些值應與原始要求的值相符。因此,請確認 requestPackageNamerequestHash 與原始要求中傳送的值相符,藉此驗證 JSON 酬載的 requestDetails 部分,如下列程式碼片段所示:

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val requestHash = requestDetails.getString("requestHash")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

RequestDetails requestDetails =
    decodeIntegrityTokenResponse
    .getTokenPayloadExternal()
    .getRequestDetails();
String requestPackageName = requestDetails.getRequestPackageName();
String requestHash = requestDetails.getRequestHash();
long timestampMillis = requestDetails.getTimestampMillis();
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request.
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

傳統 API 要求:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded URL-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: "1617893780"
}

這些值應與原始要求的值相符。因此,請確認 requestPackageNamenonce 與原始要求中傳送的值相符,以驗證 JSON 酬載的 requestDetails 部分,如下列程式碼片段所示:

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val nonce = requestDetails.getString("nonce")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName = requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

應用程式完整性欄位

appIntegrity 欄位含有套件的相關資訊。

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates (base64-encoded URL-safe).
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict 的值可能如下:

PLAY_RECOGNIZED
應用程式和憑證符合 Google Play 發行的版本。
UNRECOGNIZED_VERSION
憑證或套件名稱與 Google Play 記錄不符。
UNEVALUATED
系統無法評估應用程式完整性,原因是未符合必要條件,例如裝置可信度不足。

為確保權杖是由您建立的應用程式產生,請確認應用程式完整性是否符合預期,如下列程式碼片段所示:

Kotlin

val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict")

if (appRecognitionVerdict == "PLAY_RECOGNIZED") {
    // Looks good!
}

Java

JSONObject appIntegrity =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    appIntegrity.getString("appRecognitionVerdict");

if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) {
    // Looks good!
}

您也可以手動檢查應用程式套件名稱、應用程式版本和應用程式憑證。

裝置完整性欄位

deviceIntegrity 欄位可包含單一值 deviceRecognitionVerdict,後者包含一或多個標籤,代表裝置可強制執行應用程式完整性檢查的程度。如果裝置不符合任何標籤的條件,deviceIntegrity 欄位就會留空。

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

根據預設,deviceRecognitionVerdict 可包含下列項目:

MEETS_DEVICE_INTEGRITY
應用程式在搭載 Google Play 服務的 Android 裝置上執行。該裝置已通過系統完整性檢查,符合 Android 相容性規定。
空白 (空白值)
執行應用程式的裝置可能遭受攻擊 (例如 API 掛鉤) 或系統遭到入侵 (例如已啟用 Root 權限);或者,應用程式不是在實體裝置 (例如未通過 Google Play 完整性檢查的模擬器) 上執行。

為確保權杖是來自可信任的裝置,請驗證 deviceRecognitionVerdict 是否符合預期,如下列程式碼片段所示:

Kotlin

val deviceIntegrity =
    JSONObject(payload).getJSONObject("deviceIntegrity")
val deviceRecognitionVerdict =
    if (deviceIntegrity.has("deviceRecognitionVerdict")) {
        deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    } else {
        ""
    }

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

Java

JSONObject deviceIntegrity =
    new JSONObject(payload).getJSONObject("deviceIntegrity");
String deviceRecognitionVerdict =
    deviceIntegrity.has("deviceRecognitionVerdict")
    ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    : "";

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

如果測試裝置在達到完整性上出現問題,請確認原廠 ROM 已安裝完成 (例如,透過重新設定裝置的方式),且系統啟動載入程式已鎖定。您也可以在 Play 管理中心建立 Play Integrity API 測試

條件式裝置標籤

如果應用程式要發布至 Google Play 遊戲電腦版deviceRecognitionVerdict 也可以包含下列標籤:

MEETS_VIRTUAL_INTEGRITY
應用程式在搭載 Google Play 服務且支援 Android 的模擬器上執行。該模擬器已通過系統完整性檢查,符合主要的 Android 相容性規定。

可選用的裝置資訊

如果您選擇在完整性判定結果中接收其他標籤deviceRecognitionVerdict 可能包含以下額外標籤:

MEETS_BASIC_INTEGRITY
執行應用程式的裝置通過了基本系統完整性檢查。該裝置可能不符合 Android 相容性規定,也可能未獲准執行 Google Play 服務。舉例來說,該裝置目前搭載的 Android 版本可能無法辨識、可能已解鎖系統啟動載入程式,或是可能尚未經過製造商認證。
MEETS_STRONG_INTEGRITY
應用程式在搭載 Google Play 服務的 Android 裝置上執行,而且裝置設有硬體支援的開機完整性等防護措施,足以充分確保系統完整性。該裝置已通過系統完整性檢查,符合 Android 相容性規定。

如果單一裝置符合所有標籤條件,就會在裝置完整性判定結果中傳回多個裝置標籤。

近期裝置活動 (Beta 版)

您也可以選擇接收最近的裝置活動,瞭解應用程式在過去一小時內針對特定裝置要求取得完整性權杖的次數。近期裝置活動資訊可幫助應用程式防範非預期的超活躍裝置。在該情況下,表示裝置可能正在遭受攻擊。您可以根據在一般裝置上安裝的應用程式每小時要求完整性權杖的預期次數,決定對每個近期裝置活動層級的信任程度。

如果選擇接收 recentDeviceActivitydeviceIntegrity 欄位就會有兩個值:

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  recentDeviceActivity: {
    // "LEVEL_2" is one of several possible values.
    deviceActivityLevel: "LEVEL_2"
  }
}

deviceActivityLevel 可含有下列其中一個值:

LEVEL_1
最低層級:過去一小時內,應用程式在這部裝置上要求 10 個以下的完整性權杖。
LEVEL_2
過去一小時內,應用程式在這部裝置上要求 11 到 25 個完整性權杖。
LEVEL_3
過去一小時內,應用程式在這部裝置上要求 26 至 50 個完整性權杖。
LEVEL_4
最高層級:過去一小時內,應用程式在這部裝置上要求超過 50 個完整性權杖。
UNEVALUATED

系統未評估近期裝置活動等級。這可能由許多因素造成,包括:

  • 裝置的可信度不足。
  • Google Play 無法辨識裝置上安裝的應用程式版本。
  • 裝置發生技術問題。

層級定義為概略值,可能有所變動。

帳戶詳細資料欄位

accountDetails 欄位包含單一值 appLicensingVerdict,代表應用程式的授權狀態。

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict 可含有下列其中一個值:

LICENSED
使用者擁有應用程式授權。也就是說,使用者是從 Google Play 安裝或購買您的應用程式。
UNLICENSED
使用者沒有應用程式授權。這可能是因為使用者側載了您的應用程式,或者不是從 Google Play 取得應用程式。 您可以向使用者顯示 GET_LICENSED 對話方塊,解決這個問題。
UNEVALUATED

由於未符合必要條件,系統無法評估授權詳細資料。

這可能由許多因素造成,包括:

  • 裝置的可信度不足。
  • Google Play 無法辨識裝置上安裝的應用程式版本。
  • 使用者未登入 Google Play。

依照下列程式碼片段所示,您可驗證 appLicensingVerdict 是否顯示預期結果,即可瞭解使用者是否擁有應用程式授權:

Kotlin

val accountDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = accountDetails.getString("appLicensingVerdict")

if (appLicensingVerdict == "LICENSED") {
    // Looks good!
}

Java

JSONObject accountDetails =
    new JSONObject(payload).getJSONObject("accountDetails");
String appLicensingVerdict = accountDetails.getString("appLicensingVerdict");

if (appLicensingVerdict.equals("LICENSED")) {
    // Looks good!
}

環境詳細資料欄位

如果您已在 Google Play 管理中心選擇加入 Play 安全防護判定結果,API 回應就會提供 environmentDetails 欄位。environmentDetails 欄位包含單一值 playProtectVerdict,可提供裝置上的 Google Play 安全防護相關資訊。

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict 可含有下列其中一個值:

NO_ISSUES
已啟用 Play 安全防護功能,且未在裝置上發現任何應用程式問題。
NO_DATA
已啟用 Play 安全防護功能,但尚未執行掃描作業。最近可能重設過裝置或 Play 商店應用程式。
POSSIBLE_RISK
已停用 Play 安全防護功能。
MEDIUM_RISK
已啟用 Play 安全防護功能,並發現裝置上安裝了可能有害的應用程式。
HIGH_RISK
已啟用 Play 安全防護功能,並發現裝置上安裝了危險的應用程式。
UNEVALUATED

未評估 Play 安全防護判定結果。

這可能由許多因素造成,包括:

  • 裝置的可信度不足。
  • 僅限遊戲:使用者帳戶未取得 LICENSED

Play 安全防護判定結果的使用指南

應用程式的後端伺服器可以按照風險容忍度,根據判定結果決定處置方式。以下提供一些建議和可能的使用者動作:

NO_ISSUES
Play 安全防護功能已啟用,且未發現任何問題,因此使用者無須採取任何動作。
POSSIBLE_RISKNO_DATA
收到這些判定結果時,要求使用者檢查是否已啟用 Play 安全防護功能,且已執行掃描作業。NO_DATA 應該只會在極少數情況下出現。
MEDIUM_RISKHIGH_RISK
視風險容忍度而定,您可以要求使用者啟動 Play 安全防護功能,並對 Play 安全防護警示採取特定動作。如果使用者無法完成這些要求,您可以透過伺服器動作封鎖危險的應用程式。