このページでは、Android 5.0(API レベル 21)以降でサポートされている標準 API リクエストを実行して、完全性判定の結果を取得する方法について説明します。アプリがサーバー呼び出しを行うたびに、完全性判定の結果を取得する標準 API リクエストを実行し、やり取りが真正なものかどうかを確認できます。
概要
標準リクエストは以下の 2 つの部分で構成されます。
- 完全性トークン プロバイダを作成する(1 回限り): 完全性判定の結果を取得する前に、Integrity API を呼び出して完全性トークン プロバイダを準備する必要があります。これはたとえば、完全性判定の結果が必要になる前に、アプリの起動時にまたはバックグラウンドで行うことができます。
- 完全性トークンをリクエストする(オンデマンド): 真正なものかどうかを確認したいサーバー リクエストをアプリが実行するたびに、完全性トークンをリクエストして、複合と検証を行うためにアプリのバックエンド サーバーに送信します。これにより、バックエンド サーバーは処理方法を決定できます。
完全性トークン プロバイダを準備する(1 回限り):
- アプリが、Google Cloud プロジェクト番号を使用して完全性トークン プロバイダを呼び出します。
- アプリは、その後の証明書チェック呼び出しのために、完全性トークン プロバイダをメモリに保持します。
完全性トークンをリクエストする(オンデマンド):
- アプリが、保護する必要があるユーザー アクションについて、実行するリクエストのハッシュを計算します(SHA256 などの適切なハッシュ アルゴリズムを使用)。
- アプリが、完全性トークンをリクエストしてリクエスト ハッシュを渡します。
- アプリが、Play Integrity API から署名付きの暗号化された完全性トークンを受信します。
- アプリが、完全性トークンをアプリのバックエンドに渡します。
- アプリのバックエンドが、トークンを Google Play サーバーに送信します。Google Play サーバーが判定結果を復号して検証し、その結果をアプリのバックエンドに返します。
- アプリのバックエンドが、トークン ペイロードに含まれるシグナルに基づいて処理方法を決定します。
- アプリのバックエンドが、決定結果をアプリに送信します。
完全性トークン プロバイダを準備する(1 回限り)
Google Play から完全性判定の結果を取得する標準リクエストを実行する前に、完全性トークン プロバイダを準備(「ウォームアップ」)する必要があります。これにより、完全性判定の結果がリクエストされたときに、Google Play はクリティカル パスのレイテンシを短縮するためにデバイス上の部分的な証明書情報のスマート キャッシングを行うことができます。再度トークン プロバイダを準備すると、リソースの使用量が少ない完全性チェックを繰り返すことができ、次にリクエストした完全性判定の結果がより最新に近づきます。
完全性トークン プロバイダは次のタイミングで準備します。
- アプリの起動時(コールド スタート時など)。トークン プロバイダの準備は非同期で行われるため、起動時間には影響しません。このオプションは、アプリの起動直後(ユーザーがアプリにログインしたときやプレーヤーがゲームに参加したときなど)に完全性判定の結果をリクエストする場合に適しています。
- アプリが開いたとき(ウォーム スタート時など)。ただし、各アプリ インスタンスが完全性トークンを準備できるのは、1 分あたり 5 回までです。
- 完全性判定の結果をリクエストする前にトークンを準備する場合、バックグラウンドで随時。
完全性トークン プロバイダを準備する手順は次のとおりです。
- 以下の例に示すように、
StandardIntegrityManager
を作成します。 setCloudProjectNumber()
メソッドで Google Cloud プロジェクト番号を指定して、PrepareIntegrityTokenRequest
を作成します。- 作成したマネージャーを使用して
prepareIntegrityToken()
を呼び出し、PrepareIntegrityTokenRequest
を指定します。
Java
import com.google.android.gms.tasks.Task; // Create an instance of a manager. StandardIntegrityManager standardIntegrityManager = IntegrityManagerFactory.createStandard(applicationContext); StandardIntegrityTokenProvider integrityTokenProvider; long cloudProjectNumber = ...; // Prepare integrity token. Can be called once in a while to keep internal // state fresh. standardIntegrityManager.prepareIntegrityToken( PrepareIntegrityTokenRequest.builder() .setCloudProjectNumber(cloudProjectNumber) .build()) .addOnSuccessListener(tokenProvider -> { integrityTokenProvider = tokenProvider; }) .addOnFailureListener(exception -> handleError(exception));
Unity
IEnumerator PrepareIntegrityTokenCoroutine() { long cloudProjectNumber = ...; // Create an instance of a standard integrity manager. var standardIntegrityManager = new StandardIntegrityManager(); // Request the token provider. var integrityTokenProviderOperation = standardIntegrityManager.PrepareIntegrityToken( new PrepareIntegrityTokenRequest(cloudProjectNumber)); // Wait for PlayAsyncOperation to complete. yield return integrityTokenProviderOperation; // Check the resulting error code. if (integrityTokenProviderOperation.Error != StandardIntegrityErrorCode.NoError) { AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " + integrityTokenProviderOperation.Error); yield break; } // Get the response. var integrityTokenProvider = integrityTokenProviderOperation.GetResult(); }
ネイティブ
/// Initialize StandardIntegrityManager StandardIntegrityManager_init(/* app's java vm */, /* an android context */); /// Create a PrepareIntegrityTokenRequest opaque object. int64_t cloudProjectNumber = ...; PrepareIntegrityTokenRequest* tokenProviderRequest; PrepareIntegrityTokenRequest_create(&tokenProviderRequest); PrepareIntegrityTokenRequest_setCloudProjectNumber(tokenProviderRequest, cloudProjectNumber); /// Prepare a StandardIntegrityTokenProvider opaque type pointer and call /// StandardIntegrityManager_prepareIntegrityToken(). StandardIntegrityTokenProvider* tokenProvider; StandardIntegrityErrorCode error_code = StandardIntegrityManager_prepareIntegrityToken(tokenProviderRequest, &tokenProvider); /// ... /// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR if (error_code != STANDARD_INTEGRITY_NO_ERROR) { /// Remember to call the *_destroy() functions. return; } /// ... /// Use polling to wait for the async operation to complete. IntegrityResponseStatus token_provider_status; /// Check for error codes. StandardIntegrityErrorCode error_code = StandardIntegrityTokenProvider_getStatus(tokenProvider, &token_provider_status); if (error_code == STANDARD_INTEGRITY_NO_ERROR && token_provider_status == INTEGRITY_RESPONSE_COMPLETED) { /// continue to request token from the token provider } /// ... /// Remember to free up resources. PrepareIntegrityTokenRequest_destroy(tokenProviderRequest);
リクエストを改ざんから保護する(推奨)
Play Integrity API を使用してアプリ内のユーザー アクションをチェックする場合は、requestHash
フィールドを利用して改ざん攻撃を軽減できます。たとえば、プレーヤーのスコアをゲームのバックエンド サーバーにレポートするゲームで、プロキシ サーバーによってスコアが改ざんされていないことを確認したい場合があります。Play Integrity API は、requestHash
フィールドに設定した値を、署名付きの完全性レスポンス内で返します。requestHash
がなければ、完全性トークンはデバイスにのみバインドされ、特定のリクエストにはバインドされないため、攻撃を受ける可能性が生じます。requestHash
フィールドを効果的に利用する方法は次のとおりです。
完全性判定の結果をリクエストするとき:
- 実行中のユーザー アクションまたはサーバー リクエストから、すべての関連するリクエスト パラメータ(安定したリクエストのシリアル化の SHA256 など)のダイジェストを計算します。
requestHash
フィールドに設定される値の最大長は 500 バイトです。アプリ リクエストの重要なデータ、あるいはチェックまたは保護するアクションに関連するデータをすべてrequestHash
に追加します。requestHash
フィールドは完全性トークンにそのまま追加されるため、長い値によってリクエスト サイズが大きくなる可能性があります。 - ダイジェストを
requestHash
フィールドとして Play Integrity API に提供し、完全性トークンを取得します。
完全性判定の結果を受け取ったとき:
- 完全性トークンをデコードし、
requestHash
フィールドを抽出します。 - アプリと同じ方法で、リクエストのダイジェスト(安定したリクエストのシリアル化の SHA256 など)を計算します。
- アプリサイドのダイジェストとサーバーサイドのダイジェストを比較します。一致しない場合、リクエストは信頼できません。
完全性判定の結果をリクエストする(オンデマンド)
完全性トークン プロバイダを準備したら、Google Play に完全性判定の結果をリクエストできます。そのための手順は次のとおりです。
- 前述の方法で
StandardIntegrityTokenProvider
を取得します。 setRequestHash
メソッドにより、保護したいユーザー アクションのリクエスト ハッシュを指定して、StandardIntegrityTokenRequest
を作成します。StandardIntegrityTokenRequest
を指定して、完全性トークン プロバイダでrequest()
を呼び出します。
Java
import com.google.android.gms.tasks.Task; StandardIntegrityTokenProvider integrityTokenProvider; // See above how to prepare integrityTokenProvider. // Request integrity token by providing a user action request hash. Can be called // several times for different user actions. String requestHash = "2cp24z..."; Task<StandardIntegrityToken> integrityTokenResponse = integrityTokenProvider.request( StandardIntegrityTokenRequest.builder() .setRequestHash(requestHash) .build()); integrityTokenResponse .addOnSuccessListener(response -> sendToServer(response.token())) .addOnFailureListener(exception -> handleError(exception));
Unity
IEnumerator RequestIntegrityTokenCoroutine() { StandardIntegrityTokenProvider integrityTokenProvider; // See above how to prepare integrityTokenProvider. // Request integrity token by providing a user action request hash. Can be called // several times for different user actions. String requestHash = "2cp24z..."; var integrityTokenOperation = integrityTokenProvider.Request( new StandardIntegrityTokenRequest(requestHash) ); // Wait for PlayAsyncOperation to complete. yield return integrityTokenOperation; // Check the resulting error code. if (integrityTokenOperation.Error != StandardIntegrityErrorCode.NoError) { AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " + integrityTokenOperation.Error); yield break; } // Get the response. var integrityToken = integrityTokenOperation.GetResult(); }
ネイティブ
/// Create a StandardIntegrityTokenRequest opaque object. const char* requestHash = ...; StandardIntegrityTokenRequest* tokenRequest; StandardIntegrityTokenRequest_create(&tokenRequest); StandardIntegrityTokenRequest_setRequestHash(tokenRequest, requestHash); /// Prepare a StandardIntegrityToken opaque type pointer and call /// StandardIntegrityTokenProvider_request(). Can be called several times for /// different user actions. See above how to prepare token provider. StandardIntegrityToken* token; StandardIntegrityErrorCode error_code = StandardIntegrityTokenProvider_request(tokenProvider, tokenRequest, &token); /// ... /// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR if (error_code != STANDARD_INTEGRITY_NO_ERROR) { /// Remember to call the *_destroy() functions. return; } /// ... /// Use polling to wait for the async operation to complete. IntegrityResponseStatus token_status; /// Check for error codes. StandardIntegrityErrorCode error_code = StandardIntegrityToken_getStatus(token, &token_status); if (error_code == STANDARD_INTEGRITY_NO_ERROR && token_status == INTEGRITY_RESPONSE_COMPLETED) { const char* integrityToken = StandardIntegrityToken_getToken(token); } /// ... /// Remember to free up resources. StandardIntegrityTokenRequest_destroy(tokenRequest); StandardIntegrityToken_destroy(token); StandardIntegrityTokenProvider_destroy(tokenProvider); StandardIntegrityManager_destroy();
完全性判定の結果の復号と検証
完全性判定の結果をリクエストすると、暗号化されたレスポンス トークンが Play Integrity API によって提供されます。デバイスの完全性判定の結果を取得するには、Google のサーバーで完全性トークンを復号する必要があります。手順は次のとおりです。
- サービス アカウントを作成する Google Cloud プロジェクト内に表示されます
アプリのサーバーで、playintegrity スコープを使用してサービス アカウントの認証情報からアクセス トークンを取得し、次のリクエストを実行します。
playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
JSON レスポンスを読み取ります。
結果として返されるペイロードは、完全性判定の結果を含む書式なしテキストのトークンです。
リプレイから自動的に保護する
リプレイ攻撃を軽減するために、Google Play は、それぞれの完全性トークンが何度も再利用されないように自動的に確認します。同じトークンを繰り返し復号しようとすると、空の判定結果が返されます。