通过 Play Integrity API 的 nonce 字段提高应用安全性
作者 / Oscar Rodriguez, Developer Relations Engineer
Play Integrity API https://developer.android.google.cn/google/play/integrity
什么是 nonce?
在密码学和安全工程学中,nonce (number once) 是一个在安全通信中仅能被使用一次的数字。nonce 用途广泛,如身份验证、数据加密和哈希处理等。
在 Play Integrity API 中,nonce 是您在调用 API 完整性检查前设置的不透明 Base64 编码二进制 blob,并通过被签名的响应中原样返回。根据创建和验证 nonce 的方式,您可以使用它来进一步加强 Play Integrity API 的现有保护措施,并缓解特定类型的攻击,例如中间人 (PITM) 篡改攻击和重放攻击。
除了在被签名的响应中按原样返回 nonce,Play Integrity API 不会对 nonce 实际数据进行任何处理,因此您可以设置任意值,只要它是一个有效的 Base64 值即可。也就是说,为了对响应进行数字签名,nonce 值将被发送到 Google 服务器,因此请勿将 nonce 设置为任何类型的个人身份信息 (PII),例如用户姓名、电话或电子邮件地址。
设置 nonce
将您的应用设置为使用 Play Integrity API 之后,您可以使用 setNonce() 方法,或其适当的变体设置 nonce,这些变体适用于 API 的 Kotlin、Java、Unity 和 Native 版本。
将您的应用设置为使用 Play Integrity API https://developer.android.google.cn/google/play/integrity/setup 设置 nonce https://developer.android.google.cn/google/play/integrity/verdict#request
val nonce: String = ...
// 创建 manager 的实例
val integrityManager =
IntegrityManagerFactory.create(applicationContext)
// 通过 nonce 获取完整性令牌
val integrityTokenResponse: Task<IntegrityTokenResponse> =
integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce) // 设置 nonce
.build())
Java:
String nonce = ...
// 创建 manager 的实例
IntegrityManager integrityManager =
IntegrityManagerFactory.create(getApplicationContext());
// 通过 nonce 获取完整性令牌
Task<IntegrityTokenResponse> integrityTokenResponse =
integrityManager
.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce) // 设置 nonce
.build());
Unity:
string nonce = ...
// 创建 manager 的实例
var integrityManager = new IntegrityManager();
// 通过 nonce 获取完整性令牌
var tokenRequest = new IntegrityTokenRequest(nonce);
var requestIntegrityTokenOperation =
integrityManager.RequestIntegrityToken(tokenRequest);
Native:
// 创建 IntegrityTokenRequest 对象
const char* nonce = ...
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce); // 设置 nonce
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
IntegrityManager_requestIntegrityToken(request, &response);
验证 nonce
Play Integrity API 的响应以 JSON 网络令牌 (JWT) 的形式返回,其负载为纯文本 JSON,格式如下:
{
requestDetails: { ... }
appIntegrity: { ... }
deviceIntegrity: { ... }
accountDetails: { ... }
}
JSON 网络令牌 (JWT) https://jwt.io/ 纯文本 JSON https://developer.android.google.cn/google/play/integrity/verdict#returned-payload-format
requestDetails: {
requestPackageName: "...",
nonce: "...",
timestampMillis: ...
}
保护重要操作
用户发起重要操作; 应用准备好要保护的消息,例如 JSON 格式的消息; 应用计算要保护的消息的加密哈希值。例如,使用 SHA-256 或 SHA-3-256 哈希算法; 应用调用 Play Integrity API,并调用 setNonce() 以将 nonce 字段设置为在上一步计算的加密哈希值; 应用将要保护的消息以及 Play Integrity API 的签名结果发送给服务器; 应用服务器验证其收到的消息的加密哈希值是否与签名结果中的 nonce 字段值匹配,并拒绝任何不匹配的结果。
防范重放攻击
也就是说,应用服务器也可以利用 Play Integrity API 中的 nonce,为每个响应分配一个唯一值,并验证该响应是否与之前设置的唯一值匹配。实现方法如下:
服务器以攻击者无法预测的方式创建全局唯一值。例如,128 位或位数更多的加密安全随机数; 应用调用 Play Integrity API,并将 nonce 字段设置为应用服务器接收的唯一值; 应用将 Play Integrity API 的签名结果发送到服务器; 服务器验证签名结果中的 nonce 字段是否与之前生成的唯一值匹配,并拒绝所有不匹配的结果。
结合两种保护措施
用户发起重要操作; 应用要求服务器提供一个标识请求的唯一值; 应用服务器生成全局唯一值,防止攻击者做出预测。例如,您可以使用加密安全的随机数生成器创建此类值。我们建议创建不小于 128 位的值; 应用服务器向应用发送全局唯一值; 应用准备好要保护的消息,例如 JSON 格式的消息; 应用计算要保护的消息的加密哈希值。例如,使用 SHA-256 或 SHA-3-256 哈希算法; 应用通过附加从应用服务器收到的唯一值以及要保护的消息的哈希值来创建一个字符串; 应用调用 Play Integrity API,并调用 setNonce() 以将 nonce 字段设置为在上一步中创建的字符串; 应用将要保护的消息以及 Play Integrity API 的签名结果发送给服务器; 应用服务器拆分 nonce 字段的值,然后验证消息的加密哈希值以及之前生成的唯一值是否与预期值相匹配,并拒绝任何不匹配的结果。
https://developer.android.google.cn/google/play/integrity
推荐阅读