Publicado por Oscar Rodríguez, Ingeniero de Relaciones con los Desarrolladores
Con el reciente lanzamiento del API de Integridad de la Reproducción, más desarrolladores están tomando medidas para proteger sus juegos y aplicaciones de interacciones potencialmente arriesgadas y fraudulentas.
Además de las señales útiles sobre la integridad de la aplicación, la integridad del dispositivo y la información de la licencia, la Play Integrity API presenta una función sencilla, pero muy útil, llamada «nonce» que, cuando se utiliza correctamente, puede reforzar aún más las protecciones existentes que ofrece la Play Integrity API, así como mitigar ciertos tipos de ataques, como los ataques de manipulación de persona en el medio (PITM) y los ataques de repetición.
En esta entrada del blog, profundizaremos en qué es el nonce, cómo funciona y cómo se puede utilizar para proteger aún más tu aplicación.
Índice de Contenidos
¿Qué es un nonce?
En criptografía e ingeniería de seguridad, un nonce (número una vez) es un número que se utiliza sólo una vez en una comunicación segura. Hay muchas aplicaciones para los nonces, como en la autenticación, el cifrado y el hashing.
En la Play Integrity API, el nonce es un blob binario opaco codificado en base-64 que estableces antes de invocar la comprobación de integridad de la API, y que se devolverá tal cual dentro de la respuesta firmada de la API. Dependiendo de cómo crees y valides el nonce, es posible aprovecharlo para reforzar aún más las protecciones existentes que ofrece la API de Integridad de la Reproducción, así como para mitigar ciertos tipos de ataques, como los ataques de manipulación de persona en el medio (PITM) y los ataques de repetición.
Aparte de devolver el nonce tal cual en la respuesta firmada, la Play Integrity API no realiza ningún procesamiento de los datos reales del nonce, así que mientras sea un valor válido en base-64, puedes establecer cualquier valor arbitrario. Dicho esto, para firmar digitalmente la respuesta, el nonce se envía a los servidores de Google, por lo que es muy importante no establecer el nonce con ningún tipo de información personal identificable (PII), como el nombre, el teléfono o la dirección de correo electrónico del usuario.
Establecer el nonce
Después de haber configurado tu aplicación para utilizar la API de integridad de Play, debes establece el nonce con el setNonce()
o su variante apropiada, disponible en las versiones Kotlin, Java, Unity y Native de la API.
Kotlin:
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) // Set the nonce .build())
Java:
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) // Set the nonce .build());
Unidad:
string 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);
Nativo:
/// Create an IntegrityTokenRequest object. const char* nonce = ... IntegrityTokenRequest* request; IntegrityTokenRequest_create(&request); IntegrityTokenRequest_setNonce(request, nonce); // Set the nonce IntegrityTokenResponse* response; IntegrityErrorCode error_code = IntegrityManager_requestIntegrityToken(request, &response);
Verificación del nonce
La respuesta de la API de integridad de la reproducción se devuelve en forma de Token Web JSON (JWT)cuya carga útil es un texto plano JSONcon el siguiente formato:
{ requestDetails: { ... } appIntegrity: { ... } deviceIntegrity: { ... } accountDetails: { ... } }
El nonce se puede encontrar dentro del requestDetails
que tiene el siguiente formato:
requestDetails: { requestPackageName: "...", nonce: "...", timestampMillis: ... }
El valor de la nonce
debe coincidir exactamente con el que has pasado previamente a la API. Además, como el nonce está dentro de la respuesta firmada criptográficamente de la API de Integridad de Reproducción, no es posible alterar su valor después de recibir la respuesta. Aprovechando estas propiedades, es posible utilizar el nonce para proteger aún más tu aplicación.
Proteger las operaciones de alto valor
Consideremos el escenario en el que un usuario malicioso está interactuando con un juego online que informa de la puntuación del jugador al servidor del juego. En este caso, el dispositivo no está comprometido, pero el usuario puede ver y modificar el flujo de datos de la red entre el juego y el servidor con la ayuda de un servidor proxy o una VPN, de modo que el usuario malicioso puede informar de una puntuación más alta, mientras que la puntuación real es mucho más baja.
La simple llamada a la Play Integrity API no es suficiente para proteger la aplicación en este caso: el dispositivo no está comprometido, y la aplicación es legítima, por lo que todas las comprobaciones realizadas por la Play Integrity API pasarán.
Sin embargo, es posible aprovechar el nonce de la API de Integridad de la Partida para proteger esta operación concreta de alto valor de informar de la puntuación de la partida, codificando el valor de la operación dentro del nonce. La implementación es la siguiente:
- El usuario inicia la acción de alto valor.
- Tu aplicación prepara un mensaje que quiere proteger, por ejemplo, en formato JSON.
- Tu aplicación calcula un hash criptográfico del mensaje que quiere proteger. Por ejemplo, con los algoritmos de hash SHA-256 o SHA-3-256.
- Tu aplicación llama a la API de Play Integrity, y llama a
setNonce()
para establecer el campo nonce con el hash criptográfico calculado en el paso anterior. - Tu aplicación envía a tu servidor tanto el mensaje que quiere proteger como el resultado firmado de la API de integridad de la reproducción.
- Tu servidor de aplicaciones verifica que el hash criptográfico del mensaje que ha recibido coincide con el valor del campo nonce del resultado firmado, y rechaza cualquier resultado que no coincida.
El siguiente diagrama secuencial ilustra estos pasos:
Siempre que el mensaje original a proteger se envíe junto con el resultado firmado, y tanto el servidor como el cliente utilicen exactamente el mismo mecanismo para calcular el nonce, esto ofrece una fuerte garantía de que el mensaje no ha sido manipulado.
Ten en cuenta que en este escenario, el modelo de seguridad funciona bajo el supuesto de que el ataque se produce en la red, no en el dispositivo ni en la app, por lo que es especialmente importante verificar también las señales de integridad del dispositivo y de la app que ofrece la Play Integrity API.
Prevención de ataques de repetición
Consideremos otro escenario en el que un usuario malintencionado intenta interactuar con una aplicación servidor-cliente protegida por la API de Integridad de la Reproducción, pero quiere hacerlo con un dispositivo comprometido, de forma que el servidor no lo detecte.
Para ello, el atacante utiliza primero la app con un dispositivo legítimo, y recoge la respuesta firmada de la API de integridad de Play. A continuación, el atacante utiliza la app con el dispositivo comprometido, intercepta la llamada a la Play Integrity API y, en lugar de realizar las comprobaciones de integridad, se limita a devolver la respuesta firmada previamente registrada.
Como la respuesta firmada no ha sido alterada de ninguna manera, la firma digital parecerá correcta, y el servidor de la aplicación puede ser engañado para que piense que se está comunicando con un dispositivo legítimo. Esto se llama ataque de repetición.
La primera línea de defensa contra un ataque de este tipo es verificar el timestampMillis
de la respuesta firmada. Este campo contiene la marca de tiempo en que se creó la respuesta, y puede ser útil para detectar respuestas sospechosamente antiguas, incluso cuando la firma digital se verifica como auténtica.
Dicho esto, también es posible aprovechar el nonce en la API de Integridad de la Reproducción, para asignar un valor único a cada respuesta, y verificar que la respuesta coincide con el valor único previamente establecido. La implementación es la siguiente:
- El servidor crea un valor globalmente único de forma que los usuarios maliciosos no puedan predecirlo. Por ejemplo, un número aleatorio criptográficamente seguro de 128 bits o más.
- Tu aplicación llama a la API de integridad de Play, y establece el campo nonce con el valor único recibido por el servidor de tu aplicación.
- Tu aplicación envía el resultado firmado de la API de Integridad de la Reproducción a tu servidor.
- Tu servidor verifica que el campo nonce del resultado firmado coincide con el valor único que generó previamente, y rechaza cualquier resultado que no coincida.
El siguiente diagrama secuencial ilustra estos pasos:
Con esta implementación, cada vez que el servidor pide a la aplicación que llame a la API de Integridad de la Reproducción, lo hace con un valor único global diferente, de modo que mientras este valor no pueda ser predicho por el atacante, no es posible reutilizar una respuesta anterior, ya que el nonce no coincidirá con el valor esperado.
Combinando ambas protecciones
Aunque los dos mecanismos descritos anteriormente funcionan de formas muy diferentes, si una aplicación requiere ambas protecciones al mismo tiempo, es posible combinarlas en una sola llamada a la API de Integridad de Reproducción, por ejemplo, añadiendo los resultados de ambas protecciones en un nonce de base-64 más grande. Una implementación que combina ambos enfoques es la siguiente:
- El usuario inicia la acción de alto valor.
- Tu aplicación pide al servidor un valor único para identificar la solicitud
- El servidor de tu aplicación genera un valor único global de forma que los usuarios maliciosos no puedan predecirlo. Por ejemplo, puedes utilizar un generador de números aleatorios criptográficamente seguro para crear dicho valor. Recomendamos crear valores de 128 bits o más.
- Tu servidor de aplicaciones envía el valor único global a la aplicación.
- Tu aplicación prepara un mensaje que quiere proteger, por ejemplo, en formato JSON.
- Tu aplicación calcula un hash criptográfico del mensaje que quiere proteger. Por ejemplo, con los algoritmos de hash SHA-256 o SHA-3-256.
- Tu aplicación crea una cadena añadiendo el valor único recibido del servidor de tu aplicación y el hash del mensaje que quiere proteger.
- Tu aplicación llama a la Play Integrity API y llama a setNonce() para establecer el campo nonce con la cadena creada en el paso anterior.
- Tu aplicación envía a tu servidor tanto el mensaje que quiere proteger como el resultado firmado de la API de integridad de la reproducción.
- El servidor de tu aplicación divide el valor del campo nonce, y verifica que el hash criptográfico del mensaje, así como el valor único que generó previamente coinciden con los valores esperados, y rechaza cualquier resultado que no coincida.
El siguiente diagrama secuencial ilustra estos pasos:
Estos son algunos ejemplos de formas en las que puedes utilizar el nonce para proteger aún más tu aplicación contra los usuarios malintencionados. Si tu aplicación maneja datos sensibles o es vulnerable a los abusos, esperamos que consideres la posibilidad de tomar medidas para mitigar estas amenazas con la ayuda de la API de integridad de Play.
Para saber más sobre el uso de la API de Integridad de la Reproducción y para empezar, visita la documentación en g.co/play/integrityapi.