Google PlayIntegrity API:一场噩梦

pva*_*lle 30 java android google-api integrity

我需要一些帮助!我是一个自学成才的加密新手,在阅读、测试和错误了两个多星期后如何解决这个问题,发现大众知识很少,几乎没有来自谷歌的文档。

我正在尝试阅读诚信判决,我已经设法做到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??
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

d) 我在我下载的 Google Cloud Platform OAuth 2.0 客户端 ID“Android client for com.company.project”JSON 文件中找到了它,但(再次)不知道如何在我的应用程序中使用它来从诚信令牌。

{"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
    }
}
Run Code Online (Sandbox Code Playgroud)

我确信我错过了很多,请帮忙

pva*_*lle 13

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

首先,您需要按照@John_S的说明在 Google Cloud 和 Google Play 中设置我们的项目,但缺少的部分是您需要将 Credential 设置为“ Service Account ”,然后按照java.io的描述设置“ Add Key ” .IOException:从流中读取凭据时出错,未指定“类型”字段,并且此https://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"
    }
Run Code Online (Sandbox Code Playgroud)

其次,下载有效的 .json 文件后,将其存储在“ src/main/resources/credentials.json ”中(如果需要,请创建新文件夹,而不是“res”文件夹),如此处所述client_secrets 必须在哪里。 json 文件位于 Android Studio 项目文件夹树中吗?

第三,要完成 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'
}
Run Code Online (Sandbox Code Playgroud)

并将它们导入到您的项目中

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;
Run Code Online (Sandbox Code Playgroud)

然后,请求“完整性令牌”并对其进行解码的完整代码为:

    // 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
                }
            }
    });
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助。


Joh*_*n_S 12

使用云服务器来解码和验证令牌效果更好。例如,如果您使用 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>
Run Code Online (Sandbox Code Playgroud)

摇篮

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

令牌解码

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();
Run Code Online (Sandbox Code Playgroud)

那么响应将如下

{
"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"
    }
}
}
Run Code Online (Sandbox Code Playgroud)

检查许可证

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

验证应用程序完整性

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;
}
Run Code Online (Sandbox Code Playgroud)

验证设备完整性

//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.");
        
    }
}
Run Code Online (Sandbox Code Playgroud)

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