# 設備證明 Level 2：Google Play Integrity API（平台替你判定）

Chance Lu

L0 的骨架是：**平台在保險箱裡蓋章，伺服器端負責驗章**。L0 也預告過：Google 有一種請求方式**由平台自動防重送**，你只要把這次請求的內容綁進去。這一站就把那句話對上具體名詞，看 Android 上的 **Play Integrity API** 怎麼跑。

> 這站學完，你要能用自己的話回答：
> ① standard 請求和 classic 請求各在什麼時候用？各自怎麼防重送？
> ② 判定結果分成哪幾類？每一類在問什麼？
> ③ 伺服器拿到判定結果後，除了「放行／拒絕」還能怎麼回應？
> ④ 跟 Apple App Attest 比，Google 最大的不同在哪？

## 先認得這站的新名詞

| 名詞 | 白話 |
| --- | --- |
| **Play Integrity API** | Google 的設備證明服務。回答「這是從 Google Play 裝的正版 App、跑在真的認證 Android 裝置上嗎？」 |
| **Google Play 元件** | 手機上 Google 提供的系統程式：Google Play Store 與 Google Play services 的合稱。**它屬於平台，不是你的 App。** |
| **integrity token** | App 向 Google Play 元件要到的一段**加密又簽過章**的資料。App 看不懂內容，只負責轉交。 |
| **verdict（判定結果）** | token 解密後的內容：一份 JSON，寫著平台**已經判斷好**的結論，例如「App 被 Google Play 認得」。 |
| **standard 請求** | 大多數情況用的請求方式：事先準備，之後每次要 token 都很快（平均幾百毫秒）。 |
| **classic 請求** | 原本的請求方式：每次都重新評估，比較慢（平均幾秒），只留給少數最重要的操作。 |
| **requestHash** | standard 請求用的欄位：**把這次請求內容做成的雜湊**，交給 Play Integrity，之後原封不動出現在判定結果裡。 |
| **Play Console** | Google Play 給開發者的管理後台：設定 App、開啟選用的判定項目、申請提高配額都在這裡。 |
| **service account** | 給伺服器用的 Google 帳號，讓你的伺服器有權限呼叫 Google 的 API（例如請 Google 解密 token）。 |

**角色對應**：L0 的「平台」在這裡＝**Google Play 元件（在手機上）＋安全硬體＋Google 伺服器**。

> 比喻延續「章」：Apple 是「開印鑑證明給你，伺服器自己存樣本、自己驗章」；Google 則像**把蓋好章的密封信封交給你**（信封＝加密，章＝簽章），你拿去 **Google 的驗件窗口**拆封，窗口直接告訴你結論。

## Play Integrity 是什麼

- **它回答三個核心問題**：App 有沒有被改過、裝置是不是真的認證裝置、使用者是不是從 Google Play 取得 App。
- **跟 L0 講的「硬體保護的金鑰」的關係**：Play Integrity 評估裝置時，會用到**硬體保護的安全訊號**。官方把這句連到 Android 的 Key Attestation（Android 用硬體金鑰替裝置作證的底層機制）。但它把不同 Android 版本、不同廠商金鑰的複雜度都包起來，你拿到的是結論，不用自己處理憑證鏈。要注意：Android 13 以前的 `MEETS_DEVICE_INTEGRITY` 在沒有硬體時，會退回軟體證明。
- **它不是唯一防線**：官方說它最適合**搭配其他訊號**，當作整體防濫用策略的一部分。

**兩種請求方式**，判定結果的格式一樣：

|  | standard 請求 | classic 請求 |
| --- | --- | --- |
| **適合** | 任何 App，隨時檢查任何操作 | 偶爾一次，檢查最有價值、最敏感的操作 |
| **速度** | 平均幾百毫秒（要先準備，約幾秒） | 平均幾秒 |
| **防竄改** | 用 `requestHash` 綁請求內容 | 用 `nonce` 綁請求內容 |
| **防重送** | **Google Play 自動處理** | **你自己**用 `nonce` 加伺服器邏輯處理 |

註：最低 Android 版本以 Overview 頁和 classic 頁的比較表為準，現行 library 兩種都是 Android 6.0 以上；standard、classic 頁首寫的 5.0／4.4 是 v1.3.0 以前的數字。

> 一句話：Play Integrity 是 Google 版的設備證明；大多數情況用 standard 請求，classic 只留給少數關鍵操作。

## standard 請求：流程

分兩段：**事先準備**，之後**每次需要時要一個 token**。

手機
  你的 App發請求的人
  Google Play 元件屬於平台
  你的伺服器轉交、做決定
  Google 伺服器解密、驗證
  
  ⓪ 準備
  準備會連到 Google 伺服器（約幾秒，事先做）
  ① 算 requestHash請求內容做雜湊
  ② request()
  token
  ③ token＋原本的請求
  ④ decodeIntegrityToken
  ⑤ 判定結果（JSON）
  ⑥ 比對、判斷requestHash 對不對判定結果夠不夠好
  ⑦ 決定（放行／限制／拒絕…）

圖 A：standard 請求。Google Play 元件背後用到安全硬體的訊號，但那屬於平台，不在你的 App 裡（點圖可放大）

### 事先準備（prepare／warm up）

- App 用 `StandardIntegrityManager`（App 端發 standard 請求的入口物件）呼叫 `prepareIntegrityToken()`，帶上你的 **Google Cloud 專案編號**（你在 Google Cloud 開的專案的編號，用來計算用量），拿到一個 **token provider**（準備好之後拿來要 token 的物件），放在記憶體裡。
- **為什麼要先準備**：讓 Google Play 先在裝置上準備好部分證明資訊，之後真正要 token 時就快很多。
- **什麼時候做**：App 啟動時、或在背景提前做。準備會連到伺服器，通常幾秒、大多數 10 秒內，官方建議逾時設長一點（例如 1 分鐘）。
- **準備不是註冊**：它不產生要你的伺服器保存的金鑰；token provider 用久了會過期；可以重做，每個 App 實例（一支手機上執行中的那份 App）每分鐘最多 5 次。

### 每次需要時：綁 requestHash、拿 token

1. **算 requestHash**：把這次操作的**所有相關參數**，用固定的方式排好再做雜湊（例如 SHA256）。最長 500 bytes。
2. **要 token**：用 `StandardIntegrityTokenRequest` 的 `setRequestHash` 帶入，呼叫 token provider 的 `request()`。
3. **送到你的伺服器**：把 token 和原本的請求一起送出。
4. **交給 Google 解密**：你的伺服器用 service account 取得存取權，呼叫 `decodeIntegrityToken`，拿回判定結果。

```
// Request integrity token by providing a user action request hash.
String requestHash = "2cp24z...";
Task<StandardIntegrityToken> integrityTokenResponse =
    integrityTokenProvider.request(
        StandardIntegrityTokenRequest.builder()
            .setRequestHash(requestHash)
            .build());
```

（程式碼摘自 Android Developers 官方文件。）

### requestHash 要綁什麼、怎麼驗

- **綁什麼**：跟這個操作有關的所有重要值。官方的例子是遊戲回報分數：把分數綁進去，伺服器就能確定分數沒被中途改掉。
- **為什麼要綁**：沒有 requestHash，token 只跟裝置綁在一起、不跟這則請求綁在一起，就留下攻擊空間。
- **伺服器怎麼驗**：用跟 App **一樣的方式**重算一次雜湊，跟判定結果裡的 `requestHash` 比對。不一樣就不能信。
- **別放明文**：這個值 App 和 Google 都看得到明文，敏感資料一律先雜湊。

### Google 怎麼替你防重送

standard 請求**自動防重送**：Google Play 會阻止同一個 token 被重複使用很多次。同一個 token 被重複解密時，判定結果會被**清空**：

- 裝置的判定變成空的。
- App 與帳號的判定變成 `UNEVALUATED`（未評估）。
- 在 Play Console 開啟的選用判定也變成 `UNEVALUATED`（多值的判定則變成空的）。

Google 擋的是**大量重複使用**，`timestampMillis` 還是要你自己檢查。對回 L0 的三項檢查，standard 下是這樣分工：

- **對不對**＝你比對 `requestHash`。
- **用過沒**＝Google 替你做。
- **過期沒**＝你檢查 `timestampMillis`。

> 一句話：standard 請求＝事先準備 → 每次把請求內容綁進 requestHash 拿 token → 你的伺服器交給 Google 解密；「用過沒」由 Google 處理，「對不對」和「過期沒」你自己查。

## classic 請求：只留給少數關鍵操作

- **什麼時候用**：偶爾一次、檢查最有價值或最敏感的操作，想在 standard 之外**多一層保證**時。它比較耗時間、流量和電，也有每日次數上限；不要每次 App 回到前景就叫。
- **想快取它就改用 standard**：官方說，如果你打算做一次 classic 請求再快取起來用，應該改做 standard 請求。
- **流程**：用 `IntegrityManager` 的 `requestIntegrityToken()`，帶一個 `nonce`。之後一樣把 token 送到你的伺服器。

### nonce 由誰產生、怎麼防重送

- **最好由伺服器產生**：一個惡意使用者猜不到的**全域唯一值**，例如伺服器端的密碼學安全亂數，或既有的 session／交易 ID；官方建議 128 bits 以上。在裝置上產生也可以，但比較不安全。
- **防重送**：伺服器要**記錄**發過（或看過）的值。收到判定結果時，值沒發過或已經用過，就拒絕。
- **同時防竄改**：把唯一值放進要保護的訊息裡，再對整個訊息做雜湊，拿雜湊當 nonce。伺服器重算雜湊比對，同時檢查唯一值。
- **格式**：URL-safe、不換行的 Base64 字串，16～500 個字元。

> **名詞提醒：三個長得像的東西**
> ・**L0 的 challenge**：伺服器發的一次性亂數，泛稱。
> ・**classic 的 `nonce`**：你交給 Play Integrity 的欄位值，內容是唯一值，或「唯一值＋訊息」的雜湊；會原封不動出現在判定結果裡。
> ・**L1 Apple 的 `nonce`**：你的伺服器用 authData 和 clientDataHash **算出來比對用**的值，不是你送出去的。

### token 解密的兩種方式

1. **交給 Google 伺服器解密（官方建議）**：跟 standard 一樣，呼叫 `decodeIntegrityToken`。
2. **自己解**：classic 可以選擇自己保管 Play Console 給的金鑰，在自己的伺服器解密、驗章；standard 只能交給 Google 解。

> 一句話：classic＝你自己產生唯一的 nonce、自己記錄防重送；token 可交給 Google 解，也可自己解。

## 判定結果：五類，各自在問什麼

不管 standard 或 classic，判定結果都是同一種格式的 JSON。官方要求**先驗 `requestDetails`，再看其他類別**。

判定結果解密後的 JSON
  requestDetails（先驗這個）這是我的 App、這次的請求嗎？時間夠新嗎？
  appIntegrity是 Google Play 認得、沒被改過的那個 App 嗎？
  deviceIntegrity這台裝置可信嗎？可信到哪一級？
  accountDetails使用者是從 Google Play 取得這個 App 的嗎？
  environmentDetails（選用）旁邊有沒有可疑的 App？Play Protect 有沒有開、有沒有發現問題？

圖 B：判定結果的五個類別（點圖可放大）

1. **`requestDetails`：這是我這次的請求嗎？**套件名稱要是你的 App，`requestHash`（classic 是 `nonce`）要對上，`timestampMillis` 要夠新。
2. **`appIntegrity`：是那個沒被改過的 App 嗎？**看 App 和它的簽章憑證是否符合 Google Play 發行的版本。
3. **`deviceIntegrity`：裝置可信嗎？**回傳一個或多個**等級標籤**；一個都不符合，就沒有標籤。
4. **`accountDetails`：使用者是從 Google Play 取得的嗎？**看這個帳號有沒有這個 App 的 Play 授權。
5. **`environmentDetails`（選用）：執行環境有沒有風險？**要在 Play Console 開啟才會出現。

**各類別的欄位與標籤（實作時看）**
- **`requestDetails`**：`requestPackageName`（官方註明可能在途中被偽造）、standard 的 `requestHash` 或 classic 的 `nonce`、`timestampMillis`。
- **`appIntegrity`**：`appRecognitionVerdict` 是 `PLAY_RECOGNIZED`／`UNRECOGNIZED_VERSION`／`UNEVALUATED`；另有 `packageName`、`certificateSha256Digest`、`versionCode`（這三個在未評估時不會出現）。
- **`deviceIntegrity`**：`deviceRecognitionVerdict` 的標籤如下；另可開啟 `deviceAttributes`（`sdkVersion`）與 `recentDeviceActivity`（`deviceActivityLevel`：`LEVEL_1`～`LEVEL_4`、`UNEVALUATED`，看過去一小時要了幾次 token）。

    | 標籤 | 預設或需開啟 | 意思 |
    | --- | --- | --- |
    | `MEETS_DEVICE_INTEGRITY` | 預設 | 真的、經過認證的 Android 裝置 |
    | `MEETS_BASIC_INTEGRITY` | 需在 Play Console 開啟 | 通過基本的系統完整性檢查，裝置不一定經過認證 |
    | `MEETS_STRONG_INTEGRITY` | 需在 Play Console 開啟 | 真的認證裝置，而且有近期安全更新 |
    | `MEETS_VIRTUAL_INTEGRITY` | 只在發佈到 Google Play Games for PC 時出現 | 帶 Google Play services 的 Android 模擬器，通過系統完整性檢查 |
- **`accountDetails`**：`appLicensingVerdict` 是 `LICENSED`／`UNLICENSED`／`UNEVALUATED`。
- **`environmentDetails`**：`appAccessRiskVerdict`（`appsDetected`：`KNOWN_`／`UNKNOWN_` 開頭的 `INSTALLED`、`CAPTURING`、`CONTROLLING`、`OVERLAYS`）、`playProtectVerdict`（`NO_ISSUES`、`NO_DATA`、`POSSIBLE_RISK`、`MEDIUM_RISK`、`HIGH_RISK`、`UNEVALUATED`）。

> 一句話：先確認 `requestDetails`（是這次的請求），再看 App、裝置、帳號、環境各自可不可信。

## 拿到判定結果後怎麼決定

### 先過基本檢查，再分級

1. **基本檢查**：`requestDetails` 對上；`appRecognitionVerdict` 是 `PLAY_RECOGNIZED`；`appLicensingVerdict` 是 `LICENSED`。
2. **依裝置可信度分級**。先知道兩件事：一台裝置會拿到它符合的**所有**標籤，而除了預設標籤，其他標籤要先在 Play Console 開啟；同一個標籤在 **Android 13 前後標準不同**。從高到低：
    - `MEETS_STRONG_INTEGRITY`（Android 13 以上）：最高級，要硬體訊號加上近期安全更新。
    - `MEETS_DEVICE_INTEGRITY`（Android 13 以上）與 `MEETS_STRONG_INTEGRITY`（Android 13 以前）：都靠硬體訊號。
    - `MEETS_DEVICE_INTEGRITY`（Android 13 以前）：有硬體就用硬體證明，沒有就退回軟體證明。
    - `MEETS_BASIC_INTEGRITY`：最弱的一級，再下去就是沒有任何標籤。
3. **再疊其他訊號**：例如 `recentDeviceActivity` 等級高的裝置，信任度就調低。

**Android 13 前後差在哪**：Android 13 以上，`MEETS_DEVICE_INTEGRITY` 要有硬體證明開機程式鎖定、系統是原廠認證映像；`MEETS_STRONG_INTEGRITY` 還要裝置所有分割區在**一年內**有安全更新。Android 12 以下的 `MEETS_STRONG_INTEGRITY` 只要求硬體證明開機完整性、不要求近期更新，所以要搭配 `deviceAttributes` 的 `sdkVersion` 一起判斷。

官方提醒：**越高等級的裝置越少**，只認最高等級會擋掉很多人，所以建議分級處理。

### 回應不只放行和拒絕

- 官方原句：可以用一系列相關的回應，例如 **Allow、Allow with limits、Allow with limits after CAPTCHA completion（完成「我不是機器人」驗證後限制放行）、Deny**。
- 官方也建議正式擋人前，**先只收集判定結果、不執行**，看清現有使用者的分布再決定。

（兩個平台共用的處理策略見 L3。）

**補救流程**：判定有問題時，App 可以用 `showDialog` 跳出 Google Play 的對話框請使用者自己修，例如 `GET_LICENSED` 引導到 Google Play 取得正版；要跳哪一種，由伺服器看完判定結果後決定。關掉對話框後再要一次 token 確認（standard 要重新準備）。

> 一句話：先過三項基本檢查，再依裝置等級分級回應；有問題時可以請使用者自己補救。

## 跟 L1 的 Apple App Attest 對照

|  | Apple App Attest | Google Play Integrity |
| --- | --- | --- |
| **誰驗** | **你的伺服器自己驗**憑證鏈、nonce、欄位 | **Google 伺服器**解密驗證，你拿回結果（classic 也可自己解） |
| **要一次性註冊嗎** | 要：attestation，伺服器存公鑰 | 不用**伺服器端**註冊（不必存公鑰）。standard 要由 App 事先準備，這步會連 Google 伺服器，但你的伺服器不參與 |
| **怎麼防重送** | 你發 challenge，加上 counter 遞增 | standard：Google 擋重複使用，你綁 requestHash、檢查時間戳<br>classic：你自己用 nonce 並記錄 |
| **伺服器拿到什麼** | 平台簽過的**證明原料**（註冊時是 Apple 開的憑證，之後是裝置金鑰蓋的章），結論自己算 | 平台**已經判斷好的判定結果**（JSON） |

兩邊差這麼多，伺服器要怎麼用同一套邏輯處理，是 L3 的主題。

## 常見的坑

- **每日配額**：預設「取 token」（classic 請求與 standard 的準備共用）和「在 Google 伺服器解密」**每天各 10,000 次**。要提高，得先把 Cloud 專案連結到 Play Console，再照 [Setup 頁](https://developer.android.com/google/play/integrity/setup)送出申請。
- **每分鐘上限（每個 App 實例）**：standard 準備 5 次、取 token 沒有公開上限；classic 取 token 5 次。所有請求在量很大時，仍受非公開的防禦性上限約束。
- **不要快取判定結果**：快取會提高 **proxying**（代理攻擊）的風險：壞人拿好裝置的判定結果，去別的環境冒用。需要時就做一次 standard 請求。
- **時效**：官方沒有公布 token 的固定有效期限；伺服器要自己訂一個允許的時間窗（官方範例叫 `ALLOWED_WINDOW_MILLIS`），`timestampMillis` 太舊就不收。token provider 用太久也會過期，下次請求出現 `INTEGRITY_TOKEN_PROVIDER_INVALID`，這時要重新準備一個。
- **Android 13 前後標準不同**：記得配 `sdkVersion` 判斷（見上方分級）。
- **沒有 Google Play 元件的裝置**：兩種請求都需要 Google Play Store 和 Google Play services。其中的 Google Play services 不可用或太舊時，會出現 `PLAY_SERVICES_NOT_FOUND`，可以用 `GET_INTEGRITY` 或 `GET_STRONG_INTEGRITY` 對話框引導使用者安裝或更新。
- **錯誤重試**：暫時性錯誤才重試。背景操作：用指數退避（每次重試的等待時間加倍：5 秒起、10 秒、20 秒）；重試三次仍失敗，就當作**沒通過所有檢查**。

> 一句話：配額要申請、結果別快取、時間戳要檢查、Android 13 前後標準不同、沒有 Google Play 元件就拿不到。

## ✔ 過關標準

能不看筆記回答下面 5 題，這站就過了。

1. standard 和 classic 請求各在什麼時候用？
standard：大多數情況，隨時檢查任何操作，事先準備、之後很快。classic：偶爾一次，只給最有價值或最敏感的操作，比較慢也比較耗資源。
2. 兩種請求各自怎麼防重送？
standard：Google Play 擋同一個 token 被大量重複使用（重複解密會清空結果）；你負責比對 requestHash、檢查 timestampMillis。classic：你自己產生唯一的 nonce，伺服器記錄用過的值，重複或沒發過的就拒絕。
3. 判定結果有哪五類？為什麼要先看 requestDetails？
requestDetails、appIntegrity、deviceIntegrity、accountDetails、environmentDetails（選用）。先看 requestDetails，是要確認這份結果真的是為你這次的請求產生的，其他類別才有意義。
4. 伺服器除了放行和拒絕，還能怎麼回應？
依裝置等級分級：例如 Allow、Allow with limits、Allow with limits after CAPTCHA completion、Deny；也可以由伺服器決定，請 App 跳出 Google Play 的補救對話框讓使用者自己修。
5. 跟 Apple App Attest 比，最大的不同是什麼？
Apple 要伺服器端的一次性註冊、你的伺服器自己驗證明、自己防重送；Google（standard）不用伺服器端註冊、由 Google 伺服器解密並擋重複使用，你拿到的是已經判斷好的結果。

## 下一站

Level 3 會回到伺服器端，看怎麼用同一套邏輯同時接住 Apple 和 Google 兩邊，以及驗證失敗時怎麼降級。

## 參考來源

- [Android Developers — Overview of the Play Integrity API](https://developer.android.com/google/play/integrity/overview)（三大判定、硬體訊號、搭配其他訊號、兩種請求比較表、分級與一系列回應、先收集再執行、不要快取）
- [Android Developers — Make a standard API request](https://developer.android.com/google/play/integrity/standard)（準備 token provider、requestHash、decodeIntegrityToken、自動防重送與清空規則、provider 過期）
- [Android Developers — Make a classic API request](https://developer.android.com/google/play/integrity/classic)（使用時機、nonce 的產生與格式、兩種解密方式、比較表與每分鐘上限）
- [Android Developers — Integrity verdicts](https://developer.android.com/google/play/integrity/verdicts)（判定結果的五個類別、各欄位與標籤、Android 13 前後的標準）
- [Android Developers — Remediation dialogs](https://developer.android.com/google/play/integrity/remediation)（`showDialog` 與對話框類型）
- [Android Developers — Set up Play Integrity API](https://developer.android.com/google/play/integrity/setup)（Cloud 專案、連結 Play Console、每日配額與申請提高）
- [Android Developers — Play Integrity API error codes](https://developer.android.com/google/play/integrity/error-codes)（重試策略、`PLAY_SERVICES_NOT_FOUND`）
- [Android Developers — Key and ID attestation](https://developer.android.com/privacy-and-security/security-key-attestation)（Overview 頁「硬體訊號」連到的底層機制）
