Google PlayIntegrity API: a Nightmare
Asked Answered
J

2

31

I need some help guys!! I am a self-taught, newbie in encryption, and after reading, testing, and error for more than two weeks on how to solve this, and finding very little crowd knowledge and almost no documentation from Google.

I am trying to read the integrity verdict, that I have managed to get it IntegrityTokenRequest doing

    String nonce = Base64.encodeToString("this_is_my_nonce".getBytes(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
    IntegrityManager myIntegrityManager =   IntegrityManagerFactory
          .create(getApplicationContext());
    // Request the integrity token by providing a nonce.
    Task<IntegrityTokenResponse> myIntegrityTokenResponse = myIntegrityManager
          .requestIntegrityToken(IntegrityTokenRequest
          .builder()
          .setNonce(nonce)
          .build());

    myIntegrityTokenResponse.addOnSuccessListener(new OnSuccessListener<IntegrityTokenResponse>() {
        @Override
        public void onSuccess(IntegrityTokenResponse myIntegrityTokenResponse) {
            String token = myIntegrityTokenResponse.token();
            // so here I have my Integrity token.
            // now how do I read it??
        }
    }

As per the documentation, it's all set up in the Play Console, and created the Google Cloud project accordingly. Now here comes the big hole in the documentation:

a) The JWT has 4 dots that divide the JWT into 5 sections, not in 3 sections as described here https://jwt.io/

b) Developer.Android.com recommends to Decrypt and Verify on Google Servers Developer.Android.com recommend to Decrypt and Verify on Google Servers

I have no idea on how or were to execute this command... :-(

c) if I choose to decrypt and verify the returned token it's more complicated as I don't have my own secure server environment, only my App and the Google Play Console.

d) I found in the Google Clound Platform OAuth 2.0 Client IDs "Android client for com.company.project" JSON file that I have downloaded, but no clue (again) on how to use it in my App for getting the veredict from the Integrity Token.

{"installed":
    {"client_id":"123456789012-abcdefghijklmnopqrstuvwxyza0g2ahk.apps.googleusercontent.com",
        "project_id":"myproject-360d3",
        "auth_uri":"https://accounts.google.com/o/oauth2/auth",
        "token_uri":"https://oauth2.googleapis.com/token",
        "auth_provider_x509_cert_url":https://www.googleapis.com/oauth2/v1/certs
    }
}

I'm sure I am missing a lot, please help

Jerold answered 10/5, 2022 at 21:18 Comment(2)
Related: developer.android.com/google/play/integrity/overview#api-usage and you need your own cloud server as that "I don't know command" I believe is github.com/googleapis/google-api-java-client-services/tree/main/… which needs to be appropriately cloud authorized. [Side note: if you never done cloud before, Google owns qwiklabs.com and your local Google Developer Group may be able to get you time limited credits/subs]Rippy
The docs say create service account credentials. not installed credentials.Osyth
C
13

Using a cloud server to decode and verify the token is better. For example, if you going with Java service then the below code will send the integrity token to the google server hence you can verify the response. Enable PlayIntegrity API in Google Cloud Platform against the app and download the JSON file and configure in the code. Similarly, you should enable PlayIntegrity API in Google PlayConsole against the app Add Google Play Integrity Client Library to your project

Maven Dependency

<project>
 <dependencies>
   <dependency>
     <groupId>com.google.apis</groupId>
     <artifactId>google-api-services-playintegrity</artifactId>
     <version>v1-rev20220211-1.32.1</version>
   </dependency>
 </dependencies>

Gradle

repositories {
   mavenCentral()
}
dependencies {
   implementation 'com.google.apis:google-api-services-playintegrity:v1-rev20220211-1.32.1'
}

Token decode

DecodeIntegrityTokenRequest requestObj = new DecodeIntegrityTokenRequest();
requestObj.setIntegrityToken(request.getJws());
//Configure downloaded Json file
GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream("<Path of JSON file>\\file.json"));
HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);

 HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
 JsonFactory JSON_FACTORY = new JacksonFactory();
 GoogleClientRequestInitializer initialiser = new PlayIntegrityRequestInitializer();
 
 
Builder playIntegrity = new PlayIntegrity.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer).setApplicationName("testapp")
        .setGoogleClientRequestInitializer(initialiser);
             PlayIntegrity play = playIntegrity.build();
    
DecodeIntegrityTokenResponse response = play.v1().decodeIntegrityToken("com.test.android.integritysample", requestObj).execute();

Then the response will be as follows

{
"tokenPayloadExternal": {
    "accountDetails": {
        "appLicensingVerdict": "LICENSED"
    },
    "appIntegrity": {
        "appRecognitionVerdict": "PLAY_RECOGNIZED",
        "certificateSha256Digest": ["pnpa8e8eCArtvmaf49bJE1f5iG5-XLSU6w1U9ZvI96g"],
        "packageName": "com.test.android.integritysample",
        "versionCode": "4"
    },
    "deviceIntegrity": {
        "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
    },
    "requestDetails": {
        "nonce": "SafetyNetSample1654058651834",
        "requestPackageName": "com.test.android.integritysample",
        "timestampMillis": "1654058657132"
    }
}
}

Check for License

String licensingVerdict = response.getTokenPayloadExternal().getAccountDetails().getAppLicensingVerdict();
    if(!licensingVerdict.equalsIgnoreCase("LICENSED")) {
         throw new Exception("Licence is not valid.");
            
    }

Verify App Integrity

public void checkAppIntegrity(DecodeIntegrityTokenResponse response,  String appId) throws Exception {
    AppIntegrity appIntegrity = response.getTokenPayloadExternal().getAppIntegrity();
    
    if(!appIntegrity.getAppRecognitionVerdict().equalsIgnoreCase("PLAY_RECOGNIZED")) {
        throw new Exception("The certificate or package name does not match Google Play records.");
    }
     if(!appIntegrity.getPackageName().equalsIgnoreCase(appId)) {
         throw new Exception("App package name mismatch.");
        
     }
     
     if(appIntegrity.getCertificateSha256Digest()!= null) {
        //If the app is deployed in Google PlayStore then Download the App signing key certificate from Google Play Console (If you are using managed signing key). 
        //otherwise download Upload key certificate and then find checksum of the certificate.
         Certificate cert = getCertificate("<Path to Signing certificate>\deployment_cert.der");
         MessageDigest md = MessageDigest.getInstance("SHA-256"); 

        byte[] der = cert.getEncoded(); 
        md.update(der);
        byte[] sha256 = md.digest();
        
        //String checksum = Base64.getEncoder().encodeToString(sha256);
       String checksum = Base64.getUrlEncoder().encodeToString(sha256);
       /** Sometimes checksum value ends with '=' character, you can avoid this character before perform the match **/
       checksum = checksum.replaceAll("=","");        
        if(!appIntegrity.getCertificateSha256Digest().get(0).contains(checksum)) {
             throw new Exception("App certificate mismatch.");
        }
     }
}
public static Certificate getCertificate(String certificatePath)
        throws Exception {
    CertificateFactory certificateFactory = CertificateFactory
            .getInstance("X509");
    FileInputStream in = new FileInputStream(certificatePath);

    Certificate certificate = certificateFactory
            .generateCertificate(in);
    in.close();

    return certificate;
}

Verify Device integrity

//Check Device Integrity
public void deviceIntegrity(DecodeIntegrityTokenResponse response) {
    DeviceIntegrity deviceIntegrity = response.getTokenPayloadExternal().getDeviceIntegrity();
    if(!deviceIntegrity.getDeviceRecognitionVerdict().contains("MEETS_DEVICE_INTEGRITY")) {
        throw new Exception("Does not meet Device Integrity.");
        
    }
}

Similary you can verify the Nonce and App Package name with previously stored data in server

Craftwork answered 1/6, 2022 at 11:54 Comment(10)
It take me a while to find the place to download the '<Path to Signing certificate>\deployment_cert.der' file. You find it in the Google Play Console -> Project -> App integrity -> App signing (tab) -> Download certificat (top right)Fermentative
@Craftwork Where is nonce created - Frontend or Backend (Java)? What is the flow with Nonce creation?Joelynn
@UrvashiSoni Nonce should be created at backend. Please follow developer.android.com/google/play/integrity/verdict#nonce to generate nonce.Dinerman
@Dinerman I followed the same link. My project is in SpringBoot and the class IntegrityManager and its method setNonce() is unavailable. These are available for Android developers in com.google.android.gms package, which is not available for SpringBoot application.Joelynn
@UrvashiSoni setNonce will be invoked from android application only as Nonce generation and play integrity response verification will be done at backend.Dinerman
@Dinerman Thanks. I did nonce generation part, and shared the same to app. But, while decoding I am getting 403 Forbidden with message : "You are not authorized to decode the requested integrity token." Have u gone through this while your development?Joelynn
@UrvashiSoni For decoding response locally on your app server you will need response encryption keys. These keys you will get on play console under App Integrity sectionDinerman
need help - I get the error "404 Not Found - decodIntegrityToken was not found on this server" in the signed APk however it works very well in debug APK. The nonce is randomly generated with alphanumeric from my end.Hiddenite
@UrvashiSoni Nonce is unique idenfier for your request which may be encrypt of combination of Date,user Id or other information.Sarena
i did same integration like that. unfortunately i am getting "You are not authorized to decode the requested integrity token." error from google cloudGothard
J
14

Thanks a lot @John_S for your answer, I'll mark it as the final answer, anyway I post here all the missing parts for future developers so they can shortcut my almost one month sucked in this issue, as there is no complete documentation nor java examples (at the time of writing this) for the Google PlayIntegrity API.

First, you need to set our project in the Google Cloud, and Google Play as stated by @John_S, but the missing part is that you need to set a Credential as "Service Account" and then "Add Key" as described java.io.IOException: Error reading credentials from stream, 'type' field not specified and this https://developers.google.com/workspace/guides/create-credentials#android; then, you can download the .json file with your Credentials. The .json file described in my question is invalid as it must have a structure like this:

    {  "type": "service_account",
       "project_id": "your-project",
       "private_key_id": "your-key-id",
       "private_key": "your-private-key",
       "client_email": "[email protected]",
       "client_id": "your-client-id",
       "auth_uri": "https://accounts.google.com/o/oauth2/auth",
       "token_uri": "https://oauth2.googleapis.com/token",
       "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
       "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-email%40appspot.gserviceaccount.com"
    }

Second, once you have your valid .json file downloaded, store it in "src/main/resources/credentials.json" (create the new folder if needed, not into "res" folder), as stated here Where must the client_secrets.json file go in Android Studio project folder tree?

Third, to complete all the missing parts of the build.gradle you must include:

dependencies {
    implementation 'com.google.android.play:integrity:1.0.1'                  
    implementation 'com.google.apis:google-api-services-playintegrity:v1-rev20220211-1.32.1'
    implementation 'com.google.api-client:google-api-client-jackson2:1.20.0'
    implementation 'com.google.auth:google-auth-library-credentials:1.7.0'
    implementation 'com.google.auth:google-auth-library-oauth2-http:1.7.0'
}

And import them to your project

import com.google.android.gms.tasks.Task;
import com.google.android.play.core.integrity.IntegrityManager;
import com.google.android.play.core.integrity.IntegrityManagerFactory;
import com.google.android.play.core.integrity.IntegrityTokenRequest;
import com.google.android.play.core.integrity.IntegrityTokenResponse;
import com.google.api.services.playintegrity.v1.PlayIntegrity;
import com.google.api.services.playintegrity.v1.PlayIntegrityRequestInitializer;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.api.services.playintegrity.v1.model.DecodeIntegrityTokenRequest;
import com.google.api.services.playintegrity.v1.model.DecodeIntegrityTokenResponse;
import com.google.api.client.googleapis.services.GoogleClientRequestInitializer;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;

Then, the complete code for requesting the "Integrity Token" and decode it will be:

    // create the NONCE  Base64-encoded, URL-safe, and non-wrapped String
    String mynonce = Base64.encodeToString("this_is_my_nonce".getBytes(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);

    // Create an instance of a manager.
    IntegrityManager myIntegrityManager = IntegrityManagerFactory.create(getApplicationContext());

    // Request the integrity token by providing a nonce.
    Task<IntegrityTokenResponse> myIntegrityTokenResponse = myIntegrityManager
        .requestIntegrityToken(IntegrityTokenRequest
        .builder()
        .setNonce(mynonce)
//      .setCloudProjectNumber(cloudProjNumber)         // necessary only if sold outside Google Play
        .build());

        // get the time to check against the decoded integrity token time
        timeRequest = Calendar.getInstance().getTimeInMillis();

        myIntegrityTokenResponse.addOnSuccessListener(new OnSuccessListener<IntegrityTokenResponse>() {
            @Override
            public void onSuccess(IntegrityTokenResponse myIntegrityTokenResponse) {
                try {
                    String token = myIntegrityTokenResponse.token();

                    DecodeIntegrityTokenRequest requestObj = new DecodeIntegrityTokenRequest();
                    requestObj.setIntegrityToken(token);

                    //Configure your credentials from the downloaded Json file from the resource
                    GoogleCredentials credentials = GoogleCredentials.fromStream(Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("credentials.json"));
                    HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);

                    HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
                    JsonFactory JSON_FACTORY  = new JacksonFactory();
                    GoogleClientRequestInitializer initializer = new PlayIntegrityRequestInitializer();

                    PlayIntegrity.Builder playIntegrity = new PlayIntegrity.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer).setApplicationName("your-project")
                        .setGoogleClientRequestInitializer(initializer);
                    PlayIntegrity play  = playIntegrity.build();

                    // the DecodeIntegrityToken must be run on a parallel thread
                    Thread thread = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try  {
                                DecodeIntegrityTokenResponse response = play.v1().decodeIntegrityToken("com.project.name", requestObj).execute();
                                String licensingVerdict = response.getTokenPayloadExternal().getAccountDetails().getAppLicensingVerdict();
                                if (licensingVerdict.equalsIgnoreCase("LICENSED")) {
                                    // Looks good! LICENSED app
                                } else {
                                    // LICENSE NOT OK
                                }
                            } catch (Exception e) {
                                //  LICENSE error
                            }
                        }
                    });

                    // execute the parallel thread 
                    thread.start();

                } catch (Error | IOException e) {
                    // LICENSE error
                } catch (Exception e) {
                    // LICENSE error
                }
            }
    });

Hope this helps.

Jerold answered 5/6, 2022 at 23:29 Comment(7)
Where would it be the best palce to do this? in the class App: Application ()?Arredondo
please don't forget to add .createScoped(PlayIntegrityScopes.PLAYINTEGRITY); into credentialsSilo
I have followed the steps, but I'm having trouble with mynonce. What exactly goes in "this_is_my_nonce" ?Renault
need help - I get the error "404 Not Found - decodIntegrityToken was not found on this server" in the signed APk however it works very well in debug APK.Hiddenite
@ParthPrajapati exactly!! see #72751311Jerold
followed these suggestions, but on the server (Java) side but keep getting PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target. Any idea?Fda
credentials.json file is visible inside the apk , how to hide it ?Sabin
C
13

Using a cloud server to decode and verify the token is better. For example, if you going with Java service then the below code will send the integrity token to the google server hence you can verify the response. Enable PlayIntegrity API in Google Cloud Platform against the app and download the JSON file and configure in the code. Similarly, you should enable PlayIntegrity API in Google PlayConsole against the app Add Google Play Integrity Client Library to your project

Maven Dependency

<project>
 <dependencies>
   <dependency>
     <groupId>com.google.apis</groupId>
     <artifactId>google-api-services-playintegrity</artifactId>
     <version>v1-rev20220211-1.32.1</version>
   </dependency>
 </dependencies>

Gradle

repositories {
   mavenCentral()
}
dependencies {
   implementation 'com.google.apis:google-api-services-playintegrity:v1-rev20220211-1.32.1'
}

Token decode

DecodeIntegrityTokenRequest requestObj = new DecodeIntegrityTokenRequest();
requestObj.setIntegrityToken(request.getJws());
//Configure downloaded Json file
GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream("<Path of JSON file>\\file.json"));
HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);

 HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
 JsonFactory JSON_FACTORY = new JacksonFactory();
 GoogleClientRequestInitializer initialiser = new PlayIntegrityRequestInitializer();
 
 
Builder playIntegrity = new PlayIntegrity.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer).setApplicationName("testapp")
        .setGoogleClientRequestInitializer(initialiser);
             PlayIntegrity play = playIntegrity.build();
    
DecodeIntegrityTokenResponse response = play.v1().decodeIntegrityToken("com.test.android.integritysample", requestObj).execute();

Then the response will be as follows

{
"tokenPayloadExternal": {
    "accountDetails": {
        "appLicensingVerdict": "LICENSED"
    },
    "appIntegrity": {
        "appRecognitionVerdict": "PLAY_RECOGNIZED",
        "certificateSha256Digest": ["pnpa8e8eCArtvmaf49bJE1f5iG5-XLSU6w1U9ZvI96g"],
        "packageName": "com.test.android.integritysample",
        "versionCode": "4"
    },
    "deviceIntegrity": {
        "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
    },
    "requestDetails": {
        "nonce": "SafetyNetSample1654058651834",
        "requestPackageName": "com.test.android.integritysample",
        "timestampMillis": "1654058657132"
    }
}
}

Check for License

String licensingVerdict = response.getTokenPayloadExternal().getAccountDetails().getAppLicensingVerdict();
    if(!licensingVerdict.equalsIgnoreCase("LICENSED")) {
         throw new Exception("Licence is not valid.");
            
    }

Verify App Integrity

public void checkAppIntegrity(DecodeIntegrityTokenResponse response,  String appId) throws Exception {
    AppIntegrity appIntegrity = response.getTokenPayloadExternal().getAppIntegrity();
    
    if(!appIntegrity.getAppRecognitionVerdict().equalsIgnoreCase("PLAY_RECOGNIZED")) {
        throw new Exception("The certificate or package name does not match Google Play records.");
    }
     if(!appIntegrity.getPackageName().equalsIgnoreCase(appId)) {
         throw new Exception("App package name mismatch.");
        
     }
     
     if(appIntegrity.getCertificateSha256Digest()!= null) {
        //If the app is deployed in Google PlayStore then Download the App signing key certificate from Google Play Console (If you are using managed signing key). 
        //otherwise download Upload key certificate and then find checksum of the certificate.
         Certificate cert = getCertificate("<Path to Signing certificate>\deployment_cert.der");
         MessageDigest md = MessageDigest.getInstance("SHA-256"); 

        byte[] der = cert.getEncoded(); 
        md.update(der);
        byte[] sha256 = md.digest();
        
        //String checksum = Base64.getEncoder().encodeToString(sha256);
       String checksum = Base64.getUrlEncoder().encodeToString(sha256);
       /** Sometimes checksum value ends with '=' character, you can avoid this character before perform the match **/
       checksum = checksum.replaceAll("=","");        
        if(!appIntegrity.getCertificateSha256Digest().get(0).contains(checksum)) {
             throw new Exception("App certificate mismatch.");
        }
     }
}
public static Certificate getCertificate(String certificatePath)
        throws Exception {
    CertificateFactory certificateFactory = CertificateFactory
            .getInstance("X509");
    FileInputStream in = new FileInputStream(certificatePath);

    Certificate certificate = certificateFactory
            .generateCertificate(in);
    in.close();

    return certificate;
}

Verify Device integrity

//Check Device Integrity
public void deviceIntegrity(DecodeIntegrityTokenResponse response) {
    DeviceIntegrity deviceIntegrity = response.getTokenPayloadExternal().getDeviceIntegrity();
    if(!deviceIntegrity.getDeviceRecognitionVerdict().contains("MEETS_DEVICE_INTEGRITY")) {
        throw new Exception("Does not meet Device Integrity.");
        
    }
}

Similary you can verify the Nonce and App Package name with previously stored data in server

Craftwork answered 1/6, 2022 at 11:54 Comment(10)
It take me a while to find the place to download the '<Path to Signing certificate>\deployment_cert.der' file. You find it in the Google Play Console -> Project -> App integrity -> App signing (tab) -> Download certificat (top right)Fermentative
@Craftwork Where is nonce created - Frontend or Backend (Java)? What is the flow with Nonce creation?Joelynn
@UrvashiSoni Nonce should be created at backend. Please follow developer.android.com/google/play/integrity/verdict#nonce to generate nonce.Dinerman
@Dinerman I followed the same link. My project is in SpringBoot and the class IntegrityManager and its method setNonce() is unavailable. These are available for Android developers in com.google.android.gms package, which is not available for SpringBoot application.Joelynn
@UrvashiSoni setNonce will be invoked from android application only as Nonce generation and play integrity response verification will be done at backend.Dinerman
@Dinerman Thanks. I did nonce generation part, and shared the same to app. But, while decoding I am getting 403 Forbidden with message : "You are not authorized to decode the requested integrity token." Have u gone through this while your development?Joelynn
@UrvashiSoni For decoding response locally on your app server you will need response encryption keys. These keys you will get on play console under App Integrity sectionDinerman
need help - I get the error "404 Not Found - decodIntegrityToken was not found on this server" in the signed APk however it works very well in debug APK. The nonce is randomly generated with alphanumeric from my end.Hiddenite
@UrvashiSoni Nonce is unique idenfier for your request which may be encrypt of combination of Date,user Id or other information.Sarena
i did same integration like that. unfortunately i am getting "You are not authorized to decode the requested integrity token." error from google cloudGothard

© 2022 - 2024 — McMap. All rights reserved.