完整性判定結果

本頁面說明如何解讀及使用系統傳回的完整性判定結果。無論您是提出標準要求還是傳統 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 相容性規定,也可能未獲准執行 Google Play 服務。舉例來說,該裝置目前搭載的 Android 版本可能無法辨識、可能已解鎖系統啟動載入程式,或是可能尚未經過製造商認證。
MEETS_STRONG_INTEGRITY
應用程式在搭載 Google Play 服務的 Android 裝置上執行,而且裝置設有硬體支援的開機完整性等防護措施,足以充分確保系統完整性。該裝置已通過系統完整性檢查,符合 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 無法辨識裝置上安裝的應用程式版本 遊玩、
  • 裝置發生技術問題。

帳戶詳細資料欄位

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 安全防護,以及 發現已知的惡意軟體

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

應用程式存取風險判定結果 (Beta 版)

啟用此信號後,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 已知的服務。
EMPTY (空白值)

如果未符合必要條件,系統不會評估應用程式存取風險。於 在這個範例中,appAccessRiskVerdict 欄位是空白的。這在 這些因素包括:

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

應用程式存取風險信號會自動排除以下類型的已驗證無障礙服務: 已通過強化的 Google Play 無障礙功能審查 (安裝自 裝置上的任何應用程式商店)。「已排除」也就是經過驗證的 在該裝置上執行的服務就不會傳回擷取、控製或 在應用程式存取風險判定結果中重疊回應。如何要求進階 Google 服務 Google Play 對您的無障礙應用程式進行無障礙功能審查,並在 Google 上發布 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 安全防護警示採取特定動作。如果使用者無法出貨 您即可封鎖這些需求,而不被伺服器動作封鎖