如果您只打算发出适用于大多数开发者的标准 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
的字段,可用于进一步保护应用免受某些攻击的侵害,例如重放攻击和篡改攻击。Play Integrity API 会在带签名的完整性响应中返回您在此字段中设置的值。请严格遵守关于如何生成 Nonce 的指南,以保护应用免遭攻击。
使用指数退避算法重试传统请求
环境条件(例如互联网连接不稳定或设备过载)可能会导致设备完整性检查失败。这样可能会导致系统不为本来可信的设备生成任何标签。为了缓解这些情况,请包含使用指数退避算法进行重试的选项。
概览
当用户在您的应用中执行您要通过完整性检查保护的重要操作时,请完成以下步骤:
- 应用的服务器端后端生成一个唯一的值,并将其发送到客户端逻辑。剩余步骤会将该逻辑称为您的“应用”。
- 您的应用会基于唯一值以及重要操作的内容创建
nonce
,然后会调用 Play Integrity API,并传入nonce
。 - 您的应用收到来自 Play Integrity API 的已签名且加密的判定。
- 应用将已签名且加密的判定结果传递到应用后端。
- 应用后端将判定结果发送到 Google Play 服务器。Google Play 服务器解密并验证判定结果,并将结果返回给应用的后端。
- 应用后端根据令牌载荷中包含的信号来决定如何继续操作。
- 应用后端将决策结果发送到您的应用。
生成 Nonce
使用 Play Integrity API 保护应用中的某项操作时,您可以利用 nonce
字段来缓解特定类型的攻击,例如中间人 (PITM) 篡改攻击和重放攻击。Play Integrity API 会在带签名的完整性响应中返回您在此字段中设置的值。
在 nonce
字段中设置的值必须采用正确的格式:
String
- 具有网址安全性
- 编码为 Base64 且没有换行
- 最少 16 个字符
- 最多 500 个字符
以下是在 Play Integrity API 中使用 nonce
字段的一些常用方法。为了获得最强大的 nonce
保护,您可以组合使用以下方法。
添加请求哈希值以防范篡改
您可以在传统 API 请求中使用 nonce
参数,方法类似于在标准 API 请求中使用 requestHash
参数,以保护请求的内容不被篡改。
请求完整性判定时:
- 计算正在发生的用户操作或服务器请求中的所有关键请求参数(例如,稳定请求序列化的 SHA256)的摘要。
- 使用
setNonce
将nonce
字段设置为计算出的摘要的值。
收到完整性判定结果后:
- 解码并验证完整性令牌,然后从
nonce
字段获取摘要。 - 采用与应用中相同的方式计算请求摘要(例如,稳定请求序列化的 SHA256)。
- 比较应用端摘要和服务器端摘要。如果二者不符,则表示请求不可信。
添加唯一值以防范重放攻击
为防止恶意用户重复使用之前来自 Play Integrity 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 管理中心关联到 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 Integrity API 会提供已签名的响应令牌。您在请求中包含的 nonce
会成为响应令牌的一部分。
令牌格式
令牌是指嵌套的 JSON 网络令牌 (JWT),即 JSON 网络签名 (JWS) 的 JSON 网络加密 (JWE)。JWE 和 JWS 组件使用紧凑的序列化来表示。
加密/签名算法在各种 JWT 实现中得到了很好的支持:
在 Google 服务器上进行解密和验证(推荐做法)
借助 Play Integrity API,您可以在 Google 服务器上解密和验证完整性判定结果,从而提高应用的安全性。为此,请完成以下步骤:
- 创建服务账号 (位于与您的应用相关联的 Google Cloud 项目中)。
在应用服务器上,使用
playintegrity
范围从服务账号凭据中提取访问令牌,然后发出以下请求:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
读取 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();
生成的载荷是包含完整性判定的纯文本令牌。