Veredictos de integridad

En esta página, se describe cómo interpretar el resultado del veredicto de integridad y trabajar con él. Ya sea que realices una solicitud estándar o clásica a la API, el veredicto de integridad se devuelve en el mismo formato con contenido similar. El veredicto de integridad comunica información sobre la validez de los dispositivos, las apps y las cuentas. El servidor de tu app puede usar la carga útil resultante en un veredicto verificado y desencriptado para determinar la mejor manera de proceder con una acción o una solicitud particulares en tu app.

Formato del resultado del veredicto de integridad

La carga útil es un archivo JSON de texto sin formato y contiene indicadores de integridad junto con información que proporciona el desarrollador.

La estructura general de la carga útil es la siguiente:

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

Antes de verificar cada veredicto de integridad, debes comprobar que los valores del campo requestDetails coincidan con los de la solicitud original. En las siguientes secciones, se describe cada campo con mayor detalle.

Campo de detalles de la solicitud

El campo requestDetails contiene información sobre la solicitud, incluida la información proporcionada por el desarrollador en el requestHash de las solicitudes estándar y en el nonce de las solicitudes clásicas.

Para solicitudes estándar a la 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"
}

Estos valores deben coincidir con los de la solicitud original. Por lo tanto, verifica la parte requestDetails de la carga útil de JSON asegurándote de que requestPackageName y requestHash coincidan con lo que se envió en la solicitud original, como se muestra en el siguiente fragmento de código:

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

Para solicitudes clásicas a la 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"
}

Estos valores deben coincidir con los de la solicitud original. Por lo tanto, verifica la parte requestDetails de la carga útil de JSON asegurándote de que requestPackageName y nonce coincidan con lo que se envió en la solicitud original, como se muestra en el siguiente fragmento de código:

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

Campo de integridad de la aplicación

El campo appIntegrity contiene información relacionada con el paquete.

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.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict puede tener los siguientes valores:

PLAY_RECOGNIZED
La app y el certificado coinciden con las versiones que distribuye Google Play.
UNRECOGNIZED_VERSION
El certificado o el nombre del paquete no coinciden con los registros de Google Play.
UNEVALUATED
No se evaluó la integridad de la aplicación. Se omitió un requisito necesario, por ejemplo, el dispositivo no es lo suficientemente confiable.

Para asegurarte de que el token haya sido generado por una app creada por ti, verifica que la integridad de la aplicación sea la esperada, como se muestra en el siguiente fragmento de código:

Kotlin

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

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

Java

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

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

También puedes verificar el nombre del paquete de la app, su versión y los certificados de forma manual.

Campo de integridad del dispositivo

El campo deviceIntegrity puede contener un solo valor, deviceRecognitionVerdict, que tenga una o más etiquetas que representen la capacidad de un dispositivo para aplicar de manera forzosa la integridad de la app. Si un dispositivo no cumple con los criterios de ninguna etiqueta, el campo deviceIntegrity estará vacío.

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

De forma predeterminada, deviceRecognitionVerdict puede tener una de las siguientes etiquetas:

MEETS_DEVICE_INTEGRITY
La app se está ejecutando en un dispositivo Android con la tecnología de los Servicios de Google Play. El dispositivo pasa las verificaciones de integridad del sistema y cumple con los requisitos de compatibilidad de Android.
Sin etiquetas (un valor en blanco)
La app se está ejecutando en un dispositivo que muestra indicios de ataque (como trampas de API) o de vulneración del sistema (como un dispositivo con permisos de administrador), o bien no se está ejecutando en un dispositivo físico (como un emulador que no pasa las verificaciones de Google Play Integrity).

Para asegurarte de que el token provenga de un dispositivo confiable, verifica que el deviceRecognitionVerdict sea el esperado, como se muestra en el siguiente fragmento de código:

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

Si tienes problemas con la integridad del dispositivo de prueba, asegúrate de que la ROM de fábrica esté instalada (por ejemplo, puedes restablecer el dispositivo) y de que el bootloader esté bloqueado. También puedes crear pruebas de la API de Play Integrity en tu Play Console.

Si aceptas recibir etiquetas adicionales en el veredicto de integridad, deviceRecognitionVerdict puede tener las siguientes etiquetas adicionales:

MEETS_BASIC_INTEGRITY
La app se está ejecutando en un dispositivo que pasa las verificaciones básicas de integridad del sistema. Es posible que el dispositivo no cumpla con los requisitos de compatibilidad de Android y que no esté aprobado para ejecutar los Servicios de Google Play. Por ejemplo, es posible que el dispositivo esté ejecutando una versión no reconocida de Android o que tenga un bootloader desbloqueado, o que el fabricante no haya certificado el dispositivo.
MEETS_STRONG_INTEGRITY
La app se está ejecutando en un dispositivo Android con la tecnología de los Servicios de Google Play y tiene una garantía sólida de integridad del sistema, como una prueba de integridad de inicio con copia de seguridad en hardware. El dispositivo pasa las verificaciones de integridad del sistema y cumple con los requisitos de compatibilidad de Android.

Además, si tu app se lanza en emuladores aprobados, el deviceRecognitionVerdict también puede adoptar la siguiente etiqueta:

MEETS_VIRTUAL_INTEGRITY
La app se está ejecutando en un Android Emulator con la tecnología de los Servicios de Google Play. El emulador pasa las verificaciones de integridad del sistema y cumple con los requisitos principales de compatibilidad de Android.

Campo de detalles de la cuenta

El campo accountDetails contiene un solo valor, appLicensingVerdict, que representa el estado de la licencia de la app o el del derecho de acceso a esta última.

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

appLicensingVerdict puede tener los siguientes valores:

LICENSED
El usuario tiene derechos de acceso a la app. En otras palabras, el usuario instaló o compró tu app en Google Play.
UNLICENSED
El usuario no tiene derechos de acceso a la app. Esto sucede, por ejemplo, cuando el usuario transfiere tu app desde una fuente desconocida o no la adquiere en Google Play.
UNEVALUATED

No se evaluó la información de las licencias porque se omitió un requisito necesario.

Estos son algunos de los diversos motivos por los que podría suceder:

  • El dispositivo no es lo suficientemente confiable.
  • La versión de tu app instalada en el dispositivo es desconocida para Google Play.
  • El usuario no accedió a Google Play.

Para verificar que el usuario tenga derechos de acceso a tu app, comprueba que el objeto appLicensingVerdict sea el esperado, como se muestra en el siguiente fragmento de código:

Kotlin

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

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

Java

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

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