javaandroidgoogle-apiintegrity

Google PlayIntegrity API: a Nightmare


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


Solution

  • 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