完整性判定結果

本頁面說明如何解讀及使用系統傳回的完整性判定結果。無論您是提出標準要求還是傳統 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 requested.
  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 13 以上版本的裝置,需要 Android 平台金鑰認證。該裝置可能不符合 Android 相容性規定,也可能未獲准執行 Google Play 服務。舉例來說,該裝置可能正在執行無法辨識的 Android 版本、系統啟動載入程式可能未鎖定,或是未取得製造商認證。
MEETS_STRONG_INTEGRITY
應用程式在搭載 Google Play 服務的 Android 裝置上執行,而且裝置設有硬體支援的開機完整性等防護措施,足以充分確保系統完整性。對於 Android 13 以上版本的裝置,則必須在過去一年內進行安全性更新。該裝置已通過系統完整性檢查,符合 Android 相容性規定。

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

近期裝置活動

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

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

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

deviceActivityLevel 定義會因模式而異,且可採用下列任一值:

近期裝置活動程度 過去一小時內,每個應用程式在這部裝置上提出的標準 API 完整性權杖要求次數 過去一小時內,每個應用程式在這部裝置上提出的傳統 API 完整性權杖要求數量
LEVEL_1 (最低) 10 部以下 5 或少於 5 本
LEVEL_2 11 到 25 之間 6 到 10 之間
LEVEL_3 介於 26 到 50 之間 11 到 15 歲
LEVEL_4 (最高) 超過 50 個 超過 15 個
UNEVALUATED 系統未評估近期裝置活動。這可能是因為:
  • 裝置可信度不足。
  • Google Play 無法辨識裝置上安裝的應用程式版本。
  • 裝置發生技術問題。

裝置屬性

您也可以選擇接收裝置屬性,瞭解裝置上執行的 Android 作業系統的 Android SDK 版本。日後可能會擴充其他裝置屬性。

SDK 版本值是 Build.VERSION_CODES 中定義的 Android SDK 版本號碼。如果未符合必要條件,系統不會評估 SDK 版本。在這種情況下,sdkVersion 欄位未設定,因此 deviceAttributes 欄位為空白。可能原因如下:

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

如果您選擇接收 deviceAttributesdeviceIntegrity 欄位就會有下列額外欄位:

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  deviceAttributes: {
    // 33 is one possible value, which represents Android 13 (Tiramisu).
    sdkVersion: 33
  }
}

如果未評估 SDK 版本,系統會將 deviceAttributes 欄位設為以下值:

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  deviceAttributes: {}  // sdkVersion field is not set.
}

帳戶詳細資料欄位

accountDetails 欄位包含單一值 appLicensingVerdict,代表裝置上已登入的使用者帳戶,其應用程式的 Google Play 授權狀態。如果使用者帳戶擁有應用程式的 Play 授權,表示使用者已從 Google Play 下載或購買該應用程式。

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!
}

環境詳細資料欄位

您也可以選擇接收有關環境的其他信號。應用程式存取風險會告知應用程式,是否有其他執行中的應用程式可用來擷取螢幕畫面、顯示重疊層,或控制裝置。Play 安全防護判定結果會指出裝置是否已啟用 Google Play 安全防護,以及是否發現已知惡意軟體。

如果您已在 Google Play 管理中心選擇加入應用程式存取風險判定結果或 Play 安全防護判定結果,API 回應就會提供 environmentDetails 欄位。environmentDetails 欄位可包含兩個值:appAccessRiskVerdictplayProtectVerdict

應用程式存取風險判定結果

啟用此信號後,Play Integrity API 酬載中的 environmentDetails 欄位就會包含新的應用程式存取風險判定結果。

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: {
      appAccessRiskVerdict: {
          // This field contains one or more responses, for example the following.
          appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
      }
 }
}

如果已評估應用程式存取風險,appAccessRiskVerdict 就會包含欄位 appsDetected,其中包含一或多個回應。這些回應會根據偵測到的應用程式安裝來源,歸入下列兩個群組之一:

  • Play 或系統應用程式:Google Play 安裝的應用程式,或裝置製造商在裝置系統分區中預先載入的應用程式 (以 FLAG_SYSTEM 標識)。這類應用程式的回應會加上前置字串 KNOWN_

  • 其他應用程式:非由 Google Play 安裝的應用程式,不包括裝置製造商在系統分區預先載入的應用程式。這類應用程式的回應會加上前置字串 UNKNOWN_

系統可傳回下列回應:

KNOWN_INSTALLEDUNKNOWN_INSTALLED
代表有已安裝的應用程式與對應的安裝來源相符。
KNOWN_CAPTURINGUNKNOWN_CAPTURING
代表有執行中的應用程式已啟用權限,可用於在應用程式執行期間查看螢幕畫面。但如果是在裝置上執行、且 Google Play 已知的經驗證無障礙服務,就不涵蓋在內。
KNOWN_CONTROLLINGUNKNOWN_CONTROLLING
有執行中的應用程式啟用了權限,可用於控制裝置並直接控管輸入到應用程式中的內容,甚至擷取應用程式的輸入和輸出內容。不過,如果是在裝置上執行、且 Google Play 已知的經驗證無障礙服務,就不涵蓋在內。
KNOWN_OVERLAYSUNKNOWN_OVERLAYS
代表有執行中的應用程式啟用了權限,可用於在應用程式上顯示疊加層。不過,如果是在裝置上執行、且 Google Play 已知的經驗證無障礙服務,就不涵蓋在內。
空白 (空白值)

如未符合必要條件,系統不會評估應用程式存取風險。在這種情況下,appAccessRiskVerdict 欄位為空白。這可能由許多因素造成,包括:

  • 裝置可信度不足。
  • 裝置板型規格不是手機、平板電腦或折疊式裝置。
  • 裝置未搭載 Android 6 (API 級別 23) 以上版本。
  • Google Play 無法辨識裝置上安裝的應用程式版本。
  • 裝置上的 Google Play 商店版本過舊。
  • 僅限遊戲:使用者帳戶沒有遊戲的 Play 授權。
  • 使用標準要求搭配 verdictOptOut 參數。
  • 您使用標準要求,但 Play Integrity API 程式庫版本尚不支援標準要求的應用程式存取風險。

無論裝置的安裝來源為何,應用程式存取風險信號都會自動排除已經過進階 Google Play 無障礙功能審查,且通過驗證的無障礙服務。「已排除」表示在裝置上執行的已驗證無障礙服務,不會在應用程式存取風險判定結果中傳回「capturing」、「controlling」或「overlays」回應。如要為無障礙應用程式申請進階 Google Play 無障礙審查,請在 Google Play 上發布應用程式,並確保應用程式資訊清單中的 isAccessibilityTool 標記設為 true,或申請審查

下表提供一些判定結果的示例,說明這些結果的含意 (表中並未列出所有可能的結果):

應用程式存取風險判定結果的回應示例 解釋
appsDetected:
["KNOWN_INSTALLED"]
只有 Google Play 可辨識的已安裝應用程式,或裝置製造商在系統分區預先載入的應用程式。
目前沒有任何執行中的應用程式會導致擷取、控制或重疊的判定結果。
appsDetected:
["KNOWN_INSTALLED",
"UNKNOWN_INSTALLED",
"UNKNOWN_CAPTURING"]
有 Google Play 安裝的應用程式,或裝置製造商在系統分區預先載入的應用程式。
目前有其他執行中的應用程式已啟用權限,可用來查看螢幕畫面或擷取其他輸入和輸出內容。
appsDetected:
["KNOWN_INSTALLED",
"KNOWN_CAPTURING",
"UNKNOWN_INSTALLED",
"UNKNOWN_CONTROLLING"]
有 Play 或系統應用程式正在執行,已啟用權限,可用於查看螢幕畫面或擷取其他輸入和輸出內容。
此外,也有其他已啟用權限的應用程式正在執行,可用於控制裝置並直接控管輸入到應用程式中的內容。
appAccessRiskVerdict: {} 由於未符合必要條件,系統不會評估應用程式存取風險。舉例來說,如果裝置可信度不足,就屬這種情況。

視風險等級而定,您可以決定要繼續採用哪些判定結果組合,以及要對哪些判定結果採取行動。以下程式碼片段舉例說明如何驗證是否沒有執行中的應用程式可擷取螢幕畫面或控制您的應用程式:

Kotlin

val environmentDetails =
    JSONObject(payload).getJSONObject("environmentDetails")
val appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict")

if (appAccessRiskVerdict.has("appsDetected")) {
    val appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}

Java

JSONObject environmentDetails =
    new JSONObject(payload).getJSONObject("environmentDetails");
JSONObject appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict");

if (appAccessRiskVerdict.has("appsDetected")) {
    String appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}
修復應用程式存取風險判定結果

視風險等級而定,您可以決定要對哪些應用程式存取風險判定結果採取行動,然後再讓使用者完成要求或操作。檢查應用程式存取風險判定結果後,您可以向使用者顯示選用的 Google Play 提示。您可以顯示 CLOSE_UNKNOWN_ACCESS_RISK,要求使用者關閉導致應用程式存取風險判定結果的不明應用程式;也可以顯示 CLOSE_ALL_ACCESS_RISK,要求使用者關閉導致應用程式存取風險判定結果的所有應用程式 (包括已知和不明應用程式)。

Play 安全防護判定結果

啟用後,Play Integrity API 酬載中的 environmentDetails 欄位就會包含 Play 安全防護判定結果:

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

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

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

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

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

  • 裝置可信度不足。
  • 僅限遊戲:使用者帳戶沒有遊戲的 Play 授權。

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

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

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