如果您只打算发起标准 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 服务器解密和验证令牌 | ✔️ | ✔️ |
典型的服务器到服务器解密请求延迟 | 具有三个九可用性的几十毫秒 | 具有三个九可用性的几十毫秒 |
在安全服务器环境中本地解密和验证令牌 | ❌ | ✔️ |
在客户端解密和验证令牌 | ❌ | ❌ |
完整性验证结果有效期 | Google Play 自动缓存和刷新 | 每个请求都会重新计算所有验证结果 |
限制 | ||
每个应用每天的请求数 | 默认情况下为 10,000(可以请求增加) | 默认情况下为 10,000(可以请求增加) |
每个应用实例每分钟的请求数 | 预热:每分钟 5 次 完整性令牌:没有公开限制* |
完整性令牌:每分钟 5 次 |
保护 | ||
减轻篡改和类似攻击 | 使用requestHash 字段 |
使用nonce 字段以及基于请求数据的绑定内容 |
减轻重放和类似攻击 | Google Play 自动减轻 | 使用nonce 字段以及服务器端逻辑 |
* 所有请求(包括那些没有公开限制的请求)在高值时都受非公开防御性限制的约束
不频繁地发起经典请求
生成完整性令牌会消耗时间、数据和电池电量,并且每个应用每天可以进行的经典请求次数都有上限。因此,您应该仅在需要对标准请求进行额外保证时,才使用经典请求来检查最高价值或最敏感的操作是否真实。您不应该对高频或低价值的操作进行经典请求。不要在应用每次进入前台时或在后台每隔几分钟进行经典请求,并避免同时从大量设备调用。如果应用发出的经典请求次数过多,可能会受到限流,以保护用户免受错误实现的影响。
避免缓存验证结果
缓存验证结果会增加遭受诸如泄露和重放等攻击的风险,攻击者可能会从不受信任的环境中重复使用有效的验证结果。如果您正在考虑发出经典请求,然后将其缓存以供以后使用,建议改为根据需要执行标准请求。标准请求会涉及在设备上进行一些缓存,但 Google Play 会使用其他保护技术来降低重放攻击和泄露的风险。
使用 nonce 字段保护经典请求
Play 完整性 API 提供了一个名为 nonce
的字段,可用于进一步保护您的应用免受某些攻击,例如重放和篡改攻击。Play 完整性 API 会在签名后的完整性响应中返回您在此字段中设置的值。请仔细遵循有关 如何生成 nonce 的指南,以保护您的应用免受攻击。
使用指数退避重试经典请求
不稳定的网络连接或设备过载等环境条件可能会导致设备完整性检查失败。这可能导致无法为原本可信的设备生成标签。为了缓解这些情况,请包含一个带有指数退避的重试选项。
概述
当用户在您的应用中执行您想要使用完整性检查保护的高价值操作时,请完成以下步骤
- 您的应用服务器端后端会生成一个唯一值并将其发送到客户端逻辑。其余步骤将此逻辑称为您的“应用”。
- 您的应用根据唯一值和高价值操作的内容创建
nonce
。然后,它调用 Play 完整性 API,并传入nonce
。 - 您的应用会从 Play 完整性 API 接收签名且加密的验证结果。
- 您的应用会将签名且加密的验证结果传递到您的应用后端。
- 您的应用后端会将验证结果发送到 Google Play 服务器。Google Play 服务器会解密并验证验证结果,并将结果返回到您的应用后端。
- 您的应用后端会根据令牌有效负载中包含的信号决定如何继续执行。
- 您的应用后端会将决策结果发送到您的应用。
生成 nonce
当您使用 Play 完整性 API 保护应用中的操作时,您可以利用 nonce
字段来缓解某些类型的攻击,例如中间人 (PITM) 篡改攻击和重放攻击。Play 完整性 API 会在签名后的完整性响应中返回您在此字段中设置的值。
在 nonce
字段中设置的值必须格式正确
字符串
- URL 安全
- 使用 Base64 编码且不换行
- 至少 16 个字符
- 最多 500 个字符
以下是使用 Play 完整性 API 中的 nonce
字段的一些常见方法。为了获得 nonce
提供的最强保护,您可以结合使用以下方法。
包含请求哈希以防止篡改
您可以像在标准 API 请求中使用 requestHash
参数一样,在经典 API 请求中使用 nonce
参数来保护请求内容免受篡改。
当您请求完整性验证结果时
- 计算所有关键请求参数的摘要(例如,正在发生的用户操作或服务器请求的稳定请求序列化的 SHA256)。
- 使用
setNonce
将nonce
字段设置为计算出的摘要的值。
当您收到完整性验证结果时
- 解码并验证完整性令牌,并从
nonce
字段中获取摘要。 - 以与应用相同的方式计算请求的摘要(例如,稳定请求序列化的 SHA256)。
- 比较应用端和服务器端的摘要。如果它们不匹配,则该请求不可信。
包含唯一值以防止重放攻击
为了防止恶意用户重复使用 Play 完整性 API 的先前响应,您可以使用 nonce
字段来唯一标识每条消息。
当您请求完整性验证结果时
- 以恶意用户无法预测的方式获取全局唯一值。例如,服务器端生成的加密安全的随机数可以作为此类值,或者可以使用预先存在的 ID,例如会话 ID 或交易 ID。一种更简单但安全性较低的方法是在设备上生成随机数。我们建议创建 128 位或更大的值。
- 调用
setNonce()
将nonce
字段设置为步骤 1 中的唯一值。
当您收到完整性验证结果时
- 解码并验证完整性令牌,并从
nonce
字段中获取唯一值。 - 如果步骤 1 中的值是在服务器上生成的,请检查接收到的唯一值是否为已生成的值之一,并且是否为第一次使用(您的服务器需要在适当的时间段内保留已生成值的记录)。如果已使用接收到的唯一值或该值未出现在记录中,则拒绝请求
- 否则,如果唯一值是在设备上生成的,请检查接收到的值是否为第一次使用(您的服务器需要在适当的时间段内保留已查看值的记录)。如果已使用接收到的唯一值,则拒绝请求。
结合防止篡改和重放攻击的两种保护措施(建议)
可以使用 nonce
字段同时防止篡改和重放攻击。为此,请按上述说明生成唯一值,并将其作为请求的一部分包含在内。然后计算请求哈希,确保将唯一值作为哈希的一部分包含在内。结合这两种方法的实现如下
当您请求完整性验证结果时
- 用户发起高价值操作。
- 按照 包含唯一值以防止重放攻击 部分中的说明获取此操作的唯一值。
- 准备要保护的消息。在消息中包含步骤 2 中的唯一值。
- 您的应用会计算其要保护的消息的摘要,如 包含请求哈希以防止篡改 部分中所述。由于消息包含唯一值,因此唯一值是哈希的一部分。
- 使用
setNonce()
将nonce
字段设置为上一步计算出的摘要。
当您收到完整性验证结果时
- 从请求中获取唯一值
- 解码并验证完整性令牌,并从
nonce
字段中获取摘要。 - 如 包含请求哈希以防止篡改 部分中所述,在服务器端重新计算摘要,并检查它是否与从完整性令牌中获取的摘要匹配。
- 如 包含唯一值以防止重放攻击 部分中所述,检查唯一值的有效性。
以下时序图说明了这些步骤,其中包含服务器端 nonce
请求完整性验证结果
生成 nonce
后,您可以向 Google Play 请求完整性验证结果。为此,请完成以下步骤
- 创建
IntegrityManager
,如下例所示。 - 构造
IntegrityTokenRequest
,通过关联构建器中的setNonce()
方法提供nonce
。仅在 Google Play 之外分发的应用和 SDK 还必须通过setCloudProjectNumber()
方法指定其 Google Cloud 项目编号。Google Play 上的应用会链接到 Play Console 中的 Cloud 项目,无需在请求中设置 Cloud 项目编号。 使用管理器调用
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 完整性 API 会提供一个签名的响应令牌。您在请求中包含的 nonce
会成为响应令牌的一部分。
令牌格式
令牌是嵌套的 JSON Web 令牌 (JWT),它是 JSON Web 加密 (JWE) 的 JSON Web 签名 (JWS)。JWE 和 JWS 组件使用 紧凑序列化 表示。
各种 JWT 实现都很好地支持加密/签名算法
在 Google 服务器上解密和验证(建议)
Play 完整性 API 允许您在 Google 服务器上解密并验证完整性验证结果,从而增强应用的安全性。为此,请完成以下步骤
- 在与您的应用关联的 Google Cloud 项目中 创建服务帐号。
在您的应用服务器上,使用
playintegrity
范围从您的服务帐号凭据中获取访问令牌,并发出以下请求playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
读取 JSON 响应。
本地解密和验证
如果您选择管理和下载响应加密密钥,则可以在您自己的安全服务器环境中解密和验证返回的令牌。您可以使用 IntegrityTokenResponse#token()
方法获取返回的令牌。
以下示例演示了如何将 Play Console 中的 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();
结果负载是一个包含 完整性判决 的纯文本令牌。