发出经典 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 服务器解密并验证令牌 ✔️ ✔️
典型解密服务器到服务器请求延迟 几十毫秒,可用性为 99.9% 几十毫秒,可用性为 99.9%
在安全的服务器环境中本地解密并验证令牌 ✔️
客户端解密并验证令牌
完整性判决时效性 由 Google Play 进行部分自动缓存和刷新 每次请求时都会重新计算所有判决
限制
每个应用每天的请求数 默认 10,000 (可申请增加) 默认 10,000 (可申请增加)
每个应用实例每分钟的请求数 预热:每分钟 5 次
完整性令牌:无公开限制*
完整性令牌:每分钟 5 个
保护
缓解篡改和类似攻击 使用 requestHash 字段 nonce 字段与基于请求数据的内容绑定结合使用
缓解重放和类似攻击 由 Google Play 自动缓解 nonce 字段与服务器端逻辑结合使用

* 所有请求 (包括没有公开限制的请求) 在高数值下均受非公开防御限制的约束

不频繁地发出经典请求

生成完整性令牌会消耗时间、数据和电池,并且每个应用每天可发出的经典请求数量上限是固定的。因此,只有当您希望为标准请求增加额外保障时,才应发出经典请求来检查最高价值或最敏感操作是否真实。您不应为高频或低价值操作发出经典请求。不要在应用每次进入前台或后台每隔几分钟就发出经典请求,并避免大量设备同时进行调用。如果应用发出的经典请求调用过多,可能会受到限制,以保护用户免受不正确实现的影响。

避免缓存判决

缓存判决会增加数据泄露和重放等攻击的风险,在这些攻击中,来自不可信环境的良好判决会被重复使用。如果您正在考虑发出经典请求,然后对其进行缓存以供日后使用,建议改为按需执行标准请求。标准请求涉及设备上的一些缓存,但 Google Play 使用额外的保护技术来缓解重放攻击和数据泄露的风险。

使用 nonce 字段保护经典请求

Play Integrity API 提供了一个名为 nonce 的字段,可用于进一步保护您的应用免受某些攻击,例如中间人 (PITM) 篡改攻击和重放攻击。Play Integrity API 会在签名的完整性响应中返回您在此字段中设置的值。请仔细遵循有关如何生成 nonce 的指南,以保护您的应用免受攻击。

采用指数退避策略重试经典请求

环境条件 (例如不稳定的互联网连接或过载的设备) 可能导致设备完整性检查失败。这可能导致对于原本可信的设备未生成任何标签。为了缓解这些情况,请加入带有指数退避策略的重试选项。

概览

Sequence diagram that shows the high-level design of the 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 字段中设置的值必须格式正确

  • 字符串
  • 网址安全
  • 编码为 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 或事务 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 的这些步骤

Sequence diagram that shows how to protect against both tampering and replay
attacks

请求完整性判决

生成 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();
}

虚幻引擎

// .h
void MyClass::OnRequestIntegrityTokenCompleted(
  EIntegrityErrorCode ErrorCode,
  UIntegrityTokenResponse* Response)
{
  // Check the resulting error code.
  if (ErrorCode == EIntegrityErrorCode::Integrity_NO_ERROR)
  {
    // Get the token.
    FString Token = Response->Token;
  }
}

// .cpp
void MyClass::RequestIntegrityToken()
{
  // Receive the nonce from the secure server.
  FString Nonce = ...

  // Create the Integrity Token Request.
  FIntegrityTokenRequest Request = { Nonce };

  // Create a delegate to bind the callback function.
  FIntegrityOperationCompletedDelegate Delegate;

  // Bind the completion handler (OnRequestIntegrityTokenCompleted) to the delegate.
  Delegate.BindDynamic(this, &MyClass::OnRequestIntegrityTokenCompleted);

  // Initiate the integrity token request, passing the delegate to handle the result.
  GetGameInstance()
    ->GetSubsystem<UIntegrityManager>()
    ->RequestIntegrityToken(Request, Delegate);
}

原生

/// 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 Encryption (JWE)JSON Web Signature (JWS)。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() 方法获取返回的令牌。

以下示例展示了如何在应用后端将 Play 管理中心中的 AES 密钥和用于签名验证的 DER 编码公钥 EC 密钥解码为特定语言的密钥(在本例中是 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();

生成的载荷是包含完整性判决的纯文本令牌。