使用 Google PlayIntegrity API 解码完整性令牌

Decode integrity token using Google PlayIntegrity API

我正在尝试对我的 Android 应用程序实施 PlayIntegrity API,但我不知道如何使用 Google 的服务器解密和验证令牌。

到目前为止,我一直在关注 documentation

现在我一直在向 googleapis 发出解码请求。我不明白这条指令是如何工作的。

我创建了一个服务帐户并下载了 JSON 凭据文件并将其放入我的 Laravel 项目中,然后我尝试了这段代码:

$client = new Client();
$client->setAuthConfig(storage_path('app/integrity_check_account.json'));
$client->addScope(PlayIntegrity::class);
$httpClient = $client->authorize();

$result = $httpClient->request('POST', 'https://playintegrity.googleapis.com/v1/my.package.name', [
    'headers' => ['Content-Type' => 'application/json'],
    'body' => "{ 'integrity_token': 'token' }"
]);

dd($result);

所以这段代码有两个问题:

  1. 我添加的范围是否正确?
  2. 我的请求是否正确?因为它不工作,因为我收到 404 错误。

我从 Google APIs Client Library for PHP.

查看 PlayIntegrity API 的来源时终于找到了问题的解决方案

导入所需的依赖项后:

composer require google/apiclient:^2.12.1

这是我的控制器:

<?php

namespace App\Http\Controllers;

use Google\Client;
use Google\Service\PlayIntegrity;
use Google\Service\PlayIntegrity\DecodeIntegrityTokenRequest;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController {
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    public function performCheck(Request $request) {
        $client = new Client();
        $client->setAuthConfig(path/to/your/credentials/json/file.json);
        $client->addScope(PlayIntegrity::PLAYINTEGRITY);
        $service = new PlayIntegrity($client);
        $tokenRequest = new DecodeIntegrityTokenRequest();
        $tokenRequest->setIntegrityToken("TOKEN_HERE");
        $result = $service->v1->decodeIntegrityToken('PACKGE_NAME_HERE', $tokenRequest);
        //check result logic here
    }
}

感谢 post,我发现它很有用。

即便如此,我在解读判决书时仍然遇到了一些问题。这是我 运行 遇到的两个问题,我不确定问题是出在我如何在设备上调用 Play Integrity API 还是我如何解码响应令牌。

解码判定令牌时,如果出现错误代码为 400/Request contains an invalid argument 的异常,您可能需要使用 IntegrityTokenRequest_setCloudProjectNumber() (C++).

如果您在解码令牌时遇到异常,错误代码为 403/调用方没有权限,double-check 您设置了正确的 Google 云项目编号。

您必须在调用 Play Integrity API 之前获取访问令牌。 请参阅以下 2 个请求:

POST /token HTTP/1.1
Accept-Encoding: gzip, deflate
User-Agent: Google-HTTP-Java-Client/1.41.1 (gzip)
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: oauth2.googleapis.com
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: close
Content-Length: 811

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjVhY2Y5NjJkNDExZmZiZDE1NmIxZTE3ODcwY2Y0ZGExYjU0ZmM4MGIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImV4cCI6MTY0ODc3NjU2OCwiaWF0IjoxNjQ4NzcyOTY4LCJpc3MiOiJwbGF5LWludGVncml0eS1mZG5iLXRlc3RAZmRuYi1wbGF5LWludGVncml0eS10ZXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9hdXRoL3BsYXlpbnRlZ3JpdHkifQ.TQM6UFswVl1oe2JLDiPIjgoEyX89eefegh1EiAd3u8ZvO3STbp7g5rgUBC03_3jH0mLspZ4nbGH7m_8cKaYdKbyVs--P7Um591QU68FJxEvG0Nxr-8mjejo-mL4Z5bxXGVTVnd9n2hkWaBEe7iQ7dcqdkRHXNS1Tg2CcLWbCU1q0pxfAtAEe1mRXj5Y-VYfVl-PiN8Cl4Q8ZEbEAPyBkP-eqSMQcMA0nwhgsmIR4JxRH3zbef20SBuZgm0GBPsngUaseyvni-yjGcTmcyB5Sa1CSQL6-384016G9X7jIytF3fOY1pjl0L-N6KD6JmB4fC6ApDYqQmyZhfb5BD4nsjA

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Vary: X-Origin
Vary: Referer
Date: Fri, 01 Apr 2022 00:29:30 GMT
Server: scaffolding on HTTPServer2
Cache-Control: private
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
Content-Length: 1083

{"access_token":"ya29.c.b0AXv0zTNFkyzpv-uCAecXsZ8U1TelBGDjRVqBckImapqKoYukyNziQ_zsKecAIns4qjS6UeSiY9bSI3cysPbg7jjeBw63079wuKtsX25yDj83WSK2yzUPKev5MfoyJCyRmRmv-SMHYbqq2qQnn5SZiWM6lNV7hisch_s9JcSe3HmRS-ko9R670ywpgMIvzhADl5tSJlD0xwQyulrNRcJDNkNwzum0e-8........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................","expires_in":3599,"token_type":"Bearer"}

POST /v1/com.example.playinegrity:decodeIntegrityToken HTTP/1.1
Accept-Encoding: gzip, deflate
Authorization: Bearer ya29.c.b0AXv0zTNFkyzpv-uCAecXsZ8U1TelBGDjRVqBckImapqKoYukyNziQ_zsKecAIns4qjS6UeSiY9bSI3cysPbg7jjeBw63079wuKtsX25yDj83WSK2yzUPKev5MfoyJCyRmRmv-SMHYbqq2qQnn5SZiWM6lNV7hisch_s9JcSe3HmRS-ko9R670ywpgMIvzhADl5tSJlD0xwQyulrNRcJDNkNwzum0e-8........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
User-Agent: Google-API-Java-Client/1.33.1 Google-HTTP-Java-Client/1.41.1 (gzip)
x-goog-api-client: gl-java/1.8.0 gdcl/1.33.1 mac-os-x/11.6.2
Content-Type: application/json; charset=UTF-8
Content-Encoding: gzip
Host: playintegrity.googleapis.com
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: close
Content-Length: 712

[GZIP Content]

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Vary: X-Origin
Vary: Referer
Date: Fri, 01 Apr 2022 00:29:33 GMT
Server: ESF
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Cache-Control: private, proxy-revalidate
Connection: close
Content-Length: 649

{
  "tokenPayloadExternal": {
    "requestDetails": {
      "requestPackageName": "com.example.playinegrity",
      "timestampMillis": "1648699890779",
      "nonce": "YWJjZGVmZ2hpajEyMzQ1Njc4OTE="
    },
    "appIntegrity": {
      "appRecognitionVerdict": "UNRECOGNIZED_VERSION",
      "packageName": "com.example.playinegrity",
      "certificateSha256Digest": [
        "JAHNMZrOYvOOVQ40zNWm2e4fTmHIFYGo-_rvgk7vs4o"
      ],
      "versionCode": "1"
    },
    "deviceIntegrity": {
      "deviceRecognitionVerdict": [
        "MEETS_DEVICE_INTEGRITY"
      ]
    },
    "accountDetails": {
      "appLicensingVerdict": "UNEVALUATED"
    }
  }
}

如果您使用 java 代码开发应用程序(API 服务),则以下代码会将完整性令牌发送到 google 服务器,因此您可以验证响应。 在 Google Cloud Platform 中针对应用程序启用 PlayIntegrity API 并下载 JSON 文件并在代码中进行配置。 同样,您应该在 Google PlayConsole 中针对应用

启用 PlayIntegrity API

将 Google Play Integrity 客户端库添加到您的项目

Maven 依赖

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

令牌解码

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 和 App Package 名称