Si solo tienes previsto realizar solicitudes a la API estándar, que son adecuadas para la mayoría de los desarrolladores, puedes pasar directamente a los veredictos de integridad. En esta página, se describe cómo realizar solicitudes a la API clásicas para obtener veredictos de integridad, que se admiten en Android 4.4 (nivel de API 19) o versiones posteriores.
Consideraciones
Comparación entre solicitudes estándar y clásicas
Puedes realizar solicitudes estándar, clásicas o una combinación de ambas según las necesidades de seguridad y antiabuso de tu app. Las solicitudes estándar son adecuadas para todas las apps y los juegos, y se pueden usar para verificar que cualquier acción o llamada al servidor sea genuina y, al mismo tiempo, delegar cierta protección contra los ataques de reinyección y el robo de datos a Google Play. La creación de solicitudes clásicas es más costosa y depende de ti implementarlas de forma correcta para protegerte contra el robo de datos y ciertos tipos de ataques. Las solicitudes clásicas deben realizarse con menos frecuencia que las solicitudes estándar, por ejemplo, de vez en cuando para verificar si una acción altamente valiosa o sensible es original.
En la siguiente tabla, se destacan las diferencias clave entre los dos tipos de solicitudes:
Solicitud a la API estándar | Solicitud a la API clásica | |
---|---|---|
Requisitos previos | ||
Una versión mínima del SDK de Android requerida | Android 5.0 (nivel de API 21) o una versión posterior | Android 4.4 (nivel de API 19) o una versión posterior |
Requisitos de Google Play | Google Play Store y Servicios de Google Play | Google Play Store y Servicios de Google Play |
Detalles de la integración | ||
Preparación de la API requerida | ✔️ (unos segundos) | ❌ |
Latencia de solicitud típica | Unos cientos de milisegundos | Unos segundos |
Frecuencia de solicitudes potencial | Frecuente (verificación a pedido de cualquier acción o solicitud) | Infrecuente (verificación única de acciones de mayor valor o solicitudes más sensibles) |
Tiempos de espera | La mayoría de las preparaciones duran menos de 10 segundos, pero requieren una llamada al servidor, por lo que se recomienda un tiempo de espera prolongado (p. ej., 1 minuto). Las solicitudes de veredicto se realizan del lado del cliente | La mayoría de las solicitudes duran menos de 10 segundos, pero requieren una llamada al servidor, por lo que se recomienda un tiempo de espera prolongado (p. ej., 1 minuto). |
Token de veredicto de integridad | ||
Contiene los detalles del dispositivo, la app y la cuenta | ✔️ | ✔️ |
Almacenamiento en caché de tokens | Almacenamiento en caché integrado y protegido en el dispositivo de Google Play | No se recomienda |
Desencriptación y verificación del token a través del servidor de Google Play | ✔️ | ✔️ |
Latencia típica de solicitud de servidor a servidor de desencriptación | Decenas de milisegundos con disponibilidad de tres nueves | Decenas de milisegundos con disponibilidad de tres nueves |
Desencriptación y verificación del token de forma local en un entorno de servidor seguro | ❌ | ✔️ |
Desencriptación y verificación del token del cliente | ❌ | ❌ |
Actualidad del veredicto de integridad | Almacenamiento automático en caché y actualización de Google Play | Todos los veredictos se vuelven a calcular en cada solicitud |
Límites | ||
Solicitudes por app por día | 10,000 de forma predeterminada (se puede solicitar un aumento) | 10,000 de forma predeterminada (se puede solicitar un aumento) |
Solicitudes por instancia de app por minuto | Preparación: 5 por minuto Tokens de integridad: sin límite público* |
Tokens de integridad: 5 por minuto |
Protección | ||
Mitigación contra manipulaciones y ataques similares | Uso del campo requestHash |
Uso del campo nonce con vinculación de contenido basada en datos de solicitudes |
Mitigación contra ataques de reinyección y similares | Mitigación automática de Google Play | Uso del campo nonce con la lógica del servidor |
* Todas las solicitudes, incluidas las que no tienen límites públicos, están sujetas a límites de defensa no públicos en valores altos.
Cómo realizar solicitudes clásicas con poca frecuencia
La generación de un token de integridad consume tiempo, datos y batería, y cada app tiene una cantidad máxima de solicitudes clásicas que puede hacer por día. Por lo tanto, solo debes realizar solicitudes clásicas para verificar que las acciones con mayor valor o más sensibles sean originales cuando quieres una garantía adicional a la de una solicitud estándar. No debes realizar solicitudes clásicas para acciones de alta frecuencia o de bajo valor. No hagas solicitudes clásicas cada vez que la app pase a primer plano ni cada pocos minutos en segundo plano, y evita llamar desde una gran cantidad de dispositivos al mismo tiempo. Es posible que una app que realiza demasiadas solicitudes clásicas sea limitada para proteger a los usuarios de implementaciones incorrectas.
Cómo evitar veredictos de almacenamiento en caché
El almacenamiento en caché de un veredicto aumenta el riesgo de ataques, como el robo de datos y los de reinyección, es decir, cuando un veredicto bueno se reutiliza en un entorno no confiable. Si tienes previsto realizar una solicitud clásica y, luego, almacenarla en caché para usarla más tarde, se recomienda, en cambio, realizar una solicitud estándar a pedido. Las solicitudes estándar implican cierto almacenamiento en caché en el dispositivo, pero Google Play utiliza técnicas de protección adicionales para mitigar el riesgo de ataques de reinyección y de robo de datos.
Cómo usar el campo nonce para proteger las solicitudes clásicas
La API de Play Integrity ofrece un campo llamado nonce
, que puede usarse para proteger aún más tu app contra ciertos ataques, como los ataques de reinyección y de manipulación. La API de Play Integrity devuelve el valor que estableces en este campo, dentro de la respuesta de integridad firmada. Sigue con atención las instrucciones para generar nonces y proteger tu app de ataques.
Cómo reintentar solicitudes clásicas con retirada exponencial
Las condiciones del entorno, como una conexión a Internet inestable o un dispositivo sobrecargado, pueden causar fallas en las verificaciones de integridad del dispositivo. Esto puede provocar que no se generen etiquetas para un dispositivo que sea de confianza. Para mitigar estas situaciones, asegúrate de incluir una opción de reintento con retirada exponencial.
Descripción general
Cuando el usuario realice una acción de alto valor en tu app que quieras proteger con una verificación de integridad, completa los siguientes pasos:
- El backend del servidor de la app genera y envía un valor único a la lógica del cliente. En los pasos restantes, se hace referencia a esta lógica como tu "app".
- Tu app crea el
nonce
a partir del valor único y el contenido de la acción de alto valor. Luego, llama a la API de Play Integrity y pasa elnonce
. - Tu app recibe un veredicto firmado y encriptado de la API de Play Integrity.
- Tu app pasa el veredicto firmado y encriptado a su backend.
- El backend de tu app envía el veredicto a un servidor de Google Play. El servidor de Google Play desencripta y verifica el veredicto, y muestra los resultados en el backend de la app.
- El backend de la app decide el procedimiento, según los indicadores contenidos en la carga útil del token.
- El backend de tu app envía los resultados de la decisión a la app.
Cómo generar un nonce
Cuando proteges una acción en tu app con la API de Play Integrity, puedes aprovechar el campo nonce
para mitigar ciertos tipos de ataques, como los de manipulación relacionados a ataques de intermediarios (PITM) y los de reinyección. La API de Play Integrity devuelve el valor que estableces en este campo, dentro de la respuesta de integridad firmada.
El valor establecido en el campo nonce
debe tener el formato correcto con las siguientes características:
String
- Seguro para URL
- Codificado en Base64 y sin unión
- Con 16 caracteres como mínimo
- Con 500 caracteres como máximo
Las siguientes son algunas formas comunes de usar el campo nonce
en la API de Play Integrity. Para obtener la protección más sólida del nonce
, puedes combinar los métodos que se muestran a continuación.
Cómo incluir un hash de solicitud para evitar manipulaciones
Puedes usar el parámetro nonce
en una solicitud a la API clásica de manera similar al parámetro requestHash
en una solicitud a la API estándar para proteger el contenido de una solicitud contra la manipulación.
Cuando solicitas un veredicto de integridad, debes hacer lo siguiente:
- Calcula un resumen de todos los parámetros de solicitud críticos (p. ej., SHA256 de una serialización de solicitud estable) de la acción del usuario o la solicitud del servidor que está sucediendo.
- Usa
setNonce
para establecer el campononce
en el valor del resumen calculado.
Cuando recibes un veredicto de integridad, debes hacer lo siguiente:
- Decodifica y verifica el token de integridad, y obtén el resumen del campo
nonce
. - Calcula un resumen de la solicitud de la misma manera que en la app (p. ej., SHA256 de una serialización de solicitud estable).
- Compara los resúmenes de la app y del servidor. Si no coinciden, la solicitud no es confiable.
Cómo incluir valores únicos para protegerte contra los ataques de reinyección
Para evitar que usuarios maliciosos reutilicen respuestas anteriores de la API de Play Integrity, puedes usar el campo nonce
para identificar cada mensaje de forma única.
Cuando solicitas un veredicto de integridad, debes hacer lo siguiente:
- Obtén un valor único a nivel global de modo que los usuarios maliciosos no puedan predecirlo. Por ejemplo, un número al azar, seguro en términos criptográficos y generado en el servidor puede ser un valor de ese tipo, o bien un ID preexistente, como un ID de sesión o transacción. Una variante más simple y menos segura es generar un número al azar en el dispositivo. Te recomendamos que crees valores de 128 bits como mínimo.
- Llama a
setNonce()
para establecer el campononce
en el valor único del paso 1.
Cuando recibes un veredicto de integridad, debes hacer lo siguiente:
- Decodifica y verifica el token de integridad, y obtén el valor único del campo
nonce
. - Si el valor del paso 1 se generó en el servidor, verifica que el valor único recibido sea uno de los valores generados y que se use por primera vez (el servidor deberá mantener un registro de los valores generados durante un tiempo apropiado). Si el valor único recibido ya se usó o no aparece en el registro, rechaza la solicitud.
- De lo contrario, si el valor único se generó en el dispositivo, verifica que el valor recibido se esté usando por primera vez (tu servidor necesita mantener un registro de los valores ya vistos durante un período adecuado). Si el valor único recibido ya se usó, rechaza la solicitud.
Cómo combinar ambas protecciones contra ataques de manipulación y reinyección (recomendado)
Puedes usar el campo nonce
para protegerte contra los ataques de manipulación y reinyección al mismo tiempo. Para hacerlo, genera el valor único como se describe anteriormente y, luego, inclúyelo como parte de tu solicitud. El paso siguiente es calcular el hash de la solicitud y asegurarte de incluir el valor único como parte del hash. Una implementación que combina ambos enfoques es la siguiente:
Cuando solicitas un veredicto de integridad, debes hacer lo siguiente:
- El usuario inicia la acción de alto valor.
- Obtén un valor único para esta acción como se describe en la sección Cómo incluir valores únicos para protegerte de los ataques de reinyección.
- Prepara un mensaje que quieras proteger. Incluye el valor único del paso 2 en el mensaje.
- Tu app calcula un resumen del mensaje que desea proteger, como se describe en la sección Cómo incluir un hash de solicitud para evitar manipulaciones. Como el mensaje contiene el valor único, este es parte del hash.
- Usa
setNonce()
para establecer el campononce
en el resumen calculado del paso anterior.
Cuando recibes un veredicto de integridad, debes hacer lo siguiente:
- Obtén el valor único de la solicitud.
- Decodifica y verifica el token de integridad, y obtén el resumen del campo
nonce
. - Como se describe en la sección Cómo incluir un hash de solicitud para evitar manipulaciones, vuelve a calcular el resumen del servidor y verifica que coincida con el resumen obtenido del token de integridad.
- Como se describe en la sección Cómo incluir valores únicos para protegerte de los ataques de reinyección, verifica la validez del valor único.
En el siguiente diagrama de secuencias, se ilustran estos pasos con un nonce
del servidor:
Cómo solicitar un veredicto de integridad
Después de generar un nonce
, puedes solicitar un veredicto de integridad desde Google Play. Para ello, completa los siguientes pasos:
- Crea un
IntegrityManager
, como se muestra en los ejemplos que aparecen a continuación. - Crea un
IntegrityTokenRequest
y proporciona elnonce
a través del métodosetNonce()
en el compilador asociado. Las apps que se distribuyen de forma exclusiva fuera de Google Play y los SDK también deben especificar el número de proyecto de Google Cloud a través del métodosetCloudProjectNumber()
. Las apps de Google Play están vinculadas a un proyecto de Cloud en Play Console y no es necesario que configures el número del proyecto de Cloud en la solicitud. Usa el administrador para llamar a
requestIntegrityToken()
, y proporciona elIntegrityTokenRequest
.
Kotlin
// Receive the nonce from the secure server. val nonce: String = ... // Create an instance of a manager. val integrityManager = IntegrityManagerFactory.create(applicationContext) // Request the integrity token by providing a nonce. val integrityTokenResponse: Task<IntegrityTokenResponse> = integrityManager.requestIntegrityToken( IntegrityTokenRequest.builder() .setNonce(nonce) .build())
Java
import com.google.android.gms.tasks.Task; ... // Receive the nonce from the secure server. String nonce = ... // Create an instance of a manager. IntegrityManager integrityManager = IntegrityManagerFactory.create(getApplicationContext()); // Request the integrity token by providing a nonce. Task<IntegrityTokenResponse> integrityTokenResponse = integrityManager .requestIntegrityToken( IntegrityTokenRequest.builder().setNonce(nonce).build());
Unity
IEnumerator RequestIntegrityTokenCoroutine() { // Receive the nonce from the secure server. var nonce = ... // Create an instance of a manager. var integrityManager = new IntegrityManager(); // Request the integrity token by providing a nonce. var tokenRequest = new IntegrityTokenRequest(nonce); var requestIntegrityTokenOperation = integrityManager.RequestIntegrityToken(tokenRequest); // Wait for PlayAsyncOperation to complete. yield return requestIntegrityTokenOperation; // Check the resulting error code. if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError) { AppendStatusLog("IntegrityAsyncOperation failed with error: " + requestIntegrityTokenOperation.Error); yield break; } // Get the response. var tokenResponse = requestIntegrityTokenOperation.GetResult(); }
Nativo
/// Create an IntegrityTokenRequest opaque object. const char* nonce = RequestNonceFromServer(); IntegrityTokenRequest* request; IntegrityTokenRequest_create(&request); IntegrityTokenRequest_setNonce(request, nonce); /// Prepare an IntegrityTokenResponse opaque type pointer and call /// IntegerityManager_requestIntegrityToken(). IntegrityTokenResponse* response; IntegrityErrorCode error_code = IntegrityManager_requestIntegrityToken(request, &response); /// ... /// Proceed to polling iff error_code == INTEGRITY_NO_ERROR if (error_code != INTEGRITY_NO_ERROR) { /// Remember to call the *_destroy() functions. return; } /// ... /// Use polling to wait for the async operation to complete. /// Note, the polling shouldn't block the thread where the IntegrityManager /// is running. IntegrityResponseStatus response_status; /// Check for error codes. IntegrityErrorCode error_code = IntegrityTokenResponse_getStatus(response, &response_status); if (error_code == INTEGRITY_NO_ERROR && response_status == INTEGRITY_RESPONSE_COMPLETED) { const char* integrity_token = IntegrityTokenResponse_getToken(response); SendTokenToServer(integrity_token); } /// ... /// Remember to free up resources. IntegrityTokenRequest_destroy(request); IntegrityTokenResponse_destroy(response); IntegrityManager_destroy();
Cómo desencriptar y verificar el veredicto de integridad
Cuando solicitas un veredicto de integridad, la API de Play Integrity proporciona un token de respuesta firmado. El nonce
que incluyes en tu solicitud se vuelve parte del token de respuesta.
Formato del token
El token es un Token web JSON (JWT) anidado, que es la Encriptación web JSON (JWE) de la Firma web JSON (JWS). Los componentes de JWE y JWS se representan con la serialización compacta.
Los algoritmos de encriptación y firma son compatibles con varias implementaciones de JWT:
Cómo desencriptar y verificar en los servidores de Google (recomendado)
La API de Play Integrity te permite desencriptar y verificar el veredicto de integridad en los servidores de Google, lo que mejora la seguridad de tu app. Para hacerlo, completa estos pasos:
- Crea una cuenta de servicio en el proyecto de Google Cloud vinculado a tu app.
En el servidor de la app, recupera el token de acceso de las credenciales de la cuenta de servicio con el permiso
playintegrity
y realiza la siguiente solicitud:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
Lee la respuesta JSON.
Cómo desencriptar y verificar de manera local
Si optas por administrar y descargar tus claves de encriptación de respuestas, puedes desencriptar y verificar el token que se devuelve dentro de tu propio entorno de servidor seguro.
Puedes obtener ese token con el método IntegrityTokenResponse#token()
.
En el siguiente ejemplo, se muestra el modo en que se decodifican la clave AES y la EC pública con codificación DER para la verificación de firma desde Play Console a claves específicas del lenguaje (el lenguaje de programación Java, en nuestro caso) en el backend de la app. Ten en cuenta que las claves están codificadas en Base64 con marcas predeterminadas.
Kotlin
// base64OfEncodedDecryptionKey is provided through Play Console. var decryptionKeyBytes: ByteArray = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT) // Deserialized encryption (symmetric) key. var decryptionKey: SecretKey = SecretKeySpec( decryptionKeyBytes, /* offset= */ 0, AES_KEY_SIZE_BYTES, AES_KEY_TYPE ) // base64OfEncodedVerificationKey is provided through Play Console. var encodedVerificationKey: ByteArray = Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT) // Deserialized verification (public) key. var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE) .generatePublic(X509EncodedKeySpec(encodedVerificationKey))
Java
// base64OfEncodedDecryptionKey is provided through Play Console. byte[] decryptionKeyBytes = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT); // Deserialized encryption (symmetric) key. SecretKey decryptionKey = new SecretKeySpec( decryptionKeyBytes, /* offset= */ 0, AES_KEY_SIZE_BYTES, AES_KEY_TYPE); // base64OfEncodedVerificationKey is provided through Play Console. byte[] encodedVerificationKey = Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT); // Deserialized verification (public) key. PublicKey verificationKey = KeyFactory.getInstance(EC_KEY_TYPE) .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));
A continuación, usa estas claves para desencriptar el token de integridad (la parte de la JWE) y, luego, verificar y extraer la parte JWS anidada.
Kotlin
val jwe: JsonWebEncryption = JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption jwe.setKey(decryptionKey) // This also decrypts the JWE token. val compactJws: String = jwe.getPayload() val jws: JsonWebSignature = JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature jws.setKey(verificationKey) // This also verifies the signature. val payload: String = jws.getPayload()
Java
JsonWebEncryption jwe = (JsonWebEncryption)JsonWebStructure .fromCompactSerialization(integrityToken); jwe.setKey(decryptionKey); // This also decrypts the JWE token. String compactJws = jwe.getPayload(); JsonWebSignature jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws); jws.setKey(verificationKey); // This also verifies the signature. String payload = jws.getPayload();
La carga útil resultante es un token de texto sin formato que contiene veredictos de integridad.