【问题标题】:Google PlayIntegrity API: a NightmareGoogle PlayIntegrity API:一场噩梦
【发布时间】:2022-06-29 05:11:39
【问题描述】:

我需要一些帮助!我是一个自学成才的加密新手,在阅读、测试和错误两个多星期后,我发现了很少的人群知识,几乎没有来自 Google 的文档。

我正在尝试阅读完整性判决,我已经设法得到它IntegrityTokenRequest

    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??
        }
    }

根据文档,这一切都在 Play Console 中设置,并相应地创建了 Google Cloud 项目。现在出现了文档中的大漏洞:

a) JWT 有 4 个点,将 JWT 分为 5 个部分,而不是这里描述的 3 个部分 https://jwt.io/

b) Developer.Android.com 建议在 Google 服务器上进行解密和验证

我不知道如何或将要执行此命令... :-(

c) 如果我选择解密并验证返回的令牌,情况会更加复杂,因为我没有自己的安全服务器环境,只有我的应用和 Google Play 控制台。

d) 我在已下载的 Google Cloud Platform OAuth 2.0 Client IDs "Android client for com.company.project" JSON 文件中找到,但(再次)不知道如何在我的应用程序中使用它来获取来自 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
    }
}

我确定我错过了很多,请帮助

【问题讨论】:

标签: java android google-api integrity


【解决方案1】:

使用云服务器解码和验证令牌更好。 例如,如果您使用 Java 服务,那么下面的代码会将完整性令牌发送到 google 服务器,因此您可以验证响应。 针对该应用在 Google Cloud Platform 中启用 PlayIntegrity API 并下载 JSON 文件并在代码中进行配置。 同样,您应该在 Google PlayConsole 中针对应用启用 PlayIntegrity API 将 Google Play Integrity Client Library 添加到您的项目中

Maven 依赖

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

分级

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

令牌解码

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();

那么响应如下

{
"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"
    }
}
}

检查许可证

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

验证应用完整性

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;
}

验证设备完整性

//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.");
        
    }
}

类似地,您可以使用以前存储在服务器中的数据来验证 Nonce 和应用程序包名称

【讨论】:

    【解决方案2】:

    非常感谢 @John_S 的回答,我会将其标记为最终答案,无论如何我都会在此处发布所有缺失的部分,以供未来的开发人员使用,以便他们可以缩短我在这个问题上近一个月的时间,因为有Google PlayIntegrity API 没有完整的文档或 Java 示例(在撰写本文时)。

    首先,您需要在 Google Cloud 和 Google Play 中设置我们的项目,如@John_S 所述,但缺少的部分是您需要将凭据设置为“服务帐户”然后按照java.io.IOException: Error reading credentials from stream, 'type' field not specifiedhttps://developers.google.com/workspace/guides/create-credentials#android; 的描述“添加密钥”,然后,您可以使用您的凭据下载 .json 文件。我的问题中描述的 .json 文件无效,因为它必须具有如下结构:

        {  "type": "service_account",
           "project_id": "your-project",
           "private_key_id": "your-key-id",
           "private_key": "your-private-key",
           "client_email": "your-email@appspot.gserviceaccount.com",
           "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"
        }
    

    其次,下载有效的 .json 文件后,将其存储在“src/main/resources/credentials.json”中(如果需要,请创建新文件夹,而不是“res”文件夹),如此处所述Where must the client_secrets.json file go in Android Studio project folder tree?

    第三,要完成 build.gradle 的所有缺失部分,您必须包括:

    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'
    }
    

    并将它们导入您的项目

    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;
    

    那么,请求“Integrity Token”并解码的完整代码为:

        // 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
                    }
                }
        });
    

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-07-01
      • 1970-01-01
      • 2011-01-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-21
      • 2020-03-14
      相关资源
      最近更新 更多