如果您只计划发起标准 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 毫秒 | 具有三个九可用性的 10 毫秒 |
在安全的服务器环境中本地解密和验证令牌 | ❌ | ✔️ |
客户端解密和验证令牌 | ❌ | ❌ |
完整性结果新鲜度 | 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();
生成的有效负载是一个纯文本令牌,其中包含完整性验证结果。