Oceny integralności

Na tej stronie opisujemy, jak interpretować zwrócone wyniki integralności i jak pracować z nimi. Niezależnie od tego, czy wykonujesz standardowe, czy klasyczne żądanie do interfejsu API, ocena integralności jest zwracana w tym samym formacie z podobną treścią. Ocena integralności przekazuje informacje o prawidłowości urządzeń, aplikacji i kont. Serwer aplikacji może użyć otrzymanego ładunku w odszyfrowanej, zweryfikowanej ocenie, aby określić, jak najlepiej wykonać określone działanie lub żądanie w aplikacji.

Format zwróconej oceny integralności

Ładunek ma postać zwykłego tekstu JSON i zawiera sygnały integralności oraz informacje podane przez dewelopera.

Ogólna struktura ładunku wygląda tak:

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

Przed sprawdzeniem każdej oceny integralności musisz najpierw sprawdzić, czy wartości w polu requestDetails są zgodne z wartościami w pierwotnym żądaniu. W sekcjach poniżej znajdziesz bardziej szczegółowe informacje o każdym polu.

Pole szczegółów żądania

Pole requestDetails zawiera informacje o żądaniu, w tym dane podane przez dewelopera w polu requestHash w przypadku żądań standardowych i nonce w przypadku żądań klasycznych.

W przypadku standardowych żądań do interfejsu 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"
}

Wartości te powinny być zgodne z wartościami z pierwotnego żądania. Dlatego sprawdź część ładunku JSON requestDetails, upewniając się, że requestPackageName i requestHash są zgodne z tym, co wysłano w pierwotnym żądaniu, jak pokazano w tym fragmencie kodu:

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

W przypadku klasycznych żądań do interfejsu 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"
}

Wartości te powinny być zgodne z wartościami z pierwotnego żądania. Dlatego sprawdź część ładunku JSON requestDetails, upewniając się, że requestPackageName i nonce są zgodne z treściami wysłanymi w pierwotnym żądaniu, jak pokazano w tym fragmencie kodu:

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

Pole integralności aplikacji

Pole appIntegrity zawiera informacje dotyczące pakietu.

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 może mieć te wartości:

PLAY_RECOGNIZED
Aplikacja i certyfikat odpowiadają wersjom rozpowszechnianym w Google Play.
UNRECOGNIZED_VERSION
Nazwa certyfikatu lub pakietu nie zgadza się z rekordami Google Play.
UNEVALUATED
Integralność aplikacji nie została oceniona. Pominięto niezbędny wymóg, np. urządzenie nie było wystarczająco godne zaufania.

Aby upewnić się, że token został wygenerowany przez utworzoną przez Ciebie aplikację, sprawdź, czy integralność aplikacji jest zgodna z oczekiwaniami, tak jak w tym fragmencie kodu:

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

Możesz też ręcznie sprawdzić nazwę pakietu i wersję aplikacji oraz certyfikaty aplikacji.

pole integralności urządzenia

Pole deviceIntegrity może zawierać 1 wartość deviceRecognitionVerdict, która może zawierać co najmniej 1 etykietę wskazującą, jak urządzenie może egzekwować integralność aplikacji. Jeśli urządzenie nie spełnia kryteriów żadnej etykiety, pole deviceIntegrity jest puste.

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

Domyślnie deviceRecognitionVerdict może zawierać:

MEETS_DEVICE_INTEGRITY
Aplikacja działa na urządzeniu z Androidem i Usługami Google Play. Urządzenie przeszło testy integralności systemu i jest zgodne z Androidem.
Pusta (pusta wartość)
Aplikacja działa na urządzeniu, które wykazuje oznaki ataku (np. zaatakowania interfejsu API) lub naruszenia systemu (np. ma dostęp do roota) albo nie działa na urządzeniu fizycznym (np. emulator, który nie przechodzi testów integralności w Google Play).

Aby upewnić się, że token pochodzi z zaufanego urządzenia, sprawdź, czy deviceRecognitionVerdict działa zgodnie z oczekiwaniami, co pokazuje ten fragment kodu:

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

Jeśli masz problemy z integracją integralności urządzenia testowego na urządzeniu testowym, upewnij się, że jest zainstalowany fabryczny ROM (np. przez zresetowanie urządzenia) oraz że program rozruchowy jest zablokowany. Możesz też tworzyć testy interfejsu Play Integrity API w Konsoli Play.

Etykiety urządzeń warunkowych

Jeśli Twoja aplikacja jest publikowana w Grach Google Play na PC, deviceRecognitionVerdict może też zawierać tę etykietę:

MEETS_VIRTUAL_INTEGRITY
Aplikacja działa w obsługiwanym przez Androida emulatorze z Usługami Google Play. Emulator przechodzi testy integralności systemu i spełnia podstawowe wymagania dotyczące zgodności z Androidem.

Opcjonalne informacje o urządzeniu

Jeśli w ramach oceny integralności zgodzisz się na otrzymywanie dodatkowych etykiet, panel deviceRecognitionVerdict może zawierać te dodatkowe etykiety:

MEETS_BASIC_INTEGRITY
Aplikacja działa na urządzeniu, które przechodzi podstawowe testy integralności systemu. Urządzenie może być niezgodne z Androidem i może nie zostać zatwierdzone do uruchamiania Usług Google Play. Na przykład może korzystać z nierozpoznanej wersji Androida, mieć odblokowany program rozruchowy lub nie mieć certyfikatu producenta.
MEETS_STRONG_INTEGRITY
Aplikacja działa na urządzeniu z Androidem i Usługami Google Play. Gwarancja integralności systemu jest wysoka – np. w formie sprzętowego potwierdzenia integralności podczas uruchamiania. Urządzenie przeszło testy integralności systemu i jest zgodne z Androidem.

Jeśli każde z tych kryteriów zostanie spełnione, jedno urządzenie zwróci wiele etykiet w ramach oceny integralności urządzenia.

Ostatnia aktywność na urządzeniach (beta)

Możesz też wyrazić zgodę na ostatnią aktywność na urządzeniu, która informuje, ile razy w ciągu ostatniej godziny aplikacja zażądała tokena integralności na konkretnym urządzeniu. Korzystając z ostatniej aktywności na urządzeniu, możesz chronić swoją aplikację przed nieoczekiwanymi, hiperaktywnymi urządzeniami, które mogą wskazywać na aktywny atak. Możesz określić poziom zaufania dla każdego poziomu ostatniej aktywności na urządzeniu na podstawie tego, ile razy Twoja aplikacja zainstalowana na typowych urządzeniach będzie prosić o token integralności w każdej godzinie.

Jeśli wyrazisz zgodę na otrzymywanie recentDeviceActivity, pole deviceIntegrity będzie miało 2 wartości:

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

Definicje deviceActivityLevel różnią się w zależności od trybów i mogą mieć jedną z tych wartości:

Poziom ostatniej aktywności na urządzeniu Standardowe żądanie do interfejsu API Klasyczne żądanie do interfejsu API
LEVEL_1 (najniższa) W ciągu ostatniej godziny aplikacja zażądała maksymalnie 10 tokenów integralności na tym urządzeniu. W ciągu ostatniej godziny aplikacja zażądała maksymalnie 5 tokenów integralności na tym urządzeniu.
LEVEL_2 W ciągu ostatniej godziny aplikacja zażądała od 11 do 25 tokenów integralności na tym urządzeniu. W ciągu ostatniej godziny aplikacja zażądała od 6 do 15 tokenów integralności na tym urządzeniu.
LEVEL_3 W ciągu ostatniej godziny aplikacja zażądała od 26 do 50 tokenów integralności na tym urządzeniu. W ciągu ostatniej godziny aplikacja zażądała od 16 do 30 tokenów integralności na tym urządzeniu.
LEVEL_4 (najwyższa) W ciągu ostatniej godziny aplikacja zażądała ponad 50 tokenów integralności na tym urządzeniu. W ciągu ostatniej godziny aplikacja zażądała ponad 30 tokenów integralności na tym urządzeniu.
UNEVALUATED Ostatnia aktywność na urządzeniu nie została oceniona.

Może się tak zdarzyć z kilku powodów. Oto niektóre z nich:

  • Urządzenie nie jest wystarczająco zaufane.
  • Google Play nie rozpoznaje wersji aplikacji zainstalowanej na urządzeniu.
  • Problemy techniczne na urządzeniu.

Pole szczegółów konta

Pole accountDetails zawiera pojedynczą wartość appLicensingVerdict, która reprezentuje stan licencji aplikacji.

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

appLicensingVerdict może mieć jedną z tych wartości:

LICENSED
Użytkownik ma uprawnienia do korzystania z aplikacji. To znaczy, że zainstalował ją z Google Play lub kupił w Google Play.
UNLICENSED
Użytkownik nie ma uprawnień do korzystania z aplikacji. Może się tak zdarzyć, jeśli np. zainstaluje ją z innego urządzenia lub nie pozyska jej z Google Play. Aby rozwiązać ten problem, możesz pokazać użytkownikom okno GET_LICENSED.
UNEVALUATED

Szczegóły dotyczące licencji nie zostały określone, ponieważ pominięto niezbędny wymóg.

Może się tak zdarzyć z kilku powodów. Oto niektóre z nich:

  • Urządzenie nie jest wystarczająco zaufane.
  • Google Play nie rozpoznaje wersji aplikacji zainstalowanej na urządzeniu.
  • użytkownik nie jest zalogowany w Google Play,

Aby sprawdzić, czy użytkownik ma uprawnienia do korzystania z Twojej aplikacji, sprawdź, czy właściwość appLicensingVerdict jest zgodna z oczekiwaniami, jak pokazano w tym fragmencie kodu:

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

Pole szczegółów środowiska

Jeśli w Konsoli Google Play masz włączoną ocenę Play Protect, odpowiedź interfejsu API będzie zawierała pole environmentDetails. Pole environmentDetails zawiera pojedynczą wartość playProtectVerdict, która dostarcza informacji o Google Play Protect na urządzeniu.

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict może mieć jedną z tych wartości:

NO_ISSUES
Funkcja Play Protect jest włączona, ale nie wykryła żadnych problemów z aplikacją na urządzeniu.
NO_DATA
Play Protect jest włączone, ale jeszcze nie przeprowadzono skanowania. Urządzenie lub aplikacja Sklep Play mogły zostać niedawno zresetowane.
POSSIBLE_RISK
Funkcja Play Protect jest wyłączona.
MEDIUM_RISK
Usługa Play Protect jest włączona i znalazła potencjalnie szkodliwe aplikacje zainstalowane na urządzeniu.
HIGH_RISK
Usługa Play Protect jest włączona i wykryła zainstalowane na urządzeniu niebezpieczne aplikacje.
UNEVALUATED

Decyzja w sprawie Play Protect nie została rozpatrzona.

Może się tak zdarzyć z kilku powodów. Oto niektóre z nich:

  • Urządzenie nie jest wystarczająco zaufane.
  • Tylko gry: konto użytkownika nie jest kontem LICENSED.

Wskazówki dotyczące korzystania z oceny Play Protect

Serwer backendu aplikacji może zdecydować o działaniu na podstawie oceny na podstawie tolerancji ryzyka. Oto kilka sugestii i potencjalnych działań użytkowników:

NO_ISSUES
Usługa Play Protect jest włączona i nie wykryła żadnych problemów, więc użytkownik nie musi nic robić.
POSSIBLE_RISKNO_DATA
Gdy otrzymasz takie oceny, poproś użytkownika o sprawdzenie, czy funkcja Play Protect jest włączona i czy przeprowadziła skanowanie. Wartość NO_DATA powinna się pojawiać tylko w rzadkich przypadkach.
MEDIUM_RISKHIGH_RISK
W zależności od tolerancji ryzyka możesz poprosić użytkownika o uruchomienie Play Protect i podjęcie działań związanych z ostrzeżeniami Play Protect. Jeśli użytkownik nie może spełnić tych wymagań, możesz zablokować mu dostęp do działania serwera.