(Migrating to Play Integrity API) Step 3 - Obtaining and Decoding the Integrity Verdict

In the previous tutorial we learnt how to obtain the encryption and decryption keys from the play console dashboard. And now it's time to make a call to the google play integrity api and get a response for the Integrity Verdict. The response is received as an encrypted string, which has to be securely decrypted using the two keys that we obtained the last time.
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

The Gradle Dependencies

Let me first list the gradle dependencies that have to be linked through your build.gradle file.

First is the play integrity dependency, and the second is jose4j. You might want to use their latest versions, and the ones shown below are the latest as on the date of publish of this article.


implementation 'com.google.android.play:integrity:1.0.1'

// https://mvnrepository.com/artifact/org.bitbucket.b_c/jose4j 
implementation group: 'org.bitbucket.b_c', name: 'jose4j', version: '0.7.12'

Video Explanation (see it happen!)

Please watch the following youtube video:

The Source Code Explained

Your app will most probably query the integrity verdict in response to a button click or perhaps during the start of your MainActivity. Hence, it's going to be a function call, the result of which will be a json string in plain text.

The code that you see here will be inside some function. The lines of code can be obtained from the blog page that I have listed in the description below. Now I will explain it line by line.

The first three lines contain hard-coded values for nonce, DECRYPTION_KEY and VERIFICATION_KEY. I have done this to keep my tutorial as simple and as to-the-point as possible. For a production scenario, you might want to refer the documentation for robust safety recommendations.

The once string must be of atleast 16 characters in length, and must be in a base64 format.

The next statement is an asynchronous request to obtain the integrity token by using the setNonce function.

The OnSuccessListener brings the integrity token, which is an encrypted string.

The rest of the code is mostly consumed as a copy paste stuff. But I'll quickly go through it.

SecretKey is obtained, followed by a PublicKey. Both are later used for decryption.

Then we have a JsonWebEncryption, followed by setKey, then we have compactJws, and similarly JsonWebSignature.

And finally we obtain the decrypted json verdict as a string jsonPlainVerdict


// based on -  https://developer.android.com/google/play/integrity/verdict 

final String TAG = "MainActivity";

// this code is put inside a function that 
// can be called on a click event 
// the code executes asynchronously 
// watch the linked video for better clarity 

// nonce must be base64 encoded  
// details https://developer.android.com/google/play/integrity/verdict 
// hardcoded only for tutorial but can be generated on your server 
// and obtained by a secure http request 
final String nonce = "aG92ZW...must_be_base64...XM=";

// DECRYPTION_KEY, VERIFICATION_KEY are hard-coded for tutorial 
// but can be stored in a safer way, for example on a server 
// and obtained by a secure http request 
final String DECRYPTION_KEY = "1Ie2XCCzu...hHI=";
final String VERIFICATION_KEY = "MFkwEwYHKoZIzj0CAQ...zKw==";

// 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())
    .addOnSuccessListener(
    (OnSuccessListener<IntegrityTokenResponse>)
    response -> {

        String integrityToken = response.token();

        Log.d(TAG, integrityToken);

        byte[] decryptionKeyBytes =
            Base64.decode(DECRYPTION_KEY, Base64.DEFAULT);

        // SecretKey 
        SecretKey decryptionKey =
            new SecretKeySpec(
            decryptionKeyBytes,
            0,
            decryptionKeyBytes.length,
            "AES");

        byte[] encodedVerificationKey =
            Base64.decode(VERIFICATION_KEY, Base64.DEFAULT);

        // PublicKey 
        PublicKey verificationKey = null;

        try {

          verificationKey = KeyFactory.getInstance("EC")
            .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

        } catch (InvalidKeySpecException e) {

            Log.d(TAG, e.getMessage());

        } catch (NoSuchAlgorithmException e) {

            Log.d(TAG, e.getMessage());

        }

        // some error occurred so return 
        if (null == verificationKey) {

            return;

        }

        // JsonWebEncryption 
        JsonWebEncryption jwe = null;
        try {

            jwe = (JsonWebEncryption) JsonWebStructure
                .fromCompactSerialization(integrityToken);

        } catch (JoseException e) {

            e.printStackTrace();

        }

        // some error occurred so return 
        if (null == jwe) {

            return;

        }

        jwe.setKey(decryptionKey);

        String compactJws = null;

        try {

            compactJws = jwe.getPayload();

        } catch (JoseException e) {

            Log.d(TAG, e.getMessage());

        }

        // JsonWebSignature 
        JsonWebSignature jws = null;

        try {

        jws = (JsonWebSignature) JsonWebStructure
                .fromCompactSerialization(compactJws);

        } catch (JoseException e) {

        Log.d(TAG, e.getMessage());

        }

        // some error occurred so return 
        if (null == jws) {

        return;

        }

        jws.setKey(verificationKey);

        // get the json human readable string 
        String jsonPlainVerdict = "";

        try {

            jsonPlainVerdict = jws.getPayload();

        } catch (JoseException e) {

        Log.d(TAG, e.getMessage());

        return;
        }

        // payload is available in json format 
        // plain text, can be processed as per needs 
        Log.d(TAG, jsonPlainVerdict);


    })
    .addOnFailureListener((OnFailureListener) ex -> { });

Run the Project

Now we shall run the project that I have in my android studio (see video for screenshots), and then check the log. Open the Android Studio and run the application. The code has been called from the OnCreate method of my activity, so it fires automatically. But please take note that this is not the right place to do so.


// you will get similar 
// response - refer documentation 
// https://developer.android.com/google/play/integrity/verdict 
// for explanation 

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
}

As you can see the response has been duly logged in the logcat window. You can refer google documentation for an interpretation of this response, which is beyond the scope of this tutorial. Thanks!


This Blog Post/Article "(Migrating to Play Integrity API) Step 3 - Obtaining and Decoding the Integrity Verdict" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.