提出傳統 API 要求

如果您只打算提出大多數開發人員適用的標準 API 要求,可以直接跳到「完整性判定結果」。本頁將說明如何提出傳統 API 要求來取得完整性判定結果,您只要使用 Android 4.4 (API 級別 19) 以上版本,即可執行這項操作。

考量重點

比較標準和傳統要求

您可以根據應用程式的安全性和反濫用需求,提出標準要求、傳統要求,或兩者並用。標準要求適用於所有應用程式和遊戲,可用來檢查任一操作或伺服器呼叫是否正規,同時將某些能抵禦重播性和竊取作業的功能委派給 Google Play。傳統要求耗用的資源則比較多,且您須負責正確實作這類要求,以防有心人士竊取資料和發動特定類型的攻擊。因此,提出傳統要求的頻率應低於標準要求,比如可以將前者做為不定期的一次性檢查,用於確認高度敏感/重要操作是否正規。

下表重點列出兩種要求間的主要差異:

標準 API 要求 傳統 API 要求
必要條件
Android SDK 最低需求版本 Android 5.0 (API 級別 21) 以上版本 Android 4.4 (API 級別 19) 以上版本
Google Play 相關規定 Google Play 商店和 Google Play 服務 Google Play 商店和 Google Play 服務
詳細整合資料
API 需要暖機 ✔️ (幾秒)
要求的一般延遲時間 幾百毫秒 幾秒
可能的要求頻率 頻繁 (隨選即可檢查任何操作或要求) 不頻繁 (對最重要的動作或最敏感的要求執行一次性檢查)
逾時 多數暖機需要不到 10 秒的時間,但會執行伺服器呼叫,因此建議設定較長的逾時時間 (例如 1 分鐘)。在用戶端判定要求 多數要求需要不到 10 秒的時間,但會執行伺服器呼叫,因此建議設定較長的逾時時間 (例如 1 分鐘)
完整性判定結果權杖
包含裝置、應用程式和帳戶詳細資料 ✔️ ✔️
權杖快取 受 Google Play 保護的裝置端快取 不建議採用
透過 Google Play 伺服器解密及驗證權杖 ✔️ ✔️
解密伺服器對伺服器要求的一般延遲時間 長達 10 毫秒,並提供 99.9% 的可用性 長達 10 毫秒,並提供 99.9% 的可用性
在本機安全伺服器環境中解密及驗證權杖 ✔️
在用戶端解密及驗證權杖
完整性判定結果的即時性 由 Google Play 部分自動快取及重新整理 根據各項要求重新計算所有判定結果
限制
每個應用程式的每日要求數 預設為 10,000 (可以要求調高) 預設為 10,000 (可以要求調高)
每個應用程式執行個體的每分鐘要求數 暖機:每分鐘 5 個
完整性權杖:無公開限制*
完整性權杖:每分鐘 5 個
安全防護
如何因應竄改和類似攻擊 使用 requestHash 欄位 根據要求資料使用 nonce 欄位和內容繫結
如何因應重播和類似攻擊 由 Google Play 自動採取因應措施 使用 nonce 欄位和伺服器端邏輯

* 所有要求 (包括無公開限制者) 皆適用非公開的防禦上限值

避免頻繁提出傳統要求

產生完整性權杖時會耗用時間、數據用量和電量,而且每個應用程式每天可提出的傳統要求有數量上限。因此,建議您僅於要對標準要求提供額外保證時,再藉由提出傳統要求檢查最重要或最敏感的操作是否正規,而不要為執行頻率高或不重要的操作提出這類要求。另外,請避免在每次應用程式進入前景時,或在背景中每隔幾分鐘就提出傳統要求,也不要同時透過大量裝置發出呼叫。如果應用程式提出傳統要求的次數過多,便會受到節流限制,這可避免錯誤導入結果對使用者造成影響。

避免快取判定結果

快取判定結果會提高資料竊取和重播等攻擊的風險,在這類攻擊中,有心人士會從不受信任的環境重複使用好的判定結果。如果您考慮提出傳統要求,並快取這類要求留待日後使用,那麼我們會建議您改為執行隨選即用的標準要求。標準要求雖然也會在裝置上執行部分快取,但此時 Google Play 便會利用額外的防護技術,降低重播攻擊和資料竊取的風險。

使用 Nonce 欄位為傳統要求提供防護

Play Integrity API 提供名為 nonce 的欄位,可進一步保護應用程式免受特定攻擊侵害,例如重播和竄改攻擊等。Play Integrity API 會在已簽署的完整性回應內,傳回您在這個欄位中設定的值。請務必按照如何產生 Nonce 指南操作,藉此保護應用程式免受攻擊。

以指數輪詢方式重試傳統要求

網路連線不穩定或超載裝置等環境條件可能會造成裝置完整性檢查失敗,這可能會導致系統並未針對可信任的裝置產生標籤。如要緩解這些情況,請加入以指數輪詢策略進行重試的選項。

總覽

顯示高階 Play Integrity API 設計的流程圖

如要透過完整性檢查功能保護使用者在應用程式中執行的重要操作,請完成下列步驟:

  1. 應用程式伺服器端的後端會產生不重複的值,並傳送至用戶端邏輯。在其餘步驟中,我們會將這個邏輯稱為您的「應用程式」。
  2. 應用程式會根據不重複的值和重要操作的內容建立 nonce,然後呼叫 Play Integrity API 並傳入 nonce
  3. 應用程式會從 Play Integrity API 收到已簽署且加密的判定結果。
  4. 應用程式會將已簽署的加密判定結果傳遞至應用程式的後端。
  5. 應用程式的後端會將判定結果傳送至 Google Play 伺服器,然後 Google Play 伺服器會解密並驗證判定結果,並將結果傳回應用程式的後端。
  6. 應用程式的後端會根據權杖酬載中的信號決定要如何繼續操作。
  7. 應用程式的後端會將決定結果傳送至您的應用程式。

產生 Nonce

使用 Play Integrity API 保護應用程式中的操作時,可以使用 nonce 欄位來因應特定類型的攻擊,例如中間人 (PITM) 竄改攻擊和重播攻擊。Play Integrity API 會在已簽署的完整性回應內,傳回您在這個欄位中設定的值。

nonce 欄位中設定的值必須採用正確的格式:

  • String
  • 具有網址安全性
  • 編碼為 Base64 且沒有換行
  • 至少 16 個字元
  • 最多 500 個字元

以下列舉在 Play Integrity API 中使用 nonce 欄位的幾種常見方式。如要讓 nonce 發揮最好的保護效果,可以搭配運用下列方法。

加入可防範竄改的要求雜湊

為防止要求內容遭到竄改,您可以在傳統 API 要求中使用 nonce 參數,方法類似於標準 API 要求中的 requestHash 參數。

當您要求系統提供完整性判定結果時,請完成以下操作:

  1. 從正在發生的使用者操作或伺服器要求中,計算所有重要要求參數的摘要 (例如穩定要求序列化的 SHA256)。
  2. 使用 setNoncenonce 欄位設為計算的摘要值。

當您收到完整性判定結果時,請完成以下操作:

  1. 將完整性權杖解碼並執行驗證,然後從 nonce 欄位取得摘要。
  2. 利用與應用程式相同的方式,計算要求摘要 (例如,穩定要求序列化的 SHA256)。
  3. 比較應用程式端和伺服器端摘要。如果兩者不相符,則代表要求可信度低。

加入可抵禦重播攻擊的不重複值

為了避免惡意使用者重複使用 Play Integrity API 之前的回應,您可以使用 nonce 欄位來明確識別每則訊息。

當您要求系統提供完整性判定結果時,請完成以下操作:

  1. 以惡意使用者無法預測的方式,取得一個全域唯一的值。例如,在伺服器端產生的加密編譯隨機號碼就屬於這類值,這也可以是工作階段或交易 ID 這類現有 ID。雖然在裝置上產生隨機號碼比較簡單,但安全性會比較低。建議您建立 128 位元以上的值。
  2. 呼叫 setNonce()nonce 欄位設為從步驟 1 取得的不重複值。

當您收到完整性判定結果時,請完成以下操作:

  1. 將完整性權杖解碼並執行驗證,然後從 nonce 欄位取得不重複值。
  2. 如果從步驟 1 取得的值是在伺服器上產生,請確認接收的不重複值屬於上述生成值,且為首次使用 (您的伺服器需要保留生成值的記錄一段適當時間)。如果已使用收到的不重複值,或是記錄中沒有這個值,請拒絕要求。
  3. 相反地,如果不重複值是在裝置上產生,請確認接收的值是否為首次使用 (您的伺服器需要保留已顯示值的記錄一段適當時間)。如果已使用收到的不重複值,請拒絕要求。

整合兩種保護機制,防止竄改和重播攻擊 (建議做法)

您可以使用 nonce 欄位來同時防範竄改和重播攻擊。方法是按照上文說明產生不重複值,然後附加到要求中。接著請計算要求雜湊,務必將不重複值加到雜湊中。結合運用這兩種方法的實作方式如下:

當您要求系統提供完整性判定結果時,請完成以下操作:

  1. 首先,使用者會發起重要操作。
  2. 按照「加入可抵禦重播攻擊的不重複值」一節說明,取得此操作的不重複值。
  3. 備妥要保護的訊息,加入從步驟 2 取得的不重複值。
  4. 應用程式會計算要保護的訊息摘要,如「加入可防範竄改的要求雜湊」一節所述。由於訊息包含不重複值,因此該值是雜湊的一部分。
  5. 使用 setNonce()nonce 欄位設為上一步中計算的摘要。

當您收到完整性判定結果時,請完成以下操作:

  1. 從要求中取得不重複值
  2. 將完整性權杖解碼並執行驗證,然後從 nonce 欄位取得摘要。
  3. 按照「加入可防範竄改的要求雜湊」一節說明,在伺服器端重新計算摘要,並確認該摘要與透過完整性權杖取得的摘要相符。
  4. 按照「加入可抵禦重播攻擊的不重複值」一節所述,檢查不重複值是否有效。

以下是這些步驟在伺服器端 nonce 的流程圖:

此流程圖顯示如何防範竄改和重播攻擊

要求系統提供完整性判定結果

產生 nonce 後,您可以要求 Google Play 提供完整性判定結果。如要這樣做,請完成下列步驟:

  1. 依照以下範例建立 IntegrityManager
  2. 建構 IntegrityTokenRequest,透過關聯建構工具中的 setNonce() 方法提供 nonce。只在 Google Play 和 SDK 以外管道發行的應用程式也必須透過 setCloudProjectNumber() 方法指定 Google Cloud 專案編號。Google Play 上的應用程式會連結至 Play 管理中心的 Cloud 專案,您不必在要求中設定 Cloud 專案編號。
  3. 使用管理工具呼叫 requestIntegrityToken(),提供 IntegrityTokenRequest

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
             .setNonce(nonce)
             .build())

Java

import com.google.android.gms.tasks.Task; ...

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
        .requestIntegrityToken(
            IntegrityTokenRequest.builder().setNonce(nonce).build());

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

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

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

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

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

原生

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer and call
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
        IntegrityManager_requestIntegrityToken(request, &response);

/// ...
/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.
/// Note, the polling shouldn't block the thread where the IntegrityManager
/// is running.

IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code =
        IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR
    && response_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrity_token = IntegrityTokenResponse_getToken(response);
    SendTokenToServer(integrity_token);
}
/// ...
/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

解密並驗證完整性判定結果

當您要求系統提供完整性判定結果時,Play Integrity API 會提供已簽署的回應權杖。您在要求中納入的 nonce 會成為回應權杖的一部分。

權杖格式

該權杖是巢狀 JSON Web Token (JWT),即為 JSON Web Signature (JWS)JSON Web Encryption (JWE)。JWE 和 JWS 元件是以精簡序列化表示。

各種 JWT 實作皆完整支援加密/簽署演算法:

  • JWE 將 A256KW 用於 alg,將 A256GCM 用於 enc

  • JWS 使用的是 ES256。

在 Google 伺服器上解密並驗證 (建議)

Play Integrity API 可讓您在 Google 伺服器上解密並驗證完整性判定結果,可加强應用程式的安全性。若要這樣做,請完成下列步驟:

  1. 在連結至應用程式的 Google Cloud 專案中建立服務帳戶。在建立帳戶的流程中,您必須將您的服務帳戶授予服務帳戶使用者服務使用情形消費者角色。
  2. 在應用程式伺服器上,使用 playintegrity 範圍從服務帳戶的憑證中擷取存取權杖,然後提出下列要求:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. 讀取 JSON 回應。

在本機解密並驗證

如果您選擇管理及下載回應加密金鑰,可以在自己的安全伺服器環境中解密及驗證傳回的權杖。您可以使用 IntegrityTokenResponse#token() 方法取得傳回的權杖。

以下範例說明如何解碼 AES 金鑰和 DER 編碼的公開 EC 金鑰,以便在應用程式後端為 Play 管理中心乃至特定語言 (在這裡是 Java 程式設計語言) 的金鑰進行簽章驗證。請注意,金鑰會使用預設標記進行 base64 編碼。

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java


// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

接下來,先使用這些金鑰解密完整性權杖 (JWE 部分),然後驗證並擷取巢狀 JWS 的部分。

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
    (JsonWebEncryption)JsonWebStructure
        .fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

產生的酬載是包含完整性判定結果的純文字權杖。