If you're only planning to make standard API requests, which are suitable for the majority of developers, you can skip ahead to integrity verdicts. This page describes making classic API requests for integrity verdicts, which are supported on Android 4.4 (API level 19) or higher.
Considerations
Compare standard and classic requests
You can make standard requests, classic requests, or a combination of the two depending on your app's security and anti-abuse needs. Standard requests are suitable for all apps and games and can be used to check that any action or server call is genuine, while delegating some protection against replayability and exfiltration to Google Play. Classic requests are more expensive to make and you are responsible for correctly implementing them to protect against exfiltration and certain types of attacks. Classic requests should be made less frequently than standard requests, for example as an occasional one-off to check if a highly valuable or sensitive action is genuine.
The following table highlights the key differences between the two types of requests:
Standard API request | Classic API request | |
---|---|---|
Pre-requisites | ||
Minimum Android SDK version required | Android 5.0 (API level 21) or higher | Android 4.4 (API level 19) or higher |
Google Play requirements | Google Play Store and Google Play services | Google Play Store and Google Play services |
Integration details | ||
API warm up required | ✔️ (a few seconds) | ❌ |
Typical request latency | A few hundred milliseconds | A few seconds |
Potential request frequency | Frequent (on-demand check for any action or request) | Infrequent (one-off check for highest value actions or most sensitive requests) |
Timeouts | Most warm ups are under 10s but they involve a server call, so a long timeout is recommended (e.g. 1 minute). Verdict requests happen client-side | Most requests are under 10s but they involve a server call, so a long timeout is recommended (e.g. 1 minute) |
Integrity verdict token | ||
Contains device, app, and account details | ✔️ | ✔️ |
Token caching | Protected on-device caching by Google Play | Not recommended |
Decrypt and verify token via Google Play server | ✔️ | ✔️ |
Typical decryption server-to-server request latency | 10s of milliseconds with three-nines availability | 10s of milliseconds with three-nines availability |
Decrypt and verify token locally in a secure server environment | ❌ | ✔️ |
Decrypt and verify token client-side | ❌ | ❌ |
Integrity verdict freshness | Some automatic caching and refreshing by Google Play | All verdicts recomputed on each request |
Limits | ||
Requests per app per day | 10,000 by default (an increase can be requested) | 10,000 by default (an increase can be requested) |
Requests per app instance per minute | Warm ups: 5 per minute Integrity tokens: No public limit* |
Integrity tokens: 5 per minute |
Protection | ||
Mitigate against tampering and similar attacks | Use requestHash field |
Use nonce field with content binding based on request data |
Mitigate against replay and similar attacks | Automatic mitigation by Google Play | Use nonce field with server side logic |
* All requests, including those without public limits, are subject to non-public defensive limits at high values
Make classic requests infrequently
Generating an integrity token uses time, data, and battery, and each app has a maximum number of classic requests it can make per day. Therefore you should only make classic requests to check the highest value or most sensitive actions are genuine when you want an additional guarantee to a standard request. You shouldn't make classic requests for high-frequency or low-value actions. Don't make classic requests every time the app goes to the foreground nor every few minutes in the background, and avoid calling from a large number of devices at the same time. An app making too many classic requests calls may be throttled to protect users from incorrect implementations.
Avoid caching verdicts
Caching a verdict increases the risk of attacks such as exfiltration and replay, where a good verdict is reused from an untrusted environment. If you're considering making a classic request and then caching it for use later, it's recommended instead to perform a standard request on demand. Standard requests involve some caching on the device but Google Play uses additional protection techniques to mitigate the risk of replay attacks and exfiltration.
Use the nonce field to protect classic requests
The Play Integrity API offers a field called nonce
, which can be used to
further protect your app against certain attacks, such as replay and tampering
attacks. The Play Integrity API returns the value you set in this field, inside
the signed integrity response. Carefully follow the guidance on how to generate
nonces to protect your app from attacks.
Retry classic requests with exponential backoff
Environmental conditions, such as an unstable Internet connection or an overloaded device, can cause device integrity checks to fail. This can lead to no labels being generated for a device that is otherwise trustworthy. To mitigate these scenarios, include a retry option with exponential backoff.
Overview
When the user performs a high-value action in your app that you want to protect with an integrity check, complete the following steps:
- Your app's server-side backend generates and sends a unique value to the client-side logic. The remaining steps refer to this logic as your "app".
- Your app creates the
nonce
from the unique value and the content of your high-value action. It then calls the Play Integrity API, passing in thenonce
. - Your app receives a signed and encrypted verdict from the Play Integrity API.
- Your app passes the signed and encrypted verdict to your app's backend.
- Your app's backend sends the verdict to a Google Play server. The Google Play server decrypts and verifies the verdict, returning the results to your app's backend.
- Your app's backend decides how to proceed, based on the signals contained in the token payload.
- Your app's backend sends the decision outcomes to your app.
Generate a nonce
When you protect an action in your app with the Play Integrity API, you can
leverage the nonce
field to mitigate certain types of attacks, such as
person-in-the-middle (PITM) tampering attacks and replay attacks. The Play
Integrity API returns the value you set in this field inside the signed
integrity response.
The value set in the nonce
field must be correctly formatted:
String
- URL-safe
- Encoded as Base64 and non-wrapping
- Minimum of 16 characters
- Maximum of 500 characters
The following are some common ways to use the nonce
field in the Play
Integrity API. To get the strongest protection from the nonce
, you can combine
the methods below.
Include a request hash to protect against tampering
You can use the nonce
parameter in a classic API request similarly to the
requestHash
parameter in a standard API request to protect the contents of a
request against tampering.
When you request an integrity verdict:
- Compute a digest of all critical request parameters (e.g. SHA256 of a stable request serialization) from the user action or server request that is happening.
- Use
setNonce
to set thenonce
field to the value of the computed digest.
When you receive an integrity verdict:
- Decode and verify the integrity token, and obtain the digest from the
nonce
field. - Compute a digest of the request in the same manner as in the app (e.g. SHA256 of a stable request serialization).
- Compare the app-side and server-side digests. If they do not match, the request is not trustworthy.
Include unique values to protect against replay attacks
In order to prevent malicious users from reusing previous responses from the
Play Integrity API, you can use the nonce
field to uniquely identify each
message.
When you request an integrity verdict:
- Obtain a globally unique value in a way that malicious users cannot predict. For example, a cryptographically-secure random number generated on the server side can be such a value, or a pre-existing ID, such as a session or a transaction ID. A simpler and less secure variant is to generate a random number on the device. We recommend creating values 128 bits or larger.
- Call
setNonce()
to set thenonce
field to the unique value from step 1.
When you receive an integrity verdict:
- Decode and verify the integrity token, and obtain the unique value from the
nonce
field. - If the value from step 1 was generated on the server, check that the received unique value was one of the generated values, and that it's being used for the first time (your server will need to keep a record of generated values for a suitable duration). If the received unique value has been used already or does not appear in the record, reject the request
- Otherwise, if the unique value was generated on the device, check that the received value is being used for the first time (your server needs to keep a record of already seen values for a suitable duration). If the received unique value has been used already, reject the request.
Combine both protections against tampering and replay attacks (recommended)
It is possible to use the nonce
field to protect against both tampering and
replay attacks at the same time. To do so, generate the unique value as
described above, and include it as part of your request. Then compute the
request hash, making sure to include the unique value as part of the hash. An
implementation that combines both approaches is as follows:
When you request an integrity verdict:
- The user initiates the high-value action.
- Obtain a unique value for this action as described in the Include unique values to protect against replay attacks section.
- Prepare a message you want to protect. Include the unique value from step 2 in the message.
- Your app calculates a digest of the message it wants to protect, as described in the Include a request hash to protect against tampering section. Since the message contains the unique value, the unique value is part of the hash.
- Use
setNonce()
to set thenonce
field to the computed digest from the previous step.
When you receive an integrity verdict:
- Obtain the unique value from the request
- Decode and verify the integrity token, and obtain the digest from the
nonce
field. - As described in the Include a request hash to protect against tampering section, recompute the digest on the server side, and check that it matches the digest obtained from the integrity token.
- As described in the Include unique values to protect against replay attacks section, check the validity of the unique value.
The following sequence diagram illustrates these steps with a server-side
nonce
:
Request an integrity verdict
After generating a nonce
, you can request an integrity verdict from Google
Play. To do so, complete the following steps:
- Create an
IntegrityManager
, as shown in the following examples. - Construct an
IntegrityTokenRequest
, supplying thenonce
through thesetNonce()
method in the associated builder. Apps exclusively distributed outside of Google Play and SDKs also have to specify their Google Cloud project number through thesetCloudProjectNumber()
method. Apps on Google Play are linked to a Cloud project in the Play Console and do not need to set the Cloud project number in the request. Use the manager to call
requestIntegrityToken()
, supplying theIntegrityTokenRequest
.
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(); }
Native
/// 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();
Decrypt and verify the integrity verdict
When you request an integrity verdict, the Play Integrity API provides a signed
response token. The nonce
that you include in your request becomes part of the
response token.
Token format
The token is a nested JSON Web Token (JWT), that is JSON Web Encryption (JWE) of JSON Web Signature (JWS). The JWE and JWS components are represented using compact serialization .
The encryption / signing algorithms are well-supported across various JWT implementations:
Decrypt and verify on Google's servers (recommended)
The Play Integrity API allows you to decrypt and verify the integrity verdict on Google's servers, which enhances your app's security. To do so, complete these steps:
- Create a service account within the Google Cloud project that's linked to your app.
On your app's server, fetch the access token from your service account credentials using the
playintegrity
scope, and make the following request:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
Read the JSON response.
Decrypt and verify locally
If you choose to manage and download your response encryption keys, you can
decrypt and verify the returned token within your own secure server environment.
You can obtain the returned token by using the IntegrityTokenResponse#token()
method.
The following example shows how to decode the AES key and the DER-encoded public EC key for signature verification from the Play Console to language-specific (the Java programming language, in our case) keys in the app's backend. Note that the keys are base64-encoded using default flags.
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));
Next, use these keys to first decrypt the integrity token (JWE part) and then verify and extract the nested JWS part.
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();
The resulting payload is a plain-text token that contains integrity verdicts.