完全性判定の結果

このページでは、返された完全性判定の結果の解釈とその対応について説明します。標準 API リクエストとクラシック API リクエストのどちらを実行しても、完全性判定の結果は同じ形式で同様の内容が返されます。完全性判定の結果は、デバイス、アプリ、アカウントの有効性に関する情報を伝えます。アプリのサーバーは、復号と検証がなされた判定結果のペイロードを使用して、アプリ内の特定のアクションまたはリクエストを処理する最善の方法を決定できます。

返される完全性判定の結果の形式

返されるペイロードは書式なしテキスト JSON であり、デベロッパー提供の情報とともに完全性シグナルを含んでいます。

ペイロードの一般的な構造は次のとおりです。

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

個々の完全性判定の結果を確認する前に、requestDetails フィールドの値が元のリクエストの値と一致していることを確認する必要があります。以下のセクションでは、各フィールドについて詳しく説明します。

リクエストの詳細フィールド

requestDetails フィールドにはリクエストに関する情報が含まれます。この情報にはデベロッパー提供情報が含まれ、標準リクエストの場合は requestHash、クラシック リクエストの場合は nonce にあります。

標準 API リクエストの場合:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the request.
  requestPackageName: "com.package.name"
  // Request hash provided by the developer.
  requestHash: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the integrity token
  // was prepared (computed on the server).
  timestampMillis: "1675655009345"
}

これらの値は、元のリクエストの値と一致する必要があります。したがって、次のコード スニペットに示すように、requestPackageNamerequestHash が元のリクエストで送信されたものと一致することを確認して、JSON ペイロードの requestDetails の部分を検証します。

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val requestHash = requestDetails.getString("requestHash")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

RequestDetails requestDetails =
    decodeIntegrityTokenResponse
    .getTokenPayloadExternal()
    .getRequestDetails();
String requestPackageName = requestDetails.getRequestPackageName();
String requestHash = requestDetails.getRequestHash();
long timestampMillis = requestDetails.getTimestampMillis();
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request.
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

クラシック API リクエストの場合:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded URL-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: "1617893780"
}

これらの値は、元のリクエストの値と一致する必要があります。したがって、次のコード スニペットに示すように、requestPackageNamenonce が元のリクエストで送信されたものと一致することを確認して、JSON ペイロードの requestDetails の部分を検証します。

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val nonce = requestDetails.getString("nonce")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName = requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

アプリの完全性フィールド

appIntegrity フィールドにはパッケージに関する情報が含まれます。

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates (base64-encoded URL-safe).
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict は次のいずれかの値を取ります。

PLAY_RECOGNIZED
アプリと証明書が、Google Play で配信されているバージョンと一致します。
UNRECOGNIZED_VERSION
証明書またはパッケージ名が Google Play の記録と一致しません。
UNEVALUATED
アプリの完全性は評価されませんでした。デバイスの信頼性が十分でないなど、要件が満たされていませんでした。

作成したアプリによってトークンが生成されたことを確認するには、次のコード スニペットに示すように、アプリの完全性が期待どおりであることを検証します。

Kotlin

val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict")

if (appRecognitionVerdict == "PLAY_RECOGNIZED") {
    // Looks good!
}

Java

JSONObject appIntegrity =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    appIntegrity.getString("appRecognitionVerdict");

if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) {
    // Looks good!
}

アプリのパッケージ名、アプリのバージョン、アプリの証明書を手動で確認することもできます。

デバイスの完全性フィールド

deviceIntegrity フィールドには deviceRecognitionVerdict という値のみが含まれます。この値には、デバイスがアプリの完全性をどの程度強化できるかを表すラベルが 1 つ以上含まれています。デバイスがラベルの基準を満たしていない場合、deviceIntegrity フィールドは空になります。

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

デフォルトでは、deviceRecognitionVerdict に含めることができる値は以下のとおりです。

MEETS_DEVICE_INTEGRITY
アプリは Google Play 開発者サービスを備えた Android デバイスで実行されています。このデバイスはシステム完全性チェックに合格し、Android の互換性要件を満たしています。
空(空の値)
アプリは、API フックなどの攻撃や root 権限取得などのシステム侵害の兆候があるデバイス、または Google Play の完全性チェックに合格していない仮想デバイス(エミュレータなど)で動作しています。

トークンが信頼できるデバイスから生成されたことを確認するには、次のコード スニペットに示すように、deviceRecognitionVerdict が期待どおりであることを検証します。

Kotlin

val deviceIntegrity =
    JSONObject(payload).getJSONObject("deviceIntegrity")
val deviceRecognitionVerdict =
    if (deviceIntegrity.has("deviceRecognitionVerdict")) {
        deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    } else {
        ""
    }

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

Java

JSONObject deviceIntegrity =
    new JSONObject(payload).getJSONObject("deviceIntegrity");
String deviceRecognitionVerdict =
    deviceIntegrity.has("deviceRecognitionVerdict")
    ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    : "";

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

テストデバイスがデバイスの完全性要件を満たさない問題が発生した場合は、工場出荷時の ROM がインストールされていることと、ブートローダーがロックされていることを確認してください(前者はデバイスをリセットするなどの方法で確認できます)。また、Google Play Console で Play Integrity API テストを作成することもできます。

条件付きのデバイスラベル

アプリを PC 版 Google Play Games にリリースする場合は、deviceRecognitionVerdict に次のラベルを付けることもできます。

MEETS_VIRTUAL_INTEGRITY
アプリは Google Play 開発者サービスを備えた Android エミュレータで実行されています。このエミュレータはシステム完全性チェックに合格し、Android の互換性に関する主要な要件を満たしています。

オプションのデバイス情報

完全性判定の結果で追加ラベルを受け取ることをオプトインすると、deviceRecognitionVerdict に以下のラベルを追加できます。

MEETS_BASIC_INTEGRITY
アプリは、基本的なシステム完全性チェックに合格したデバイスで動作しています。このデバイスは Android の互換性要件を満たしておらず、Google Play 開発者サービスの実行を承認されていない可能性があります。たとえば、認識されていないバージョンの Android を搭載している、ブートローダーがロック解除されている、メーカーによる認定を受けていない、などの場合があります。
MEETS_STRONG_INTEGRITY
アプリは、Google Play 開発者サービスを搭載した Android デバイスで動作しており、システム完全性の強力な保証(ハードウェア格納型のブート完全性保証など)が存在します。このデバイスはシステム完全性チェックに合格し、Android の互換性要件を満たしています。

ラベルのそれぞれの条件が満たされている場合、1 つのデバイスがデバイスの完全性判定の結果で複数のデバイスラベルを返します。

最近のデバイスのアクティビティ(ベータ版)

最近のデバイスのアクティビティをオプトインすることもできます。これにより、過去 1 時間にアプリが特定のデバイスで完全性トークンをリクエストした回数を確認できます。最近のデバイスのアクティビティを使用することで、予期せぬ不審なアクティビティを過剰に行っている(アクティブな攻撃を示している可能性がある)デバイスからアプリを保護できます。一般的なデバイスにインストールされたアプリが 1 時間ごとに完全性トークンをリクエストする回数の予測に基づいて、最近のデバイスのアクティビティの信頼レベルを決定できます。

recentDeviceActivity を受け取ることをオプトインした場合、deviceIntegrity フィールドには以下の 2 つの値が含まれるようになります。

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  recentDeviceActivity: {
    // "LEVEL_2" is one of several possible values.
    deviceActivityLevel: "LEVEL_2"
  }
}

deviceActivityLevel 定義はモードによって異なり、次のいずれかの値を取ることができます。

最近のデバイスのアクティビティ レベル 標準 API リクエスト クラシック API リクエスト
LEVEL_1(最低) 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 10 個以下です。 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 5 個以下です。
LEVEL_2 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 11 ~ 25 個です。 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 6 ~ 15 個です。
LEVEL_3 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 26 ~ 50 個です。 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 16 ~ 30 個です。
LEVEL_4(最高) 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 50 個を超えています。 過去 1 時間にこのデバイスでアプリがリクエストした完全性トークンが 30 個を超えています。
UNEVALUATED 最近のデバイスのアクティビティは評価されませんでした。

次のような原因が考えられます。

  • デバイスの信頼性が十分でない。
  • デバイスにインストールされているアプリのバージョンが Google Play に認識されていない。
  • デバイスで技術的な問題が起きている。

アカウントの詳細フィールド

accountDetails フィールドには appLicensingVerdict という値のみが含まれます。この値は、アプリのライセンスのステータスを表します。

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict の値は次のいずれかになります。

LICENSED
ユーザーはアプリの利用資格を持っています。つまり、ユーザーは Google Play でアプリをインストールまたは購入しました。
UNLICENSED
ユーザーはアプリの利用資格を持っていません。ユーザーがアプリをサイドローディングした場合や、アプリを Google Play から取得したのではない場合などが該当します。この問題の解決のために、ユーザーに対して [GET_LICENSED] ダイアログを表示できます。
UNEVALUATED

要件が満たされていないため、ライセンスの詳細は評価されませんでした。

次のような原因が考えられます。

  • デバイスの信頼性が十分でない。
  • デバイスにインストールされているアプリのバージョンが Google Play に認識されていない。
  • ユーザーが Google Play にログインしていない。

ユーザーにアプリの利用資格があるかどうかを確かめるには、次のコード スニペットに示すように、appLicensingVerdict が想定どおりであることを確認します。

Kotlin

val accountDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = accountDetails.getString("appLicensingVerdict")

if (appLicensingVerdict == "LICENSED") {
    // Looks good!
}

Java

JSONObject accountDetails =
    new JSONObject(payload).getJSONObject("accountDetails");
String appLicensingVerdict = accountDetails.getString("appLicensingVerdict");

if (appLicensingVerdict.equals("LICENSED")) {
    // Looks good!
}

環境の詳細フィールド

Google Play Console で Play プロテクトの判定結果にオプトインしている場合は、API レスポンスに environmentDetails フィールドが含まれます。environmentDetails フィールドには playProtectVerdict という値のみが含まれます。この値は、デバイス上の Google Play プロテクトに関する情報を表します。

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict の値は次のいずれかになります。

NO_ISSUES
Play プロテクトはオンになっていますが、デバイスにおいてアプリの問題は検知されませんでした。
NO_DATA
Play プロテクトはオンになっていますが、まだスキャンが実行されていません。デバイスまたは Play ストア アプリが最近リセットされた可能性があります。
POSSIBLE_RISK
Play プロテクトがオフになっています。
MEDIUM_RISK
Play プロテクトがオンになっており、有害な可能性があるアプリがデバイスにインストールされていることが検知されました。
HIGH_RISK
Play プロテクトがオンになっており、危険なアプリがデバイスにインストールされていることが検知されました。
UNEVALUATED

Play プロテクトの判定結果は評価されませんでした。

次のような原因が考えられます。

  • デバイスの信頼性が十分でない。
  • ゲームのみ: ユーザー アカウントが LICENSED ではない。

Play プロテクトの判定結果の使用方法に関するガイダンス

アプリのバックエンド サーバーは、リスク許容度に基づいて、判定結果に基づく処理方法を決定できます。推奨される使い方と、考えられるユーザーの操作には次のようなものがあります。

NO_ISSUES
Play プロテクトがオンになっていて、問題は検出されていないため、ユーザーの操作は必要ありません。
POSSIBLE_RISKNO_DATA
この判定結果を受け取ったら、Play プロテクトがオンになっていてスキャンが実行されているか確認するようユーザーに依頼します。NO_DATA はまれにしか表示されません。
MEDIUM_RISKHIGH_RISK
リスク許容度に応じて、Play プロテクトを起動し、Play プロテクトの警告に対応するようユーザーに依頼できます。ユーザーがこれらの要件を満たすことができない場合は、サーバー アクションからブロックできます。