Android アプリに Play Integrity を追加する

1. はじめに

最終更新日: 2023 年 1 月 4 日

Play Integrity とは

Play Integrity API を使用すると、リスクが潜んでいる可能性のある不正な操作からアプリとゲームを保護できます。Play Integrity API を使用してアプリとデバイスの完全性判定の結果を取得することによって、不正行為、偽装、不正アクセスなどの攻撃や不正使用のリスクを軽減する適切な措置を講じられます。

以前のソリューション

Play Integrity API はアプリのライセンス ライブラリSafetyNet Attestation API という、これまでの 2 つのソリューションに代わるものです。新しいアプリの場合は Play Integrity が推奨されます。以前のソリューションを使用している既存のアプリの場合は、更新して Play Integrity を使用することをご検討ください。

パスを選択する

Play Integrity には、次のような異なる種類のアプリのライブラリ オプションが用意されています。

  • C/C++ で記述されたゲームやその他のアプリ
  • Kotlin または Java プログラミング言語で記述されたアプリ
  • Unity エンジンで開発されたゲーム

この Codelab には、この 3 つのオプションすべてのパスが含まれています。開発のニーズに適したパスを選択してください。この Codelab では、バックエンド サーバーの構築とデプロイを行います。このサーバーは 3 つのアプリのタイプすべてで共通です。

作成するアプリの概要

この Codelab では、Play Integrity をサンプルアプリに統合し、API を使用してデバイスとアプリの完全性を確認します。小規模なバックエンド サーバー アプリをデプロイして、それを使って完全性チェック プロセスをサポートします。

学習内容

  • Play Integrity ライブラリをアプリに統合する方法
  • Play Integrity API を使用して完全性チェックを実施する方法
  • サーバーを使用して完全性判定の結果を安全に処理する方法
  • 完全性判定の結果の見方

この Codelab では Play Integrity に特化して説明します。関連性のない概念やコードブロックについては説明しませんが、そのままコピーして貼り付けられるようにしています。

必要なもの

  • 有効な Android デベロッパー登録がされた、Google Play Console と Google Cloud Console にアクセスできる Google アカウント
  • C++ パスまたは Kotlin パスの場合は Android Studio 2021.1.1 以降
  • Unity パスの場合は Unity 2020 LTS 以降
  • 開発者向けオプションUSB デバッグが有効になっている、パソコンに接続された Android 搭載デバイス

この Codelab には、ビルドの署名と Google Play Console へのアップロードに関するリソースのリンクが含まれていますが、このプロセスについてある程度知識を持っていることを前提となります。

2. コードを取得する

Codelab プロジェクトは Git リポジトリから入手できます。リポジトリの親ディレクトリには、次の 4 つのディレクトリがあります。

  • server ディレクトリには、デプロイするサンプル サーバーのコードが含まれています。
  • cpp ディレクトリには、C++ のゲームやアプリに Play Integrity を追加するための Android Studio プロジェクトが含まれています。
  • kotlin ディレクトリには、標準の Android アプリに Play Integrity を追加するための Android Studio プロジェクトが含まれています。
  • unity ディレクトリには、Play Integrity を Unity プロジェクトに追加するための 2020 LTS バージョンの Unity エンジンで作成されたプロジェクトが含まれています。

各クライアント サンプル ディレクトリには、次の 2 つのサブディレクトリが含まれています。

  • start ディレクトリには、この Codelab で修正するプロジェクトのバージョンが含まれています。
  • final ディレクトリには、Codelab の完了時のプロジェクトの状態と同じプロジェクトのバージョンが含まれています。

リポジトリのクローンを作成する

コマンドラインから、ルート add-play-integrity-codelab ディレクトリを含めるディレクトリに変更し、次の構文を使用して GitHub からプロジェクトのクローンを作成します。

git clone https://github.com/android/add-play-integrity-codelab.git

依存関係を追加する(C++ のみ)

C++ パスを使用する場合は、リポジトリのサブモジュールを初期化して Dear ImGui ライブラリをセットアップする必要があります。これはユーザー インターフェースで使用されます。コマンドラインから次の手順で行います。

  1. 作業ディレクトリを add-play-integrity-codelab/cpp/start/third-party に変更します。
  2. git submodule update --init

3. クライアントとサーバーの機能を理解する

Play Integrity セキュリティ モデルの重要な要素は、デバイスから検証オペレーションを自分が制御している安全なサーバーに移行することです。サーバーでこのオペレーションを行うことにより、不正使用されたデバイスによるリプレイ攻撃やメッセージの内容の改ざんなどの場合に保護できます。

Codelab サンプル サーバーは一例として作成されています。わかりやすくするために本番環境では備えておいた方がよい多くの機能は含まれていません。生成された値のリストはメモリにのみ保存され、永続ストレージによってバッキングされていません。サーバー エンドポイントは認証を必要とする設定にはなっていません。

performCommand エンドポイント

サーバーは HTTP POST リクエストを介してアクセスされる /performCommand エンドポイントを提供します。リクエスト本文は、次の Key-Value ペアを含む JSON ペイロードである必要があります。

{
   "commandString": "command-here",
   "tokenString": "token-here"
}

/performCommand エンドポイントは次の Key-Value ペアを含む JSON ペイロードを返します。

{
   "commandSuccess": true/false,
   "diagnosticMessage": "summary text",
   "expressToken": "token-here"
}

commandString パラメータの実際のコンテンツは重要ではありません。サーバーは Play Integrity 使用時に commandString の完全性を検証しますが、コマンド値は使用しません。すべてのクライアント バージョンで、値 "TRANSFER FROM alice TO bob CURRENCY gems QUANTITY 1000" が使用されます。

tokenString パラメータの値は次のいずれかである必要があります。

  • Play Integrity API によって生成されたトークン
  • 正常に行われた前回の /performCommand 呼び出しで返されたエクスプレス トークン

サーバーは Play Integrity API から取得したトークンを復号して検証します。復号結果は完全性シグナルの JSON ペイロードになります。シグナルの値に応じて、サーバーはコマンドを承認するか、拒否するかを選択できます。トークンが正常に復号されると、diagnosticMessage にシグナルの概要説明が返されます。通常、本番環境のアプリではこの情報をクライアントに返しませんが、Codelab クライアントではこれを使用して、サーバーログを確認することなく、オペレーションの結果を表示します。トークンの処理中にエラーが発生すると、diagnosticMessage でエラーが返されます。

ノンスの生成

Play Integrity リクエストを実行するには、ノンスを生成してリクエストに関連付ける必要があります。ノンスを使用することによって、完全性リクエストが固有で、1 回のみ処理されるようにします。また、ノンスを使って、完全性リクエストに関連付けられたメッセージ コンテンツが改ざんされていないことも確認できます。Play Integrity のノンスについて詳しくは、こちらのドキュメントをご覧ください。

この Codelab では次の 2 つの値を組み合わせてノンスを生成します。

  • 暗号で保護された乱数ジェネレータで生成した 128 ビットの乱数
  • commandString 値の SHA-256 ハッシュ

Play Integrity API では、ノンスは URL エンコードされたパディングされていない Base64 文字列であることが求められます。ノンス文字列を作成するために、この Codelab では乱数とハッシュ値のバイト配列を 16 進文字列に変換して連結します。生成された文字列は有効な Base64 文字列ですが、エンコードもデコードもされていません。

Codelab クライアントは、HTTP GET リクエストを使って、サーバー上で /getRandom エンドポイントを呼び出して、乱数を取得します。この方法は、コマンド リクエストで使用された乱数のソースであることをサーバーが確認できるため、最も安全なランダム生成方法です。ただし、この方法はサーバーへの追加のラウンドトリップが発生します。クライアントは乱数を自ら生成することで、セキュリティを天秤にかけたうえで、このラウンドトリップを回避できます。

エクスプレス トークン

PIA の呼び出しは高コストであるため、サーバーはエクスプレス トークンも提供しています。これは高セキュリティではないものの低コストで認証を可能にする代替手段です。エクスプレス トークンは完全性チェックに合格し /serverCommand の呼び出しに成功すると expressToken フィールドに返されます。Play Integrity API の呼び出しは計算コストが高く、価値の高いオペレーションの保護に使用することを想定しています。エクスプレス トークンを返すことにより、Play Integrity API で完全な検証を行うほど重要ではないオペレーションや頻度が多すぎるオペレーションを実行する際の低セキュリティの認証が可能になります。/serverCommand を呼び出すときに Play Integrity トークンの代わりにエクスプレス トークンを使用できます。各エクスプレス トークンは 1 回しか使用できません。有効なエクスプレス トークンを使用した /serverCommand の呼び出しが成功すると、新しいエクスプレス トークンが返されます。

Codelab の実装では、エクスプレス トークンはサーバーで一意に生成されるため、コマンドは引き続きリプレイ攻撃から保護されます。ただし、エクスプレス トークンではコマンド変更に対するハッシュ保護が省略されており、Play Integrity API の最初の呼び出し後に発生したデバイス変更を検出することができないため、安全性が低くなります。

Codelab サーバー アーキテクチャ

この Codelab では、Ktor フレームワークを使用して Kotlin で記述された付属のサンプル サーバー プログラムを使用して、サーバーをインスタンス化できます。このプロジェクトには、Google Cloud App Engine にデプロイするための構成ファイルが含まれています。この Codelab の手順では App Engine のビルドとデプロイについて説明します。別のクラウド プロバイダを使用している場合、Ktor は複数のクラウド サービスでのデプロイ手順を用意しています。必要に応じてプロジェクトを修正し、任意のサービスをデプロイしてください。独自のローカル サーバー インスタンスにデプロイすることもできます。

サーバーでのサンプルコードの使用は必須ではありません。使い慣れたウェブ アプリケーション フレームワークがある場合は、Codelab サンプル サーバーを参照しながら、独自のフレームワークに /getRandom エンドポイントと /performCommand エンドポイントを実装できます。

クライアントの設計

C++、Kotlin、Unity エンジンの 3 つのクライアント バージョンはすべて、ユーザー インターフェースが同じです。

[Request random] ボタンは、サーバーの /getRandom エンドポイントを呼び出します。結果はテキスト フィールドに表示されます。これを使用して、Play Integrity 統合を追加する前に、サーバーの接続と動作を確認できます。

[Call server with integrity check] ボタンは、この Codelab の開始時は機能しません。次の手順でコードを追加します。

  • /getRandom を呼び出し、乱数を取得
  • ノンスを生成
  • ノンスを使用して Play Integrity リクエストを作成
  • Play Integrity リクエストで生成されたトークンを使用して /performCommand を呼び出し
  • /performCommand の結果を表示

コマンドの結果はテキスト フィールドに表示されます。コマンドが正常に完了すると、こちらには Play Integrity チェックによって返されたデバイス判定結果の概要が表示されます。

[Call server with express token] ボタンは、[Call server with integrity check] オペレーションが正常に完了すると表示されます。先ほどの /performCommand のエクスプレス トークンを使用して /performCommand を呼び出します。テキスト フィールドにコマンドが正常に完了したか、失敗したかが表示されます。返されたエクスプレス トークンの値は、乱数に使用されたテキスト フィールドに表示されます。

Play Integrity API 関数はエラーコードを返して、エラーを報告します。このようなエラーコードの詳細については、Play Integrity のドキュメントをご覧ください。不安定なインターネット接続や過負荷のデバイスといった環境条件が原因で、エラーが発生することがあります。そのようなエラーが発生した場合は、指数バックオフによる再試行オプションの追加をご検討ください。この Codelab では、Play Integrity エラーは Logcat に出力されます。

4. Google Cloud App Engine をセットアップする

Google Cloud を使用するには、次の手順に沿って操作します。

  1. Google Cloud Platform に登録します。Google Play Console に登録している Google アカウントと同じアカウントを使用します。
  2. 請求先アカウントを作成します。
  3. Google Cloud SDK をインストールして初期化します。

新しくインストールした Google Cloud CLI を使用して、次のコマンドを実行します。

  1. gcloud components install app-engine-java で Java 用の App Engine 拡張機能をインストールします。
  2. gcloud projects create $yourprojectname --set-as-default コマンドの一意の識別子を $yourname に置き換えて、新しい Cloud プロジェクトを作成します。
  3. gcloud app create で Cloud プロジェクトに App Engine アプリを作成します。

5. サーバーをデプロイする

アプリのパッケージ名を設定する

このステップでは、アプリのパッケージ名をサーバーコードに追加します。テキスト エディタで ValidateCommand.kt ソースファイルを開きます。このファイルは次のディレクトリに保存されています。

add-play-integrity-codelab/server/src/main/kotlin/com/google/play/integrity/codelab/server/util

次の行を見つけ、プレースホルダ テキストを一意のパッケージ ID に置き換えて、ファイルを保存します。

const val APPLICATION_PACKAGE_IDENTIFIER = "com.your.app.package"

この ID は後ほど Google Play Console にアプリをアップロードする前に、クライアント プロジェクトに設定します。

サーバーをビルドして App Engine にデプロイする

Google Cloud CLI を使用して add-play-integrity/server ディレクトリから次のコマンドを実行し、サーバーをビルドしてデプロイします。

Linux または macOS の場合:

./gradlew appengineDeploy

Microsoft Windows の場合:

gradlew.bat appengineDeploy

正常に完了したデプロイで出力されたデプロイ済みサービスの場所を書き留めます。この URL はサーバーと通信するためにクライアントを設定する際に必要となります。

デプロイを確認する

Google Cloud CLI を使用して次のコマンドを実行すると、サーバーが正常に動作していることを確認できます。

gcloud app browse

このコマンドを実行すると、ウェブブラウザとルート URL が開きます。サンプル サーバーにルート URL からアクセスすると、Hello World! というメッセージが表示されます。

6. Google Play Console でアプリを設定する

Google Play Console でアプリの完全性を設定する

Google Play Console に既存のアプリエントリがある場合は、この Codelab で使用できます。または、手順に沿って Google Play Console で新しいアプリを作成します。Google Play Console でアプリを選択または作成したうえで、アプリの完全性を設定する必要があります。Google Play Console の左側のメニューで、[リリース] セクションの [アプリの完全性] を選択します。

[Cloud プロジェクトをリンク] ボタンをクリックします。サーバーで使用した Google Cloud プロジェクトを選択し、[プロジェクトをリンク] ボタンをクリックします。

Google Cloud によるサーバーへのアクセス

バックエンド サーバーでは、Play Integrity API によってクライアントで生成された完全性トークンを復号する必要があります。Play Integrity では 2 種類の鍵の管理方法から選べます。Google が生成し管理する鍵と、デベロッパーが提供する鍵です。この Codelab では、Google が管理する鍵の推奨されるデフォルト動作を使用します。

Google 管理の鍵を使用した場合、バックエンド サーバーは暗号化された完全性トークンを復号のために Google Play サーバーに渡します。Codelab サーバーは Google API クライアント ライブラリを使用して、Google Play サーバーと通信します。

これでサーバーが稼働を開始し、Google Play Console でのアプリの設定が完了しました。続いて、選択したプラットフォームに対応するクライアントをカスタマイズしていきます。特定のプラットフォーム向けのステップはすべてまとめて記載しているため、使用しないプラットフォームの手順はスキップしてください。

7. クライアントをビルドして実行する(C++)

Android Studio を実行します。[Welcome to Android Studio] ウィンドウで [Open] ボタンをクリックし、add-play-integrity-codelab/cpp/start にある Android Studio プロジェクトを開きます。

アプリケーション ID を更新する

ビルドを Google Play にアップロードする前に、アプリケーション ID をデフォルトのものから一意のものに変更する必要があります。次の手順で行います。

  1. Android Studio の [Project] ペインで、start/app の下にある build.gradle ファイルを見つけて開きます。
  2. applicationId ステートメントを見つけます。
  3. com.google.play.integrity.codelab.cpp をサーバーのデプロイ時に選んだパッケージ名に変更し、ファイルを保存します。
  4. ファイルの上に、Gradle ファイルの変更を通知するバナーが表示されます。[Sync Now] をクリックして、ファイルを再読み込みし再同期します。
  5. Android Studio の [Project] ペインで、start/app/src/main の下にある AndroidManifest.xml ファイルを開きます。
  6. package="com.example.google.codelab.playintegritycpp" ステートメントを見つけます。
  7. com.example.google.codelab.playintegritycpp を固有のパッケージ名に置き換えて、ファイルを保存します。
  8. Android Studio の [Project] ペインで、start/app/src/main/java/com.example.google.codelab.playintegritycpp の下にある PlayIntegrityCodelabActivity ファイルを開きます。
  9. package com.example.google.codelab.playintegritycpp ステートメントを見つけます。
  10. com.example.google.codelab.playintegritycpp を固有のパッケージ名に置き換えます。
  11. 新しいパッケージ名を右クリックし、[Show Context Actions] を選択します。
  12. [Move to(新しいパッケージ名)] を選択します。
  13. 表示されている場合は、ファイルの上の [Sync Now] ボタンをクリックします。

サーバーの URL を更新する

プロジェクトを更新して、サーバーをデプロイした URL の場所を指すようにする必要があります。

  1. Android Studio の [Project] ペインで、start/app/src/main/cpp の下にある server_urls.hpp ファイルを開きます。
  2. サーバーのデプロイ時に表示されたルート URL を GET_RANDOM_URL 定義と PERFORM_COMMAND_URL 定義に追加し、ファイルを保存します。

結果は次のようになります。

constexpr char GET_RANDOM_URL[] = "https://your-play-integrity-server.uc.r.appspot.com/getRandom";
constexpr char PERFORM_COMMAND_URL[] = "https://your-play-integrity-server.uc.r.appspot.com/performCommand";

実際の URL はプロジェクト名と、サーバーのデプロイに使用した Google Cloud リージョンによって異なります。

ビルドして実行する

開発用に構成された Android デバイスを接続します。Android Studio でプロジェクトをビルドし、接続済みのデバイスで実行します。アプリには次のように表示されます。

429ccc112f78d454.png

[Request Random] ボタンをタップしてコードを実行し、サーバーに HTTP リクエストを送信して、乱数をリクエストします。しばらくすると、画面に乱数が表示されます。

62acee42ba1fa80.png

エラー メッセージが表示された場合は、[Logcat] ペインの出力に詳細が記載されている場合があります。

正常に乱数値を取得でき、サーバーと通信できていることを確認したら、Play Integrity API の統合を開始します。

8. Play Integrity をプロジェクトに追加する(C++)

SDK をダウンロードする

Play Core SDK をダウンロードして展開する必要があります。次の手順を行います。

  1. Play Core Native SDK ページからから ZIP ファイルに入った Play Core SDK をダウンロードします。
  2. ZIP ファイルを展開します。
  3. 新しく展開したディレクトリの名前が play-core-native-sdk になっていることを確認し、そのディレクトリを add-play-integrity-codelab/cpp/start ディレクトリにコピーまたは移動します。

build.gradle を更新する

Android Studio の [Project] ペインで、start/app ディレクトリ内のモジュール レベルの build.gradle ファイルを開きます。

apply plugin: 'com.android.application' 行の下に次の行を追加します。

def playcoreDir = file("../play-core-native-sdk")

defaultConfig ブロックで externalNativeBuild ブロックを見つけ、cmake ブロック内の arguments ステートメントを次のように変更します。

                arguments "-DANDROID_STL=c++_shared",
                          "-DPLAYCORE_LOCATION=$playcoreDir"

android ブロックの末尾に次のコードを追加します。

    buildTypes {
        release {
            proguardFiles getDefaultProguardFile("proguard-android.txt"),
                          "proguard-rules.pro",
                          "$playcoreDir/proguard/common.pgcfg",
                          "$playcoreDir/proguard/integrity.pgcfg"
        }
    }

dependencies ブロックの末尾に次の行を追加します。

    implementation files("$playcoreDir/playcore.aar")

変更内容を保存します。ファイルの上に、Gradle ファイルの変更を通知するバナーが表示されます。[Sync Now] をクリックして、ファイルを再読み込みし再同期します。

CMakeList.txt を更新する

Android Studio の [Project] ペインで、start/app/src/main/cpp ディレクトリ内の CMakeLists.txt ファイルを開きます。

find_package コマンドの下に次の行を追加します。

include("${PLAYCORE_LOCATION}/playcore.cmake")
add_playcore_static_library()

target_include_directories(game PRIVATE 行を見つけ、その下に次の行を追加します。

        ${PLAYCORE_LOCATION}/include

target_link_libraries(game 行を見つけ、その下に次の行を追加します。

        playcore

ファイルを保存します。ファイルの上に、外部ビルドファイルが変更されたことを通知するバナーが表示されます。[Sync Now] をクリックして、ファイルを再読み込みし再同期します。

[Build] メニューから [Make Project] を選択し、プロジェクトが正常にビルドされことを確認します。

9. 完全性リクエストを送信する(C++)

アプリは Play Integrity API を使用してトークンをリクエストし、完全性情報を取得します。その後このトークンをサーバーに送信して、復号と検証を行います。これからプロジェクトにコードを追加して Play Integrity API を初期化し、それを使用して完全性リクエストを送信します。

コマンドボタンを追加する

実際のアプリまたはゲームでは、ストアでの購入やマルチプレーヤー ゲーム セッションへの参加など、特定のアクティビティの前に完全性チェックを実施します。この Codelab ではボタンを UI に追加して、完全性チェックを手動でトリガーし、サーバーを呼び出して、生成された Play Integrity トークンを渡せるようにします。

Codelab プロジェクトには、client_manager.cpp ソースファイルと client_manager.hpp ソースファイルで定義された ClientManager クラスが含まれています。便宜上、このファイルはすでにプロジェクトに追加されていますが、実装コードは含まれていないため、これから追加します。

UI ボタンを追加するには、Android Studio の [Project] ペインで、start/app/src/main/cpp ディレクトリにある demo_scene.cpp ファイルを開きます。まず、空の DemoScene::GenerateCommandIntegrity() 関数を見つけて、次のコードを追加します。

    const auto commandResult =
            NativeEngine::GetInstance()->GetClientManager()->GetOperationResult();
    if (commandResult != ClientManager::SERVER_OPERATION_PENDING) {
        if (ImGui::Button("Call server with integrity check")) {
            DoCommandIntegrity();
        }
    }

次に、空の DemoScene::DoCommandIntegrity() 関数を見つけます。次のコードを追加します。

    ClientManager *clientManager = NativeEngine::GetInstance()->GetClientManager();
    clientManager->StartCommandIntegrity();
    mServerRandom = clientManager->GetCurrentRandomString();

ファイルを保存します。次にサンプルの ClientManager クラスを更新して、実際の Play Integrity 機能を追加します。

マネージャー ヘッダー ファイルを更新する

Android Studio の [Project] ペインで、start/app/src/main/cpp ディレクトリにある client_manager.hpp ファイルを開きます。

#include "util.hpp" 行の下に次の行を追加して、Play Integrity API のヘッダー ファイルを含めます。

#include "play/integrity.h"

ClientManager クラスは IntegrityTokenRequest オブジェクトと IntegrityTokenResponse オブジェクトへの参照を保持する必要があります。ClientManager クラス定義の一番下に次の行を追加します。

    IntegrityTokenRequest *mTokenRequest;
    IntegrityTokenResponse *mTokenResponse;

ファイルを保存します。

Play Integrity を初期化してシャットダウンする

Android Studio の [Project] ペインで、start/app/src/main/cpp ディレクトリにある client_manager.cpp ファイルを開きます。

ClientManager::ClientManager() コンストラクタを見つけます。mInitialized = false; ステートメントを次のコードに置き換えます。

    mTokenRequest = nullptr;
    mTokenResponse = nullptr;

    const android_app *app = NativeEngine::GetInstance()->GetAndroidApp();
    const IntegrityErrorCode errorCode = IntegrityManager_init(app->activity->vm,
                                                               app->activity->javaGameActivity);
    if (errorCode == INTEGRITY_NO_ERROR) {
        mInitialized = true;
    } else {
        mInitialized = false;
        ALOGE("Play Integrity initialization failed with error: %d", errorCode);
        ALOGE("Fatal Error: Play Integrity is unavailable and cannot be used.");
    }

ClientManager::~ClientManager() デストラクタに次のコードを追加します。

    if (mInitialized) {
        IntegrityManager_destroy();
        mInitialized = false;
    }

完全性トークンをリクエストする

Play Integrity API からの完全性トークンのリクエストは非同期処理です。トークン リクエスト オブジェクトを作成し、それにノンス値を割り当て、トークン リクエストを行う必要があります。そのために、空の ClientManager::StartCommandIntegrity() 関数に次のコードを追加します。

    // Only one request can be in-flight at a time
    if (mStatus != CLIENT_MANAGER_REQUEST_TOKEN) {
        mResult = SERVER_OPERATION_PENDING;
        // Request a fresh random
        RequestRandom();
        if (mValidRandom) {
            GenerateNonce();
            IntegrityTokenRequest_create(&mTokenRequest);
            IntegrityTokenRequest_setNonce(mTokenRequest, mCurrentNonce.c_str());

            const IntegrityErrorCode errorCode =
                    IntegrityManager_requestIntegrityToken(mTokenRequest, &mTokenResponse);
            if (errorCode != INTEGRITY_NO_ERROR) {
                // Log the error, in a real application, for potentially
                // transient errors such as network connectivity, you should
                // add retry with an exponential backoff
                ALOGE("Play Integrity returned error: %d", errorCode);
                CleanupRequest();
                mStatus = CLIENT_MANAGER_IDLE;
            } else {
                mStatus = CLIENT_MANAGER_REQUEST_TOKEN;
            }
        }
    }

トークン リクエストは非同期処理であるため、完了を確認する必要があります。ClientManager クラスには、アプリ アップデート ループの一環で呼び出される Update() 関数があります。次のコードを ClientManager::Update() 関数に追加して、トークン リクエストのステータスを確認し、完了後に結果を処理します。

    if (mStatus == CLIENT_MANAGER_REQUEST_TOKEN) {
        IntegrityResponseStatus responseStatus = INTEGRITY_RESPONSE_UNKNOWN;
        const IntegrityErrorCode errorCode =
                IntegrityTokenResponse_getStatus(mTokenResponse, &responseStatus);
        if (errorCode != INTEGRITY_NO_ERROR) {
            // Log the error, in a real application, for potentially
            // transient errors such as network connectivity, you should
            // add retry with an exponential backoff
            ALOGE("Play Integrity returned error: %d", errorCode);
            CleanupRequest();
            mStatus = CLIENT_MANAGER_IDLE;
        } else if (responseStatus == INTEGRITY_RESPONSE_COMPLETED) {
            std::string tokenString = IntegrityTokenResponse_getToken(mTokenResponse);
            SendCommandToServer(tokenString);
            CleanupRequest();
            mStatus = CLIENT_MANAGER_RESPONSE_AVAILABLE;
        }
    }

リクエスト オブジェクトをクリーンアップする

トークン リクエスト オブジェクトとレスポンス オブジェクトの処理が完了したら、Play Integrity API に通知して、それらを破棄しリソースを再利用できるようにする必要があります。次のコードを ClientManager::CleanupRequest() 関数に追加します。

    if (mTokenResponse != nullptr) {
        IntegrityTokenResponse_destroy(mTokenResponse);
        mTokenResponse = nullptr;
    }
    if (mTokenRequest != nullptr) {
        IntegrityTokenRequest_destroy(mTokenRequest);
        mTokenRequest = nullptr;
    }

[Build] メニューから [Make Project] を選択し、プロジェクトが正常にビルドされることを確認します。

10. トークンをサーバーに送信する(C++)

次に完全性トークンを含むサーバーにコマンドを送信するコードを追加します。また、結果を処理するコードも追加します。

次のコードを ClientManager::SendCommandToServer() 関数に追加します。

// Note that for simplicity, we are doing HTTP operations as
// synchronous blocking instead of managing them from a
// separate network thread
HTTPClient client;
std::string errorString;

// Manually construct the json payload for ServerCommand
std::string payloadString = COMMAND_JSON_PREFIX;
payloadString += TEST_COMMAND;
payloadString += COMMAND_JSON_TOKEN;
payloadString += token;
payloadString += COMMAND_JSON_SUFFIX;

auto result = client.Post(PERFORM_COMMAND_URL, payloadString, &errorString);
if (!result) {
   ALOGE("SendCommandToServer Curl reported error: %s", errorString.c_str());
   mResult = SERVER_OPERATION_NETWORK_ERROR;
} else {
   ALOGI("SendCommandToServer result: %s", (*result).c_str())
   // Preset to success, ParseResult will set a failure result if the parsing
   // errors.
   mResult = SERVER_OPERATION_SUCCESS;
   ParseResult(*result);
}

次のコードを ClientManager::ParseResult() 関数に追加します。

    bool validJson = false;
    JsonLookup jsonLookup;
    if (jsonLookup.ParseJson(resultJson)) {
        // Look for all of our needed fields in the returned json
        auto commandSuccess = jsonLookup.GetBoolValueForKey(COMMANDSUCCESS_KEY);
        if (commandSuccess) {
            auto diagnosticString = jsonLookup.GetStringValueForKey(DIAGNOSTICMESSAGE_KEY);
            if (diagnosticString) {
                auto expressString = jsonLookup.GetStringValueForKey(EXPRESSTOKEN_KEY);
                if (expressString) {
                    if (*commandSuccess) {
                        // Express token only valid if the server reports the command succeeded
                        mValidExpressToken = true;
                    } else {
                        mValidExpressToken = false;
                        mResult = SERVER_OPERATION_REJECTED_VERDICT;
                    }
                    mCurrentSummary = *diagnosticString;
                    mCurrentExpressToken = *expressString;
                    validJson = true;
                }
            }
        }
    }
    if (!validJson) {
        mResult = SERVER_OPERATION_INVALID_RESULT;
    }

次に署名済みの App Bundle を生成して Google Play Console にアップロードし、アプリをテストします。

11. ビルドしてアップロードする(C++)

アプリのキーストアを作成してセットアップする

Android では、すべてのアプリはデバイスにインストールまたは更新される前に証明書でデジタル署名されている必要があります。

この Codelab では、アプリのキーストアを作成します。既存のゲームのアップデートを公開する場合は、アプリの以前のバージョンをリリースしたときと同じキーストアを再度使用します。

キーストアを作成してリリース App Bundle をビルドする

Android Studio でのキーストアの手順に沿ってキーストアを作成し、それを使用してゲームの署名付きリリースビルドを生成します。Android Studio で [Build] メニューから [Generate Signed Bundle / APK] を選択し、ビルドプロセスを開始します。[Android App Bundle] か [APK] を選択するよう求めるメッセージが表示されたら、[App Bundle] オプションを選択します。このプロセスが完了すると、Google Play Console へのアップロードに適した .aab ファイルが作成されます。

Google Play Console にアップロードする

App Bundle ファイルを作成したら、Google Play Console にアップロードします。内部テストトラックを使用して、ビルドにすばやくアクセスできるようにすることをおすすめします。

テストビルドを実行する

テストビルドを Google Play ストアからダウンロードして実行します。このケースでは、関数にプレースホルダの成功コードが含まれており、サーバーに完全性トークンを送信するため、完全性チェックは正常に開始し、次のような結果が画面に表示されます。

ef5f55d73f808791.png

これで Play Integrity を C++ アプリに統合できました。続けてその他のクライアントの例を確認するか、この Codelab の最後までスキップしてください。

12. クライアントをビルドして実行する(Unity)

Codelab の Unity プロジェクトは Unity 2020 LTS(2020.3.31f1)を使用して作成されていますが、上位バージョンの Unity とも互換性があります。Unity 用 Play Integrity プラグインは、Unity 2018 LTS 以降と互換性があります。

プロジェクトのセットアップ

次の手順で行います。

  1. Unity Hub または Unity エディタで、add-play-integrity-codelab/unity/start にある Unity プロジェクトを開きます。
  2. プロジェクトが読み込まれたら、Unity の [File] メニューから [Build Settings...] を選択します。
  3. [Build Settings] ウィンドウで、プラットフォームを [Android] に変更します。
  4. [Build Settings] ウィンドウで、[Player Settings...] ボタンをクリックします。
  5. [Project Settings] ウィンドウで、[Player] カテゴリを選択し、[Settings for Android] セクションを見つけます。[Other Settings] リストを開きます。

b994587b808c7be4.png

  1. [Identification] の下にある [Package Name] を見つけます。

d036e5be73096083.png

  1. パッケージ名をサーバーのデプロイ時に選択した ID に変更します。
  2. [Project Settings] ウィンドウを閉じます。
  3. Unity の [File] メニューから [Save Project] をクリックします。

サーバー URL を更新する

プロジェクトを更新して、サーバーをデプロイした URL の場所を指すようにする必要があります。手順は次のとおりです。

  1. IDE またはテキスト エディタで、Scripts フォルダの PlayIntegrityController.cs ファイルを開きます。
  2. URL_GETRANDOM 変数と URL_PERFORMCOMMAND 変数の値を変更して、サーバーを指定するようにします。
  3. ファイルを保存します。

結果は次のようになります。

    private readonly string URL_GETRANDOM = "https://your-play-integrity-server.uc.r.appspot.com/getRandom";
    private readonly string URL_PERFORMCOMMAND = "https://your-play-integrity-server.uc.r.appspot.com/performCommand";

実際の URL はプロジェクト名と、サーバーのデプロイに使用した Google Cloud リージョンによって異なります。

サーバー機能をテストする

サーバー機能をテストするには、Unity エディタでプロジェクトを実行します。次の手順で行います。

  1. Scenes フォルダにある SampleScene シーンファイルを開きます。
  2. エディタで [Play] ボタンをクリックします。
  3. [Game] ディスプレイの [Request Random] ボタンをクリックします。

しばらくすると、次のように乱数が画面に表示されます。

f22c56cdd2e56050.png

13. Play Integrity プラグインをプロジェクトに追加する(Unity)

プラグインをダウンロードする

ウェブブラウザで、Play Unity Plugin GitHub リポジトリのリリースページを開きます。プラグインの最新リリースを使用します。com.google.play.integrity-<version> をダウンロードします。[Assets] リストで、Play Integrity の unitypackage ファイルを選択します。

プラグインをインストールする

Unity エディタのメニューバーで、[Assets] -> [Import Package] -> [Custom Package...] の順にクリックし、ダウンロードした .unitypackage ファイルを開きます。[Import Unity Package] ウィンドウが表示されたら、[Import] ボタンをクリックします。

このプラグインには、Unity 用 External Dependency Manager(EDM4U)が含まれています。EDM4U は Play Integrity の使用で必要な Java コンポーネントの依存関係解決の自動化を実装します。依存関係解決の自動化を有効にするように求められたら、[Enable] ボタンをクリックします。

5bf0be9139fab036.png

解決の自動化を使用することを強くおすすめします。依存関係の問題はプロジェクトのビルドまたは実行の失敗につながる可能性があります。

14. 完全性リクエストを送信する(Unity)

完全性リクエストを作成する

完全性リクエストを作成するには、次の手順に沿って行います。

  1. IDE またはテキスト エディタで、Scripts フォルダの PlayIntegrityController.cs ファイルを開きます。
  2. ファイルの一番上にある using ステートメントのブロックに次の行を追加します。
using Google.Play.Integrity;
  1. RunIntegrityCommand() 関数を見つけ、yield return null; ステートメントを次のコードに置き換えます。
        // Call our server to retrieve a random number.
        yield return GetRandomRequest();
        if (!string.IsNullOrEmpty(_randomString))
        {
            // Create an instance of an integrity manager.
            var integrityManager = new IntegrityManager();

            // Request the integrity token by providing a nonce.
            var tokenRequest = new IntegrityTokenRequest(GenerateNonceString(_randomString,
                TEST_COMMAND));
            var requestIntegrityTokenOperation =
                integrityManager.RequestIntegrityToken(tokenRequest);

            // Wait for PlayAsyncOperation to complete.
            yield return requestIntegrityTokenOperation;

            // Check the resulting error code.
            if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
            {
                // Log the error, in a real application, for potentially
                // transient errors such as network connectivity, you should
                // add retry with an exponential backoff
                Debug.Log($@"IntegrityAsyncOperation failed with error: 
                    {requestIntegrityTokenOperation.Error.ToString()}");
                yield break;
            }

            // Get the response.
            var tokenResponse = requestIntegrityTokenOperation.GetResult();

            // Send the command to our server with a POST request, including the
            // token, which will be decrypted and verified on the server.
            yield return PostServerCommand(tokenResponse.Token);
        }

コマンドをサーバーに送信する

引き続き PlayIntegrityController.cs ファイルを編集します。PostServerCommand() 関数を見つけて、yield return null; ステートメントを次のコードに置き換えます。

        // Start a HTTP POST request to the performCommand URL, sending it the
        // command and integrity token data provided by Play Integrity.
        var serverCommand = new ServerCommand(TEST_COMMAND, tokenResponse);
        var commandRequest = new UnityWebRequest(URL_PERFORMCOMMAND, "POST");
        string commandJson = JsonUtility.ToJson(serverCommand);
        byte[] jsonBuffer = Encoding.UTF8.GetBytes(commandJson);
        commandRequest.uploadHandler = new UploadHandlerRaw(jsonBuffer);
        commandRequest.downloadHandler = new DownloadHandlerBuffer();
        commandRequest.SetRequestHeader(CONTENT_TYPE, JSON_CONTENT);
        yield return commandRequest.SendWebRequest();

        if (commandRequest.result == UnityWebRequest.Result.Success)
        {
            // Parse the command result Json
            var commandResult = JsonUtility.FromJson<CommandResult>(
                commandRequest.downloadHandler.text);
            if (commandResult != null)
            {
                resultLabel.text = commandResult.diagnosticMessage;
                _expressToken = commandResult.expressToken;
                if (commandResult.commandSuccess)
                {
                    resultLabel.color = Color.green;
                    expressButton.SetActive(true);
                }
                else
                {
                    resultLabel.color = Color.black;
                    expressButton.SetActive(false);
                }
            }
            else
            {
                Debug.Log("Invalid CommandResult json");
            }
        }
        else
        {
            Debug.Log($"Web request error on processToken: {commandRequest.error}");
        }

ファイルを保存します。

15. ビルドしてアップロードする(Unity)

Unity エディタの Android Keystore Manager を使用し、Google Play Console にアップロードできるようビルドに署名を設定します。

署名情報の設定後に、次の手順を行います。

  1. Unity の [File] メニューから [Build] -> [Build Settings...] を選択します。
  2. SampleScene が [Scenes in Build] リストに含まれていることを確認します。
  3. [Build App Bundle (Google Play)] チェックボックスがオンになっていることを確認します。
  4. [Build] ボタンをクリックし、エクスポート ファイルに名前を付けます。

App Bundle ファイルを作成したら、Google Play Console にアップロードします。内部テストトラックを使用して、ビルドにすばやくアクセスできるようにすることをおすすめします。

これで、ビルドをダウンロードしてインストールし、完全性チェックを実行できるようになりました。結果は次のようになります。

fa83cdb1a700ca0b.png

これで Play Integrity を Unity エンジン プロジェクトに統合できました。続けてその他のクライアントの例を確認するか、この Codelab の最後までスキップしてください。

16. プロジェクトをビルドして実行する(Kotlin)

Android Studio を実行します。[Welcome to Android Studio] ウィンドウで [Open] ボタンをクリックし、add-play-integrity-codelab/kotlin/start にある Android Studio プロジェクトを開きます。

アプリケーション ID を更新する

ビルドを Google Play にアップロードする前に、アプリケーション ID をデフォルトのものから一意のものに変更する必要があります。次の手順で行います。

  1. Android Studio の [Project] ペインで、モジュール PlayIntegrityCodelab.appbuild.gradle ファイルを開きます。
  2. applicationId ステートメントを見つけます。
  3. com.example.google.codelab.playintegritykotlin をサーバーのデプロイ時に選択した ID に変更し、ファイルを保存します。
  4. ファイルの上に、Gradle ファイルの変更を通知するバナーが表示されます。[Sync Now] をクリックして、ファイルを再読み込みし再同期します。

サーバーの URL を更新する

プロジェクトを更新して、サーバーをデプロイした URL の場所を指すようにする必要があります。手順は次のとおりです。

  1. Android Studio の [Project] ペインで、start/app/src/main/java/com.example.google.codelab.playintegritykotlin/integrity の下にある IntegrityServer ファイルを開きます。
  2. URL を ‘https://your.play-integrity.server.com' からサーバーのベース URL に変更し、ファイルを保存します。

結果は次のようになります。

    private val SERVER_URL: String = "https://your-play-integrity-server.uc.r.appspot.com"

実際の URL はプロジェクト名と、サーバーのデプロイに使用した Google Cloud リージョンによって異なります。

ビルドして実行する

開発用に構成された Android デバイスを接続します。Android Studio でプロジェクトをビルドし、接続済みのデバイスで実行します。アプリには次のように表示されます。

d77ca71dc209452f.png

起動時に、アプリはサーバー上で getRandom エンドポイントを呼び出し、結果を表示します。URL が正しくない、サーバーが機能していないなどのエラーが発生した場合は、エラー ダイアログが表示されます。[Request Random] ボタンをクリックすると、サーバーから新しい乱数を取得できます。[Call server with integrity check] ボタンはまだ機能していません。この機能は次のセクションで追加します。

17. Play Integrity をプロジェクトに追加する(Kotlin)

Play Integrity ライブラリとサポートする依存関係をプロジェクトに追加する手順は次のとおりです。

  1. Android Studio の [Project] ペインで、start/app の下にある build.gradle ファイルを開きます。
  2. ファイルの下にある dependencies ブロックを見つけます。
  3. dependencies ブロックの末尾に次の行を追加します。
    implementation "com.google.android.play:integrity:1.0.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1"
  1. ファイルを保存します。
  2. ファイルの上に、Gradle ファイルの変更を通知するバナーが表示されます。[Sync Now] をクリックして、ファイルを再読み込みし再同期します。

Kotlin のサンプルではコルーチンを使用します。kotlinx-coroutines-play-services ライブラリによる拡張機能で、Kotlin コルーチン内から Play Integrity 非同期 Task オブジェクトを簡単に操作できます。

18. 完全性リクエストを送信する(Kotlin)

Android Studio の [Project] ペインで、start/app/src/main/java/com.example.google.codelab.playintegritykotlin/integrity の下にある IntegrityServer ファイルを開きます。ファイルの下には空の integrityCommand 関数があります。[Call server with integrity check] ボタンをクリックすると、UI はこの関数を呼び出します。

integrityCommand 関数にコードを追加して、次の操作を行います。

  1. ノンスの作成に使用する新しい乱数をサーバーから取得して、完全性チェックに関連付けます。
  2. Play Integrity API を呼び出して完全性リクエストを送信し、結果を含む完全性トークンを受け取ります。
  3. HTTP POST リクエストを使用して、コマンドと完全性トークンをサーバーに送信します。
  4. 結果を処理して表示します。

空の integrityCommand 関数に次のコードを追加します。

        // Set our state to working to trigger a switch to the waiting UI
        _serverState.emit(ServerState(
            ServerStatus.SERVER_STATUS_WORKING))
        // Request a fresh random from the server as part
        // of the nonce we will generate for the request
        var integrityRandom = IntegrityRandom("", 0U)
        try {
            val returnedRandom = httpClient.get<IntegrityRandom>(
                SERVER_URL + "/getRandom")
            integrityRandom = returnedRandom
        } catch (t: Throwable) {
            Log.d(TAG, "getRandom exception " + t.message)
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_UNREACHABLE,
                IntegrityRandom("", 0U)))
        }

        // If we have a random, we are ready to request an integrity token
        if (!integrityRandom.random.isNullOrEmpty()) {
            val nonceString = GenerateNonce.GenerateNonceString(TEST_COMMAND,
                integrityRandom.random)
            // Create an instance of an IntegrityManager
            val integrityManager = IntegrityManagerFactory.create(context)

            // Use the nonce to configure a request for an integrity token
            try {
                val integrityTokenResponse: Task<IntegrityTokenResponse> =
                    integrityManager.requestIntegrityToken(
                        IntegrityTokenRequest.builder()
                            .setNonce(nonceString)
                            .build()
                    )
                // Wait for the integrity token to be generated
                integrityTokenResponse.await()
                if (integrityTokenResponse.isSuccessful && integrityTokenResponse.result != null) {
                    // Post the received token to our server
                    postCommand(integrityTokenResponse.result!!.token(), integrityRandom)
                } else {
                    Log.d(TAG, "requestIntegrityToken failed: " +
                            integrityTokenResponse.result.toString())
                    _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_FAILED_TO_GET_TOKEN))
                }
            } catch (t: Throwable) {
                Log.d(TAG, "requestIntegrityToken exception " + t.message)
                _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_FAILED_TO_GET_TOKEN))
            }
        }

サーバーへのコマンドを POST するコードは、別々の postCommand 関数に分割されます。空の postCommand 関数に次のコードを追加します。

        try {
            val commandResult = httpClient.post<CommandResult>(
                SERVER_URL + "/performCommand") {
                contentType(ContentType.Application.Json)
                body = ServerCommand(TEST_COMMAND, tokenString)
            }
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_REACHABLE,
                integrityRandom,
                commandResult.diagnosticMessage,
                commandResult.commandSuccess,
                commandResult.expressToken))
        } catch (t: Throwable) {
            Log.d(TAG, "performCommand exception " + t.message)
            _serverState.emit(ServerState(ServerStatus.SERVER_STATUS_UNREACHABLE))
        }

不足しているインポートを解決して、ファイルを保存します。

19. 結果を表示する(Kotlin)

判定結果の概要を使って UI を更新する

現在、UI には完全性判定の結果に関する概要のプレースホルダ テキストが表示されています。プレースホルダを実際の概要に置き換えるには、次の手順を行います。

  1. Android Studio の [Project] ペインで、start/app/src/main/java/com.example.google.codelab.playintegritykotlin/ui/main の下にある MainView.kt ファイルを開きます。
  2. MainUI 関数の末尾に移動して、text = "None", ステートメントを見つけ、次のコードに置き換えます。
                        text = state.serverState.serverVerdict,
  1. 不足しているインポートを解決して、ファイルを保存します。

20. ビルドしてアップロードする(Kotlin)

アプリのキーストアを作成してセットアップする

Android では、すべてのアプリはデバイスにインストールまたは更新される前に証明書でデジタル署名されている必要があります。

この Codelab では、アプリのキーストアを作成します。既存のゲームのアップデートを公開する場合は、アプリの以前のバージョンをリリースしたときと同じキーストアを再度使用します。

キーストアを作成してリリース App Bundle をビルドする

Android Studio でのキーストアの手順に沿ってキーストアを作成し、それを使用してゲームの署名付きリリースビルドを生成します。Android Studio で [Build] メニューから [Generate Signed Bundle / APK] を選択し、ビルドプロセスを開始します。[Android App Bundle] か [APK] を選択するよう求めるメッセージが表示されたら、[App Bundle] オプションを選択します。このプロセスが完了すると、Google Play Console へのアップロードに適した .aab ファイルが作成されます。

Google Play Console にアップロードする

App Bundle ファイルを作成したら、Google Play Console にアップロードします。内部テストトラックを使用して、ビルドにすばやくアクセスできるようにすることをおすすめします。

テストビルドを実行する

テストビルドを Google Play ストアからダウンロードして実行します。[Call server with integrity check] ボタンが正常にクリックできる状態になっており、次の画面が表示されます。

3291795e192396c9.png

21. 完了

これで Android アプリに Play Integrity API を正常に追加できました。

参考資料