在 Android 應用程式中加入 Play Integrity

1. 簡介

上次更新時間:2023 年 1 月 4 日

什麼是 Play Integrity?

Play Integrity API 可協助防範應用程式和遊戲中發生具有潛在風險的詐欺性的互動。您可以使用 Play Integrity API 取得應用程式和裝置的完整性判定結果,據以採取適當行動回應,減少攻擊與濫用行為,例如詐欺、作弊及未經授權的存取。

先前的解決方案

Play Integrity API 取代了兩種先前的解決方案:應用程式授權程式庫SafetyNet Attestation API。新應用程式均建議使用 Play Integrity。如果是採用先前解決方案的現有應用程式,則應考慮更新,改為使用 Play Integrity。

選擇路徑

Play Integrity 為下列不同類型的應用程式提供多種程式庫選項:

  • 使用 C/C++ 編寫的遊戲或其他應用程式
  • 使用 Kotlin 或 Java 程式設計語言編寫的應用程式
  • 使用 Unity 引擎開發的遊戲

本程式碼研究室提供適用於這三個選項的路徑。您可以選擇與開發需求相關的路徑。這個程式碼研究室會說明如何建構及部署後端伺服器。這三種應用程式類型可共用此伺服器。

建構項目

在本程式碼研究室中,您會將 Play Integrity API 整合至範例應用程式,並使用此 API 檢查裝置和應用程式完整性。您將部署一個小型後端伺服器應用程式,用來支援完整性檢查程序。

課程內容

  • 如何將 Play Integrity 程式庫整合至應用程式
  • 如何使用 Play Integrity API 執行完整性檢查
  • 如何使用伺服器安全處理完整性判定結果
  • 如何解讀完整性判定結果

本程式碼研究室著重於 Play Integrity,不會詳細說明無關的概念和程式碼區塊,但會事先準備好這些程式碼區塊,屆時您只要複製及貼上即可。

軟硬體需求

  • Google 帳戶所具備的 Android 開發人員資格處於有效註冊期間,且能存取 Play 管理中心和 Google Cloud 控制台。
  • 如要採用 C++ 或 Kotlin 路徑,須具備 Android Studio 2021.1.1 以上版本。
  • 如要採用 Unity 路徑,須具備 Unity 2020 LTS 以上版本。
  • 可連接至電腦的 Android 裝置,且已啟用開發人員選項USB 偵錯功能。

本程式碼研究室提供實用資源的連結,可讓您瞭解如何簽署及上傳版本至 Play 管理中心,不過我們假設您對這項程序有一定程度的瞭解。

2. 取得程式碼

您可在 Git 存放區中取用程式碼研究室專案。存放區的父項目錄中有四個目錄:

  • server 目錄包含程式碼,適用於要部署的範例伺服器。
  • cpp 目錄包含 Android Studio 專案,可用來將 Play Integrity 加入 C++ 遊戲或應用程式。
  • kotlin 目錄包含 Android Studio 專案,可用來將 Play Integrity 加入標準 Android 應用程式。
  • unity 目錄包含使用 Unity 引擎 2020 LTS 版本建立的專案,可用來將 Play Integrity 加入 Unity 專案。

每個用戶端範例目錄都有兩個子目錄:

  • start 目錄,其中包含要在這個程式碼研究室修改的專案版本。
  • final 目錄,其中包含在程式碼研究室完成後應呈現的專案版本。

複製存放區

透過指令列,變更為要容納根層級 add-play-integrity-codelab 目錄的目錄,然後使用下列語法從 GitHub 複製專案:

git clone https://github.com/android/add-play-integrity-codelab.git

新增依附元件 (僅限 C++)

如要使用 C++ 路徑,您需要初始化存放區子模組,設定用於使用者介面的 Dear ImGui 程式庫,方法是透過指令列執行以下操作:

  1. 將工作目錄變更為:add-play-integrity-codelab/cpp/start/third-party
  2. git submodule update --init

3. 瞭解用戶端函式與伺服器函式

Play Integrity 安全性模型的一大關鍵,就是將驗證作業從裝置移到由您控管的安全伺服器中。在伺服器上執行這些作業有助於防範各種情況,例如遭駭裝置企圖部署重播攻擊或竄改訊息內容。

本程式碼研究室的範例伺服器只是示例,為求簡單明瞭,不會包含正式環境中需要的許多功能。所產生的值清單只會儲存在記憶體中,不會備份在永久儲存空間。此外,伺服器端點也未設定為需要驗證

performCommand 端點

伺服器會提供透過 HTTP POST 要求存取的 /performCommand 端點。要求主體應為具有下列鍵/值組合的 JSON 酬載:

{
   "commandString": "command-here",
   "tokenString": "token-here"
}

/performCommand 端點會傳回具有下列鍵/值組合的 JSON 酬載:

{
   "commandSuccess": true/false,
   "diagnosticMessage": "summary text",
   "expressToken": "token-here"
}

commandString 參數的實際內容並不重要。伺服器會使用 Play Integrity 驗證 commandString 的完整性,但不會使用指令值。所有用戶端版本均使用這個值:"TRANSFER FROM alice TO bob CURRENCY gems QUANTITY 1000"

tokenString 參數必須是以下任一種值:

  • Play Integrity API 產生的權杖
  • 先前成功呼叫 /performCommand 後傳回的快速權杖

伺服器會解密並驗證 Play Integrity API 提供的權杖。解密結果是完整性信號的 JSON 酬載。伺服器可根據信號值,選擇核准或拒絕指令。如果權杖已成功解密,系統會在 diagnosticMessage 中傳回信號的摘要說明。在正式版應用程式中,一般不會將這項資訊傳回給用戶端,但程式碼研究室的用戶端會運用這項資訊來顯示作業結果,這樣就不必查看伺服器記錄。如果在處理權杖時發生錯誤狀況,系統會在 diagnosticMessage 中傳回錯誤。

產生 Nonce

提出 Play Integrity 要求時,必須產生 Nonce,並與要求建立關聯。使用 Nonce 可確保該完整性要求不會重複,且只會處理一次。此外,Nonce 也可用來確認與完整性要求相關的訊息內容未遭竄改。如要進一步瞭解 Play Integrity Nonce,請參閱說明文件

在本程式碼研究室中,產生 Nonce 的方法是結合下列兩個值:

  • 128 位元隨機號碼,由經過加密的安全隨機號碼產生器產生
  • commandString 值的 SHA-256 雜湊

Play Integrity API 預期 Nonce 是網址編碼的無間距 Base64 字串。為建立 Nonce 字串,本程式碼研究室會將隨機號碼和雜湊值的位元組陣列轉換為十六進位字串,並串連在一起。產生的字串是有效的 Base64 字串,但不會進行編碼或解碼。

程式碼研究室的用戶端會使用 HTTP GET 要求呼叫伺服器上的 /getRandom 端點,擷取這組隨機號碼。這是最安全的隨機號碼產生方式,因為伺服器可驗證在指令要求中使用的是隨機號碼的來源。但是,這種做法確實需要對伺服器進行額外的往返作業。如果用戶端自行產生隨機號碼,就不需進行這項往返作業,但必須在安全性上有所取捨。

快速權杖

呼叫 PIA 非常昂貴,因此伺服器也會提供快速權杖,這種替代驗證方式成本較低,安全性也較低。成功呼叫 /serverCommand 並通過完整性檢查後,系統會透過 expressToken 欄位傳回快速權杖。呼叫 Play Integrity API 的運算成本高昂,僅供用來保護高價值作業。傳回快速權杖時,伺服器會提供安全性較低的驗證程序,適用於執行較不重要的作業,或作業過於頻繁,無法使用 Play Integrity API 進行完整驗證的情況。呼叫 /serverCommand 時,可以使用快速權杖,不要使用 Play Integrity 權杖。每組快速權杖都只能使用一次。使用有效的快速權杖成功呼叫 /serverCommand 時,系統會傳回新的快速權杖。

在程式碼研究室實作中,伺服器只會產生一組快速權杖,因此指令仍然可以防範重播攻擊。不過,快速權杖的安全性較低,因為這種權杖省略了防範指令修改的雜湊保護措施,且無法偵測對 Play Integrity API 初始呼叫後發生的裝置修改。

程式碼研究室的伺服器架構

在本程式碼研究室中,您可以使用以 Kotlin 編寫且採用 Ktor 架構的範例伺服器程式,將伺服器例項化。這個專案包含設定檔,可將專案部署至 Google Cloud App Engine。本程式碼研究室提供建構及部署至 App Engine 的操作說明。如果您使用其他雲端服務供應商,Ktor 也提供多種雲端服務的部署操作說明,讓您據以修改專案,並部署至慣用的服務。您也可以選擇部署至本機伺服器例項。

您不一定要使用程式碼範例設定伺服器。如果您有偏好的網頁應用程式架構,可以用程式碼研究室的範例伺服器做為參考,在自己的架構中實作 /getRandom/performCommand 端點。

用戶端設計

這三個用戶端版本 (C++、Kotlin 和 Unity 引擎) 都會顯示類似的使用者介面。

「Request random」按鈕會呼叫伺服器上的 /getRandom 端點,其結果會顯示在文字欄位中。在加入 Play Integrity 整合之前,您可以使用這個結果驗證伺服器連線和函式。

在程式碼研究室一開始時,「Call server with Integrity check」按鈕不會執行任何動作。請按照下列步驟,新增執行下列作業的程式碼:

  • 呼叫 /getRandom 取得隨機號碼
  • 產生 Nonce
  • 使用 Nonce 建立 Play Integrity 要求
  • 使用 Play Integrity 要求產生的權杖呼叫 /performCommand
  • 顯示 /performCommand 的結果

指令執行結果會顯示在文字欄位中。如果成功執行指令,這就是由 Play Integrity 檢查傳回的裝置判定資訊摘要。

成功執行「Call server with Integrity check」作業後,畫面上會顯示「Call server with express token」按鈕。這個按鈕會使用前一個 /performCommand 提供的快速權杖呼叫 /performCommand。系統會使用一個文字欄位顯示指令執行成功或失敗,並將傳回的快速權杖值顯示在用於隨機號碼的文字欄位中。

Play Integrity API 函式會傳回錯誤代碼來回報錯誤。如要進一步瞭解這些錯誤代碼,請參閱 Play Integrity 說明文件。部分錯誤的發生原因可能是環境狀況,例如網路連線不穩定或超載裝置。如果是這類錯誤,建議您加入以指數輪詢策略進行重試的選項。在本程式碼研究室中,Play Integrity 錯誤會顯示在「Logcat」中。

4. 設定 Google Cloud App Engine

如要使用 Google Cloud,請執行下列步驟:

  1. 前往 Google Cloud Platform 註冊。請使用您註冊 Play 管理中心的 Google 帳戶。
  2. 建立帳單帳戶
  3. 安裝並初始化 Google Cloud SDK

使用新安裝的 Google Cloud CLI 執行下列指令:

  1. 安裝 Java 適用的 App Engine 擴充功能:gcloud components install app-engine-java
  2. 建立新的 Cloud 專案,並將以下指令中的 $yourname 替換為專屬 ID:gcloud projects create $yourprojectname --set-as-default
  3. 在 Cloud 專案中建立 App Engine 應用程式:gcloud app create

5. 部署伺服器

設定應用程式套件名稱

在這個步驟中,您要將應用程式套件名稱新增至伺服器程式碼。在文字編輯器中,開啟 ValidateCommand.kt 來源檔案,這個檔案位於以下目錄中:

add-play-integrity-codelab/server/src/main/kotlin/com/google/play/integrity/codelab/server/util

找出以下指令列,將預留位置文字替換為專屬套件 ID,然後儲存檔案:

const val APPLICATION_PACKAGE_IDENTIFIER = "com.your.app.package"

您稍後會先在用戶端專案中設定這個 ID,再將應用程式上傳至 Play 管理中心。

建構伺服器並部署至 App Engine

使用 Google Cloud CLI 從 add-play-integrity/server 目錄執行下列指令,建構及部署伺服器:

Linux 或 macOS:

./gradlew appengineDeploy

Microsoft Windows:

gradlew.bat appengineDeploy

成功部署後,記下輸出內容中的「Deployed service」位置。您需要使用這個網址來設定用戶端與伺服器的通訊功能。

驗證部署

您可以使用 Google Cloud CLI 執行下列指令,確認伺服器是否正常運作:

gcloud app browse

這個指令會開啟網路瀏覽器並前往根網址。從根網址存取內容時,範例伺服器應會顯示「Hello World!」訊息。

6. 在 Play 管理中心設定應用程式

在 Play 管理中心設定應用程式完整性

如果您在 Play 管理中心內已有應用程式項目,可用於此程式碼研究室。您也可以按照步驟操作,在 Play 管理中心建立新的應用程式。在 Play 管理中心選取或建立應用程式後,您需要設定應用程式完整性。請在 Play 管理中心左側選單的「版本」部分選取「應用程式完整性」

請按一下「連結雲端專案」按鈕,然後選取要搭配伺服器使用的 Google Cloud 專案,再按一下「連結專案」按鈕。

伺服器的 Google Cloud 存取權

您的後端伺服器必須解密由 Play Integrity API 在用戶端產生的完整性權杖。Play Integrity 提供兩種金鑰管理方式:由 Google 產生及管理的金鑰,或由開發人員提供的金鑰。本程式碼研究室會採用建議的預設行為,亦即由 Google 管理的金鑰。

使用 Google 管理的金鑰時,為解密完整性權杖,後端伺服器會將經過加密的完整性權杖傳遞至 Google Play 伺服器。程式碼研究室的伺服器會使用 Google API 用戶端程式庫與 Google Play 伺服器通訊。

現在伺服器已啟動並順利運作,您也已在 Play 管理中心設定應用程式,可以開始根據所選平台自訂一或多個用戶端。我們會分別說明特定平台的所有步驟,因此如果您未使用該平台,即可略過相關操作說明。

7. 建構及執行用戶端 (C++)

執行 Android Studio。在「Welcome to Android Studio」視窗中按一下「Open」按鈕,然後開啟位於 add-play-integrity-codelab/cpp/start 的 Android Studio 專案。

更新應用程式 ID

將版本上傳至 Google Play 前,需要將應用程式 ID 從預設值變更為專屬名稱。請執行下列步驟:

  1. 在 Android Studio 的「Project」窗格中,找到 start/app 底下的 build.gradle 檔案並開啟檔案。
  2. 找出 applicationId 陳述式。
  3. com.google.play.integrity.codelab.cpp 變更為您在部署伺服器時所選的套件名稱,然後儲存檔案。
  4. 檔案頂端會顯示橫幅,指出 Gradle 檔案已變更。按一下「Sync Now」,重新載入及重新同步處理檔案。
  5. 在 Android Studio 的「Project」窗格中,開啟 start/app/src/main 底下的 AndroidManifest.xml 檔案。
  6. 找出 package="com.example.google.codelab.playintegritycpp" 陳述式。
  7. com.example.google.codelab.playintegritycpp 替換成專屬套件名稱,然後儲存檔案。
  8. 在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/java/com.example.google.codelab.playintegritycpp 底下的 PlayIntegrityCodelabActivity 檔案。
  9. 找出 package com.example.google.codelab.playintegritycpp 陳述式。
  10. com.example.google.codelab.playintegritycpp 替換成專屬套件名稱。
  11. 在新的套件名稱上按一下滑鼠右鍵,然後選擇「Show Context Actions」
  12. 選擇「Move to」(新的套件名稱)。
  13. 如果檔案頂端有顯示「Sync Now」按鈕,請選擇該按鈕。

更新伺服器網址

您需要更新專案,使其指向部署伺服器的網址位置。

  1. 在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/cpp 底下的 server_urls.hpp 檔案。
  2. 將部署伺服器時顯示的根網址加入 GET_RANDOM_URLPERFORM_COMMAND_URL 定義中,然後儲存檔案。

結果應該類似於以下內容:

constexpr char GET_RANDOM_URL[] = "https://your-play-integrity-server.uc.r.appspot.com/getRandom";
constexpr char PERFORM_COMMAND_URL[] = "https://your-play-integrity-server.uc.r.appspot.com/performCommand";

具體網址會因專案名稱和用於部署伺服器的 Google Cloud 區域而異。

建構及執行

連結專為開發目的設定的 Android 裝置。在 Android Studio 中,建立專案並在已連結的裝置上執行。應用程式應如下所示:

429ccc112f78d454.png

輕觸「Request Random」按鈕執行程式碼,向伺服器發出 HTTP 要求,取得隨機號碼。經過短暫延遲後,畫面上應會顯示隨機號碼:

62acee42ba1fa80.png

如果系統顯示錯誤訊息,「Logcat」窗格的輸出內容可能包含更多詳細資料。

藉由成功擷取隨機值來驗證伺服器通訊功能後,就可以開始整合 Play Integrity API。

8. 將 Play Integrity 加入專案 (C++)

下載 SDK

您需要下載 Play Core SDK 並解壓縮。請執行下列步驟:

  1. 前往「Play Core 原生 SDK」頁面,下載封裝在 .zip 檔案中的 Play Core SDK。
  2. 將 ZIP 檔案解壓縮。
  3. 確認新解壓縮的目錄名為 play-core-native-sdk,並複製該目錄或移至 add-play-integrity-codelab/cpp/start 目錄中。

更新 build.gradle

在 Android Studio 的「Project」窗格中,開啟 start/app 目錄內的模組層級 build.gradle 檔案。

apply plugin: 'com.android.application' 行下方加入以下指令行:

def playcoreDir = file("../play-core-native-sdk")

defaultConfig 區塊中找出 externalNativeBuild 區塊,變更 cmake 區塊內的 arguments 陳述式,如下所示:

                arguments "-DANDROID_STL=c++_shared",
                          "-DPLAYCORE_LOCATION=$playcoreDir"

android 區塊內的結尾處加入以下內容:

    buildTypes {
        release {
            proguardFiles getDefaultProguardFile("proguard-android.txt"),
                          "proguard-rules.pro",
                          "$playcoreDir/proguard/common.pgcfg",
                          "$playcoreDir/proguard/integrity.pgcfg"
        }
    }

dependencies 區塊內的結尾處加入這一行:

    implementation files("$playcoreDir/playcore.aar")

儲存變更。檔案頂端會顯示橫幅,指出 Gradle 檔案已變更。按一下「Sync Now」,重新載入及重新同步處理檔案。

更新 CMakeList.txt

在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/cpp 目錄內的 CMakeLists.txt 檔案。

find_package 指令下方加入以下指令行:

include("${PLAYCORE_LOCATION}/playcore.cmake")
add_playcore_static_library()

找出 target_include_directories(game PRIVATE 行,在下方加入以下指令行:

        ${PLAYCORE_LOCATION}/include

找出 target_link_libraries(game 行,在下方加入以下指令行:

        playcore

儲存檔案。檔案頂端會顯示橫幅,指出外部建構檔案已變更。按一下「Sync Now」,重新載入及重新同步處理檔案。

從「Build」選單中選擇「Make Project」,並確認專案是否建構成功。

9. 提出完整性要求 (C++)

應用程式要取得完整性資訊時,會使用 Play Integrity API 要求權杖,讓您傳送給伺服器進行解密及驗證。您現在可以在專案中加入程式碼,用於初始化 Play Integrity API 並提出完整性要求。

新增指令按鈕

在實際的應用程式或遊戲中,您可以在特定活動 (例如商店購物或加入多人遊戲工作階段) 之前執行完整性檢查。在本程式碼研究室中,我們會在 UI 中新增按鈕,手動觸發完整性檢查並呼叫伺服器,同時傳遞產生的 Play Integrity 權杖。

程式碼研究室專案包含 client_manager.cppclient_manager.hpp 來源檔案中定義的 ClientManager 類別。為了方便起見,這個檔案已加入專案,但缺少目前要新增的實作程式碼。

如要新增 UI 按鈕,請先在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/cpp 目錄中的 demo_scene.cpp 檔案。首先,找出空白的 DemoScene::GenerateCommandIntegrity() 函式並加入以下程式碼:

    const auto commandResult =
            NativeEngine::GetInstance()->GetClientManager()->GetOperationResult();
    if (commandResult != ClientManager::SERVER_OPERATION_PENDING) {
        if (ImGui::Button("Call server with integrity check")) {
            DoCommandIntegrity();
        }
    }

接著,找出空白的 DemoScene::DoCommandIntegrity() 函式,加入下列程式碼︰

    ClientManager *clientManager = NativeEngine::GetInstance()->GetClientManager();
    clientManager->StartCommandIntegrity();
    mServerRandom = clientManager->GetCurrentRandomString();

儲存檔案。您現在可以更新範例的 ClientManager 類別,新增實際的 Play Integrity 功能。

更新管理員標頭檔案

在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/cpp 目錄中的 client_manager.hpp 檔案。

#include "util.hpp" 行下方加入下列指令行,納入 Play Integrity API 的標頭檔案:

#include "play/integrity.h"

ClientManager 類別需要保留對 IntegrityTokenRequestIntegrityTokenResponse 物件的參照。請在 ClientManager 類別定義底部新增以下幾行:

    IntegrityTokenRequest *mTokenRequest;
    IntegrityTokenResponse *mTokenResponse;

儲存檔案。

初始化及關閉 Play Integrity

在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/cpp 目錄中的 client_manager.cpp 檔案。

找出 ClientManager::ClientManager() 建構函式。使用下列程式碼取代 mInitialized = false; 陳述式:

    mTokenRequest = nullptr;
    mTokenResponse = nullptr;

    const android_app *app = NativeEngine::GetInstance()->GetAndroidApp();
    const IntegrityErrorCode errorCode = IntegrityManager_init(app->activity->vm,
                                                               app->activity->javaGameActivity);
    if (errorCode == INTEGRITY_NO_ERROR) {
        mInitialized = true;
    } else {
        mInitialized = false;
        ALOGE("Play Integrity initialization failed with error: %d", errorCode);
        ALOGE("Fatal Error: Play Integrity is unavailable and cannot be used.");
    }

將下列程式碼加入 ClientManager::~ClientManager() 函式中:

    if (mInitialized) {
        IntegrityManager_destroy();
        mInitialized = false;
    }

要求完整性權杖

透過 Play Integrity API 要求完整性權杖為非同步作業。您需要建立權杖要求物件、為其指派 Nonce 值,再提出權杖要求。為此,請在空白的 ClientManager::StartCommandIntegrity() 函式中加入下列程式碼:

    // Only one request can be in-flight at a time
    if (mStatus != CLIENT_MANAGER_REQUEST_TOKEN) {
        mResult = SERVER_OPERATION_PENDING;
        // Request a fresh random
        RequestRandom();
        if (mValidRandom) {
            GenerateNonce();
            IntegrityTokenRequest_create(&mTokenRequest);
            IntegrityTokenRequest_setNonce(mTokenRequest, mCurrentNonce.c_str());

            const IntegrityErrorCode errorCode =
                    IntegrityManager_requestIntegrityToken(mTokenRequest, &mTokenResponse);
            if (errorCode != INTEGRITY_NO_ERROR) {
                // Log the error, in a real application, for potentially
                // transient errors such as network connectivity, you should
                // add retry with an exponential backoff
                ALOGE("Play Integrity returned error: %d", errorCode);
                CleanupRequest();
                mStatus = CLIENT_MANAGER_IDLE;
            } else {
                mStatus = CLIENT_MANAGER_REQUEST_TOKEN;
            }
        }
    }

權杖要求是非同步運作,因此需要檢查要求的完成狀態。ClientManager 類別提供 Update() 函式,可供在應用程式更新迴圈中呼叫。請將下列程式碼加入 ClientManager::Update() 函式中,檢查權杖要求的狀態,並在完成後處理結果:

    if (mStatus == CLIENT_MANAGER_REQUEST_TOKEN) {
        IntegrityResponseStatus responseStatus = INTEGRITY_RESPONSE_UNKNOWN;
        const IntegrityErrorCode errorCode =
                IntegrityTokenResponse_getStatus(mTokenResponse, &responseStatus);
        if (errorCode != INTEGRITY_NO_ERROR) {
            // Log the error, in a real application, for potentially
            // transient errors such as network connectivity, you should
            // add retry with an exponential backoff
            ALOGE("Play Integrity returned error: %d", errorCode);
            CleanupRequest();
            mStatus = CLIENT_MANAGER_IDLE;
        } else if (responseStatus == INTEGRITY_RESPONSE_COMPLETED) {
            std::string tokenString = IntegrityTokenResponse_getToken(mTokenResponse);
            SendCommandToServer(tokenString);
            CleanupRequest();
            mStatus = CLIENT_MANAGER_RESPONSE_AVAILABLE;
        }
    }

清理要求物件

用完權杖要求和回應物件後,您需要告知 Play Integrity API,讓 API 進行刪除並回收使用的資源。請將下列程式碼加入 ClientManager::CleanupRequest() 函式中:

    if (mTokenResponse != nullptr) {
        IntegrityTokenResponse_destroy(mTokenResponse);
        mTokenResponse = nullptr;
    }
    if (mTokenRequest != nullptr) {
        IntegrityTokenRequest_destroy(mTokenRequest);
        mTokenRequest = nullptr;
    }

從「Build」選單中選擇「Make Project」,並確認專案是否建構成功。

10. 將權杖傳送至伺服器 (C++)

您現在可以加入程式碼,將包含完整性權杖的指令傳送至伺服器。此外,您也要加入用來處理結果的程式碼。

將下列程式碼加入 ClientManager::SendCommandToServer() 函式中:

// Note that for simplicity, we are doing HTTP operations as
// synchronous blocking instead of managing them from a
// separate network thread
HTTPClient client;
std::string errorString;

// Manually construct the json payload for ServerCommand
std::string payloadString = COMMAND_JSON_PREFIX;
payloadString += TEST_COMMAND;
payloadString += COMMAND_JSON_TOKEN;
payloadString += token;
payloadString += COMMAND_JSON_SUFFIX;

auto result = client.Post(PERFORM_COMMAND_URL, payloadString, &errorString);
if (!result) {
   ALOGE("SendCommandToServer Curl reported error: %s", errorString.c_str());
   mResult = SERVER_OPERATION_NETWORK_ERROR;
} else {
   ALOGI("SendCommandToServer result: %s", (*result).c_str())
   // Preset to success, ParseResult will set a failure result if the parsing
   // errors.
   mResult = SERVER_OPERATION_SUCCESS;
   ParseResult(*result);
}

將下列程式碼加入 ClientManager::ParseResult() 函式中:

    bool validJson = false;
    JsonLookup jsonLookup;
    if (jsonLookup.ParseJson(resultJson)) {
        // Look for all of our needed fields in the returned json
        auto commandSuccess = jsonLookup.GetBoolValueForKey(COMMANDSUCCESS_KEY);
        if (commandSuccess) {
            auto diagnosticString = jsonLookup.GetStringValueForKey(DIAGNOSTICMESSAGE_KEY);
            if (diagnosticString) {
                auto expressString = jsonLookup.GetStringValueForKey(EXPRESSTOKEN_KEY);
                if (expressString) {
                    if (*commandSuccess) {
                        // Express token only valid if the server reports the command succeeded
                        mValidExpressToken = true;
                    } else {
                        mValidExpressToken = false;
                        mResult = SERVER_OPERATION_REJECTED_VERDICT;
                    }
                    mCurrentSummary = *diagnosticString;
                    mCurrentExpressToken = *expressString;
                    validJson = true;
                }
            }
        }
    }
    if (!validJson) {
        mResult = SERVER_OPERATION_INVALID_RESULT;
    }

您現在可以產生已簽署的應用程式套件,並上傳至 Play 管理中心測試應用程式。

11. 建構及上傳 (C++)

為應用程式建立及設定 KeyStore

Android 要求所有應用程式都必須完成數位憑證簽署,才能安裝到裝置上或進行更新。

在本程式碼研究室中,我們會為應用程式建立「KeyStore」。如果要為現有遊戲發布更新,請重複使用應用程式先前版本發布時使用的 KeyStore。

建立 KeyStore 並建構發布應用程式套件

按照利用 Android Studio 產生 KeyStore的步驟建立 KeyStore,然後使用該 KeyStore 產生已簽署的遊戲發布子版本。在 Android Studio 中,從「Build」選單中選取「Generate Signed Bundle/APK」,啟動建構程序。當系統提示您選取「Android App Bundle」或「APK」時,請選擇「App Bundle」選項。處理完成後,會產生適合上傳至 Google Play 管理中心的 .aab 檔案。

上傳至 Play 管理中心

建立應用程式套件檔案後,請將檔案上傳至 Play 管理中心。建議您使用內部測試群組,方便快速存取版本。

執行測試版本

現在請前往 Play 商店下載及執行測試版本。在這個階段,函式中會有一個預留位置成功代碼,可將完整性權杖傳送至伺服器,因此啟動完整性檢查應會成功,並產生以下畫面:

ef5f55d73f808791.png

恭喜,您已將 Play Integrity 整合到 C++ 應用程式中!繼續查看其他用戶端範例,或跳至本程式碼研究室的結尾。

12. 建構及執行用戶端 (Unity)

程式碼研究室 Unity 專案是使用 Unity 2020 LTS (2020.3.31f1) 建立而成,但應與較新版本的 Unity 相容。適用於 Unity 的 Play Integrity 外掛程式與 Unity 2018 LTS 以上版本相容。

專案設定

請執行下列步驟:

  1. 在 Unity Hub 或 Unity 編輯器中,開啟位於 add-play-integrity-codelab/unity/start 的 Unity 專案。
  2. 專案載入後,從 Unity 的「File」選單中選取「Build Settings...」
  3. 在「Build Settings」視窗中,將平台變更為「Android」
  4. 在「Build Settings」視窗中,按一下「Player Settings...」按鈕。
  5. 在「Project Settings」視窗中選取「Player」類別,然後找到「Settings for Android」部分。展開「Other Settings」清單。

b994587b808c7be4.png

  1. 在「Identification」下方,找出「Package Name」項目。

d036e5be73096083.png

  1. 將套件名稱變更為部署伺服器時所選的 ID。
  2. 關閉「Project Settings」視窗。
  3. 從 Unity 的「File」選單中選取「Save Project」

更新伺服器網址

您需要更新專案,使其指向部署伺服器的網址位置。如要這樣做,請按照下列步驟操作:

  1. 在 IDE 或文字編輯器中,開啟 Scripts 資料夾中的 PlayIntegrityController.cs 檔案。
  2. URL_GETRANDOMURL_PERFORMCOMMAND 變數的值變更為指向伺服器。
  3. 儲存檔案。

結果應該類似於以下內容:

    private readonly string URL_GETRANDOM = "https://your-play-integrity-server.uc.r.appspot.com/getRandom";
    private readonly string URL_PERFORMCOMMAND = "https://your-play-integrity-server.uc.r.appspot.com/performCommand";

具體網址會因專案名稱和用於部署伺服器的 Google Cloud 區域而異。

測試伺服器功能

您可以在 Unity 編輯器中執行專案,測試伺服器功能。請執行下列步驟:

  1. 開啟位於 Scenes 資料夾中的 SampleScene 情境檔案。
  2. 按一下編輯器中的「Play」按鈕。
  3. 按一下「Game」畫面中的「Request Random」按鈕。

經過短暫延遲後,畫面上應會顯示隨機值,類似於以下所示:

f22c56cdd2e56050.png

13. 將 Play Integrity 外掛程式加入專案 (Unity)

下載外掛程式

在網路瀏覽器中,開啟 Play Unity 外掛程式 GitHub 存放區的版本頁面。請使用最新版本的外掛程式。下載 Play Integrity 適用的 com.google.play.integrity-<version>.unitypackage 檔案 (位於「Assets」清單中)

安裝外掛程式

在 Unity 編輯器的選單列中,依序選取「Assets」->「Import Package」->「Custom Package...」,然後開啟您下載的 .unitypackage 檔案。在畫面顯示「Import Unity Package」視窗後,按一下「Import」按鈕。

這個外掛程式包含 External Dependency Manager for Unity (EDM4U)。EDM4U 會針對使用 Play Integrity 所需的 Java 元件,實作依附元件自動解析功能。系統提示您啟用依附元件自動解析功能時,請按一下「Enable」按鈕。

5bf0be9139fab036.png

我們強烈建議您使用自動解析功能。依附元件問題可能會導致無法建構或執行專案。

14. 提出完整性要求 (Unity)

建立完整性要求

如要建立完整性要求,請執行下列步驟:

  1. 在 IDE 或文字編輯器中,開啟 Scripts 資料夾中的 PlayIntegrityController.cs 檔案。
  2. 在檔案頂端的 using 陳述式區塊中,加入下列指令行:
using Google.Play.Integrity;
  1. 找出 RunIntegrityCommand() 函式,將 yield return null; 陳述式替換為以下程式碼:
        // Call our server to retrieve a random number.
        yield return GetRandomRequest();
        if (!string.IsNullOrEmpty(_randomString))
        {
            // Create an instance of an integrity manager.
            var integrityManager = new IntegrityManager();

            // Request the integrity token by providing a nonce.
            var tokenRequest = new IntegrityTokenRequest(GenerateNonceString(_randomString,
                TEST_COMMAND));
            var requestIntegrityTokenOperation =
                integrityManager.RequestIntegrityToken(tokenRequest);

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

            // Check the resulting error code.
            if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
            {
                // Log the error, in a real application, for potentially
                // transient errors such as network connectivity, you should
                // add retry with an exponential backoff
                Debug.Log($@"IntegrityAsyncOperation failed with error: 
                    {requestIntegrityTokenOperation.Error.ToString()}");
                yield break;
            }

            // Get the response.
            var tokenResponse = requestIntegrityTokenOperation.GetResult();

            // Send the command to our server with a POST request, including the
            // token, which will be decrypted and verified on the server.
            yield return PostServerCommand(tokenResponse.Token);
        }

將指令傳送至伺服器

繼續編輯 PlayIntegrityController.cs 檔案,找出 PostServerCommand() 函式,將 yield return null; 陳述式替換為以下程式碼:

        // Start a HTTP POST request to the performCommand URL, sending it the
        // command and integrity token data provided by Play Integrity.
        var serverCommand = new ServerCommand(TEST_COMMAND, tokenResponse);
        var commandRequest = new UnityWebRequest(URL_PERFORMCOMMAND, "POST");
        string commandJson = JsonUtility.ToJson(serverCommand);
        byte[] jsonBuffer = Encoding.UTF8.GetBytes(commandJson);
        commandRequest.uploadHandler = new UploadHandlerRaw(jsonBuffer);
        commandRequest.downloadHandler = new DownloadHandlerBuffer();
        commandRequest.SetRequestHeader(CONTENT_TYPE, JSON_CONTENT);
        yield return commandRequest.SendWebRequest();

        if (commandRequest.result == UnityWebRequest.Result.Success)
        {
            // Parse the command result Json
            var commandResult = JsonUtility.FromJson<CommandResult>(
                commandRequest.downloadHandler.text);
            if (commandResult != null)
            {
                resultLabel.text = commandResult.diagnosticMessage;
                _expressToken = commandResult.expressToken;
                if (commandResult.commandSuccess)
                {
                    resultLabel.color = Color.green;
                    expressButton.SetActive(true);
                }
                else
                {
                    resultLabel.color = Color.black;
                    expressButton.SetActive(false);
                }
            }
            else
            {
                Debug.Log("Invalid CommandResult json");
            }
        }
        else
        {
            Debug.Log($"Web request error on processToken: {commandRequest.error}");
        }

儲存檔案。

15. 建構及上傳 (Unity)

使用 Unity 編輯器 Android KeyStore Manager,設定要簽署並上傳至 Play 管理中心的版本。

設定簽署資訊後,請執行下列步驟:

  1. 從 Unity 的「File」選單中,依序選取「Build」->「Build Settings...」
  2. 確認 SampleScene 已加入「Scenes in Build」清單。
  3. 確認已勾選「Build App Bundle (Google Play)」方塊。
  4. 按一下「Build」按鈕,並為匯出檔案命名。

建立應用程式套件檔案後,請將檔案上傳至 Play 管理中心。建議您使用內部測試群組,方便快速存取版本。

您現在可以下載並安裝版本,執行完整性檢查。結果應會類似於以下內容:

fa83cdb1a700ca0b.png

恭喜,您已將 Play Integrity 整合到 Unity 引擎專案中!繼續查看其他用戶端範例,或跳至本程式碼研究室的結尾。

16. 建構並執行專案 (Kotlin)

執行 Android Studio。在「Welcome to Android Studio」視窗中按一下「Open」按鈕,然後開啟位於 add-play-integrity-codelab/kotlin/start 的 Android Studio 專案。

更新應用程式 ID

將版本上傳至 Google Play 前,需要將應用程式 ID 從預設值變更為專屬名稱。請執行下列步驟:

  1. 在 Android Studio 的「Project」窗格中,開啟 PlayIntegrityCodelab.app 模組的 build.gradle 檔案。
  2. 找出 applicationId 陳述式。
  3. com.example.google.codelab.playintegritykotlin 變更為部署伺服器時所選的 ID,並儲存檔案。
  4. 檔案頂端會顯示橫幅,指出 Gradle 檔案已變更。按一下「Sync Now」,重新載入及重新同步處理檔案。

更新伺服器網址

您需要更新專案,使其指向部署伺服器的網址位置。如要這樣做,請按照下列步驟操作:

  1. 在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/java/com.example.google.codelab.playintegritykotlin/integrity 底下的 IntegrityServer 檔案。
  2. 將網址從 ‘https://your.play-integrity.server.com' 變更為伺服器的基準網址,然後儲存檔案。

結果應該類似於以下內容:

    private val SERVER_URL: String = "https://your-play-integrity-server.uc.r.appspot.com"

具體網址會因專案名稱和用於部署伺服器的 Google Cloud 區域而異。

建構及執行

連結專為開發目的設定的 Android 裝置。在 Android Studio 中,建立專案並在已連結的裝置上執行。應用程式應如下所示:

d77ca71dc209452f.png

啟動時,應用程式會呼叫伺服器上的 getRandom 端點,並顯示結果。如果發生錯誤 (例如網址不正確或伺服器無法運作),系統會顯示錯誤對話方塊。您可以選取「Request Random」按鈕,從伺服器擷取新的隨機號碼。「Call server with integrity check」按鈕目前還沒有任何作用。您將在後續章節中新增此功能。

17. 將 Play Integrity 加入專案 (Kotlin)

如要為專案加入 Play Integrity 程式庫和支援的依附元件,請執行下列步驟:

  1. 在 Android Studio 的「Project」窗格中,開啟 start/app 底下的 build.gradle 檔案。
  2. 找到檔案底部的 dependencies 區塊。
  3. dependencies 區塊底部加入下列指令行:
    implementation "com.google.android.play:integrity:1.0.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1"
  1. 儲存檔案。
  2. 檔案頂端會顯示橫幅,指出 Gradle 檔案已變更。按一下「Sync Now」,重新載入及重新同步處理檔案。

Kotlin 範例使用協同程式。kotlinx-coroutines-play-services 程式庫新增了擴充功能,可從 Kotlin 協同程式內使用 Play Integrity 非同步 Task 物件。

18. 提出完整性要求 (Kotlin)

在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/java/com.example.google.codelab.playintegritykotlin/integrity 底下的 IntegrityServer 檔案。檔案底部有一個空白的 integrityCommand 函式。當使用者按下「Call server with Integrity check」按鈕時,UI 會呼叫此函式。

您需要在 integrityCommand 函式中加入程式碼,執行下列作業:

  1. 從伺服器中擷取一組新的隨機號碼,用來在建構 Nonce 時與完整性檢查建立關聯
  2. 呼叫 Play Integrity API 來發出完整性要求,並接收包含結果的完整性權杖
  3. 使用 HTTP POST 要求將指令和完整性權杖傳送至伺服器
  4. 處理並顯示結果

將下列程式碼加入空白的 integrityCommand 函式中:

        // Set our state to working to trigger a switch to the waiting UI
        _serverState.emit(ServerState(
            ServerStatus.SERVER_STATUS_WORKING))
        // Request a fresh random from the server as part
        // of the nonce we will generate for the request
        var integrityRandom = IntegrityRandom("", 0U)
        try {
            val returnedRandom = httpClient.get<IntegrityRandom>(
                SERVER_URL + "/getRandom")
            integrityRandom = returnedRandom
        } catch (t: Throwable) {
            Log.d(TAG, "getRandom exception " + t.message)
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_UNREACHABLE,
                IntegrityRandom("", 0U)))
        }

        // If we have a random, we are ready to request an integrity token
        if (!integrityRandom.random.isNullOrEmpty()) {
            val nonceString = GenerateNonce.GenerateNonceString(TEST_COMMAND,
                integrityRandom.random)
            // Create an instance of an IntegrityManager
            val integrityManager = IntegrityManagerFactory.create(context)

            // Use the nonce to configure a request for an integrity token
            try {
                val integrityTokenResponse: Task<IntegrityTokenResponse> =
                    integrityManager.requestIntegrityToken(
                        IntegrityTokenRequest.builder()
                            .setNonce(nonceString)
                            .build()
                    )
                // Wait for the integrity token to be generated
                integrityTokenResponse.await()
                if (integrityTokenResponse.isSuccessful && integrityTokenResponse.result != null) {
                    // Post the received token to our server
                    postCommand(integrityTokenResponse.result!!.token(), integrityRandom)
                } else {
                    Log.d(TAG, "requestIntegrityToken failed: " +
                            integrityTokenResponse.result.toString())
                    _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_FAILED_TO_GET_TOKEN))
                }
            } catch (t: Throwable) {
                Log.d(TAG, "requestIntegrityToken exception " + t.message)
                _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_FAILED_TO_GET_TOKEN))
            }
        }

將指令 POST 到伺服器的程式碼已劃分為獨立的 postCommand 函式。請將下列程式碼加入空白的 postCommand 函式中:

        try {
            val commandResult = httpClient.post<CommandResult>(
                SERVER_URL + "/performCommand") {
                contentType(ContentType.Application.Json)
                body = ServerCommand(TEST_COMMAND, tokenString)
            }
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_REACHABLE,
                integrityRandom,
                commandResult.diagnosticMessage,
                commandResult.commandSuccess,
                commandResult.expressToken))
        } catch (t: Throwable) {
            Log.d(TAG, "performCommand exception " + t.message)
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_UNREACHABLE))
        }

修正所有缺少的匯入內容,並儲存檔案。

19. 顯示結果 (Kotlin)

根據判定結果摘要更新 UI

UI 目前會為完整性判定結果摘要顯示預留位置文字。如要將預留位置替換為實際摘要,請執行下列步驟:

  1. 在 Android Studio 的「Project」窗格中,開啟 start/app/src/main/java/com.example.google.codelab.playintegritykotlin/ui/main 底下的 MainView.kt 檔案。
  2. 前往 MainUI 函式的結尾,找到 text = "None", 陳述式並替換為以下程式碼:
                        text = state.serverState.serverVerdict,
  1. 修正所有缺少的匯入內容,並儲存檔案。

20. 建構及上傳 (Kotlin)

為應用程式建立及設定 KeyStore

Android 要求所有應用程式都必須完成數位憑證簽署,才能安裝到裝置上或進行更新。

在本程式碼研究室中,我們會為應用程式建立「KeyStore」。如果要為現有遊戲發布更新,請重複使用應用程式先前版本發布時使用的 KeyStore。

建立 KeyStore 並建構發布應用程式套件

按照利用 Android Studio 產生 KeyStore的步驟建立 KeyStore,然後使用該 KeyStore 產生已簽署的遊戲發布子版本。在 Android Studio 中,從「Build」選單中選取「Generate Signed Bundle/APK」,啟動建構程序。當系統提示您選取「Android App Bundle」或「APK」時,請選擇「App Bundle」選項。處理完成後,會產生適合上傳至 Google Play 管理中心的 .aab 檔案。

上傳至 Play 管理中心

建立應用程式套件檔案後,請將檔案上傳至 Play 管理中心。建議您使用內部測試群組,方便快速存取版本。

執行測試版本

現在請前往 Play 商店下載及執行測試版本。選取「Call server with Integrity check」按鈕應會執行成功,並產生以下畫面:

3291795e192396c9.png

21. 恭喜

恭喜,您已成功將 Play Integrity 加入 Android 應用程式!

其他資訊