Revisión de seguridad de la aplicación móvil Wiqaytna, de rastreo de contactos de COVID-19
Pruebas de seguridad móvil de la aplicación de rastreo de contactos de Covid-19 Wiqaytna
Resumen
Esta serie de artículos es una revisión de aplicaciones de rastreo de contactos, realizada en colaboración con RandoriSec. El primero es una revisión de la aplicación móvil marroquí Wiqaytna, tanto en iOS como en Android. La revisión abordará el funcionamiento interno de la aplicación y del protocolo de rastreo de contactos. Evaluará la aplicación en cuanto a la transparencia de su proceso, la seguridad, la privacidad, las capacidades de rastreo y la accesibilidad.
Contexto
La pandemia de coronavirus COVID-19 es la crisis sanitaria mundial que define nuestra época y, probablemente, el mayor desafío de esta década. Desde su aparición en Asia a finales del año pasado, el virus se ha extendido a casi todos los continentes. Los casos aumentan a diario en África, América y Europa.
Lamentablemente, esta pandemia se ha cobrado la vida de cientos de miles de personas, ha provocado el confinamiento total de muchos países, ha empujado a las empresas a cerrar e incluso a quebrar, y ha dejado a mucha gente atrapada en países extranjeros, lejos de sus hogares, incluido uno de los autores.
En ausencia de un remedio eficaz o de vacunas, los países compiten por frenar la propagación del virus mediante la realización de pruebas y el tratamiento de los pacientes, la limitación de los viajes, la cuarentena de los ciudadanos y la cancelación de grandes aglomeraciones, como eventos deportivos, conciertos y clases.
La tecnología también se utiliza para combatir la COVID-19, aprovechando los 4.5 mil millones de teléfonos móviles. La idea de rastrear los movimientos de una persona mediante su teléfono móvil para encontrar a las personas a las que pudo haber contagiado ganó mucha popularidad, y hoy la mayoría de los países ya han lanzado una aplicación móvil que implementa el rastreo de contactos.
Entonces, ¿qué es el rastreo de contactos? Consiste simplemente en registrar a todas las personas con las que usted pudo haberse cruzado. Si se detecta que una persona está infectada, se avisa a las demás para que se hagan la prueba o guarden cuarentena.
El estándar de referencia
Antes de revisar cualquiera de las aplicaciones de rastreo de contactos, es necesario acordar primero un conjunto de expectativas o un estándar que estas aplicaciones deben cumplir.
Hasta donde sabemos, no existe ningún estándar establecido en ninguna parte. Mientras que algunas organizaciones hablan de privacidad y otras abordan la eficacia del protocolo de rastreo y las garantías que ofrece, no hay ninguna especificación ni guía en la que podamos basarnos.
Por tanto, tuvimos que redactar nuestras propias expectativas a partir de una serie de aportaciones que recopilamos y de nuestro mejor sentido común.
Estos requisitos son de alto nivel y, deliberadamente, independientes de la implementación:
- Transparencia
- El protocolo de rastreo está documentado
- La arquitectura y el diseño de la aplicación están documentados
- La aplicación cuenta con un programa para abordar los problemas de privacidad y seguridad (Bug Bounty)
- La aplicación es de código abierto para facilitar su revisión y la verificación por parte de la comunidad
- Privacidad
- La aplicación no recopila información de identificación personal (PII)
- La aplicación no rastrea a los usuarios ni recopila datos de ubicación
- La aplicación utiliza un conjunto limitado de permisos en el dispositivo, restringidos en su mayor parte al descubrimiento por Bluetooth y al acceso a internet
- La aplicación ofrece a los usuarios control sobre sus datos, para su borrado y su compartición
- Seguridad
- La aplicación mantiene una superficie de ataque reducida, sin anuncios, sin pagos ni funcionalidades ajenas
- La aplicación utiliza dependencias de terceros verificadas y actualizadas
- La aplicación se comunica a través de un canal cifrado seguro
- La aplicación es segura y no filtra información de rastreo a ninguna parte
- Eficacia del rastreo
- El protocolo de rastreo no permite rastrear a las personas
- Compartir la información de contactos es voluntario (opt-in)
- El rastreo no comparte ni recopila datos de ubicación ni PII
- El rastreo no permite volver a rastrear a las personas infectadas
- La comprobación del rastreo no está centralizada y es controlada por el usuario
- El rastreo funciona en Android e iOS incluso cuando la aplicación está en segundo plano o la pantalla está apagada
- Accesibilidad
- La aplicación está disponible en varios idiomas, incluidos los de los usuarios no locales
- La aplicación puede ser utilizada por no residentes (bloqueados en el país por la prohibición de viajar)
- La aplicación es utilizable por personas con discapacidad (daltonismo, discapacidad visual, etc.)
- La aplicación es utilizable en teléfonos de gama baja (tamaño de la aplicación, consumo de batería, ancho de banda reducido, etc.)
Metodología
Nuestra metodología de pruebas consistió, en primer lugar, en elaborar una amplia lista de aplicaciones de rastreo de contactos, centrándonos primero en las aplicaciones gubernamentales y priorizándolas por número de descargas.
Como algunas aplicaciones exigen datos de residente, como el número de pasaporte o un número de teléfono local, nos centramos en las aplicaciones que pudimos probar.
Cada aplicación se escaneó primero con Ostorlab Security Scanner en busca de vulnerabilidades de seguridad y después se revisó manualmente tanto en Android como en iOS. Cada aplicación se revisó en función de los 5 temas (Transparencia, Privacidad, Seguridad, Eficacia del rastreo y Accesibilidad).
La revisión completa sigue en curso debido al gran número de aplicaciones.
TL;DR
Los resultados generales no fueron satisfactorios. Varias aplicaciones implementan protocolos de rastreo propios que comparten datos de Bluetooth y de ubicación de forma continua, exigen PII, tienen una amplia superficie de ataque que da lugar a vulnerabilidades o abusos, algunas se comunican en texto claro y la mayoría carece de procesos de transparencia o de documentación.
La siguiente sección profundizará en una única aplicación que utiliza el protocolo OpenTrace. El objetivo no es señalar una aplicación concreta, sino compartir un ejemplo de nuestro enfoque técnico.
Sobre la aplicación Wiqaytna:
En general, la aplicación no presenta vulnerabilidades críticas, tiene en cuenta la privacidad y el equipo ha hecho sus deberes para que la aplicación se adapte a las particularidades del mercado marroquí.
- Elección del protocolo de rastreo respetuosa con la privacidad
- Sin recopilación de información de identificación personal (PII) ni de datos de ubicación
- Decisiones de implementación y de pila tecnológica sensatas
- Esfuerzo real por hacer el proceso transparente y documentado
Resumen de problemas:
| Elemento | Categoría | Severidad |
|---|---|---|
| iOS no recopilaba los identificadores temporales cuando la aplicación no estaba en primer plano | Rastreo de contactos | Alta |
| Se utiliza el mismo identificador temporal incluso pasado su periodo de caducidad | Rastreo de contactos / Privacidad | Alta |
| Faltan pruebas unitarias | Calidad del código / Seguridad | Alta |
| Dependencia vulnerable en iOS | Seguridad | Media |
| La validación del PIN es vulnerable a la fuerza bruta | Seguridad | Media |
| El servicio Bluetooth debe ser único, pero utiliza el UUID de pruebas de TraceTogther de Singapur | Rastreo de contactos | Baja |
| Falta la implementación del servidor en código abierto | Transparencia | Baja |
| La compatibilidad de idiomas se limita a 2 idiomas; ampliarla a otros idiomas oficiales y extranjeros | Accesibilidad | Baja |
| Uso de mecanismos IPC obsoletos | Calidad del código / Seguridad | Baja |

Análisis en profundidad: la aplicación Wiqaytna
Transparencia
La aplicación marroquí es una de las pocas aplicaciones de código abierto que hemos catalogado. La mayor parte del código fuente es accesible en el repositorio de Github, salvo el código fuente del backend, que simplemente remite al repositorio de OpenTrace. El análisis de la aplicación compilada muestra que la versión compilada coincide con la versión de código abierto.

La aplicación se apoya en el protocolo y la implementación de OpenTrace para el rastreo de contactos. El protocolo está bien documentado y lo utilizan otras aplicaciones, como las de Singapur y Austria.
Sin embargo, el protocolo ha sufrido varios problemas y presenta algunas limitaciones de rastreo en iOS.

Sin embargo, la implementación de OpenTrace no se incluye como biblioteca en la aplicación marroquí, sino que el código está integrado directamente en ella. Esto crea un riesgo de divergencia respecto de la implementación estándar y dificulta añadir correcciones y actualizaciones. El equipo de Wiqaytna debe vigilar los cambios y actualizar su repositorio para asegurarse de incorporar todos los cambios relevantes.
OpenTrace es uno de los varios protocolos de rastreo publicados; TCN, Robert y el rastreo de Apple y Google son otras implementaciones. Algunas aplicaciones han optado por utilizar sus propias implementaciones, lo que ha dado lugar a graves problemas de privacidad.

Aunque todos estos protocolos utilizan identificadores temporales para protegerse del rastreo, la principal divergencia está en el proceso de resolución del rastreo, es decir, en averiguar si una persona estuvo en contacto con otra persona infectada.
OpenTrace utiliza una implementación de rastreo centralizada, mientras que las implementaciones de TCN, Apple y Google, por ejemplo, son locales.
Si bien la segunda opción (la local) ofrece más garantías de privacidad, plantea desafíos de implementación en cuanto al ancho de banda y al consumo de batería.
El razonamiento del equipo de Wiqaytna para elegir el protocolo se publicó en un libro blanco/

El libro blanco repasa varios aspectos de las aplicaciones, incluido el proceso de pruebas; sin embargo, no hemos encontrado pruebas unitarias ni de integración en el repositorio. El equipo también ha añadido una política de seguridad para notificar problemas de seguridad.

Seguridad, privacidad y rastreo de contactos
Evaluar la seguridad de una aplicación móvil es un tema amplio. Para los fines de este artículo, nos centraremos solo en los aspectos más interesantes. Varios aspectos de la aplicación se revisaron de forma automática con el análisis de vulnerabilidades de Ostorlab.
La aplicación móvil tiene una superficie de ataque reducida. En comparación, varias de las otras aplicaciones de rastreo de contactos que hemos revisado añaden más funcionalidades, como QRCode, incluyen bibliotecas de pagos y de anuncios y exponen esquemas de URL, proveedores de contenido e interfaces de Javascript.
Dependencias de terceros
Se han añadido muy pocas dependencias a las aplicaciones; sin embargo, en el momento de las auditorías, una de las dependencias de la aplicación de iOS presentaba una vulnerabilidad conocida. La dependencia se identifica por huella a partir de la lista de Frameworks y también puede confirmarse en el código fuente de Podfile.lock.
Payload/Wiqaytna.app/Frameworks/nanopb.framework/Info.plist:
{
"BuildMachineOSBuild": "19E287",
"CFBundleDevelopmentRegion": "en",
"CFBundleExecutable": "nanopb",
"CFBundleIdentifier": "org.cocoapods.nanopb",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleName": "nanopb",
"CFBundlePackageType": "FMWK",
"CFBundleShortVersionString": "0.3.9011",
"CFBundleSignature": "????",
"CFBundleSupportedPlatforms": [
"iPhoneOS"
],
"CFBundleVersion": "1",
"DTCompiler": "com.apple.compilers.llvm.clang.1_0",
"DTPlatformBuild": "17F65",
"DTPlatformName": "iphoneos",
"DTPlatformVersion": "13.5",
"DTSDKBuild": "17F65",
"DTSDKName": "iphoneos13.5",
"DTXcode": "1150",
"DTXcodeBuild": "11E608c",
"MinimumOSVersion": "8.0",
"UIDeviceFamily": [
1,
2
]
}
La dependencia vulnerable es una dependencia transitiva de las bibliotecas de Firebase. Sin embargo, es poco probable que sea explotable. La vulnerabilidad afecta a la versión 0.3.9011 de nanopb. La descripción de CVE-2020-5235 es:
Existe una condición de falta de memoria potencialmente explotable en Nanopb anterior a 0.4.1, 0.3.9.5 y 0.2.9.4. Cuando nanopb se compila con PB_ENABLE_MALLOC, el mensaje que se va a decodificar contiene un campo repetido de tipo cadena, bytes o mensaje y realloc() se queda sin memoria al ampliar el array, nanopb puede acabar llamando a free() sobre un valor de puntero procedente de memoria sin inicializar. Dependiendo de la plataforma, esto puede provocar un fallo o una corrupción de memoria adicional, que en algunos casos puede ser explotable. Este problema está corregido en nanopb-0.4.1, nanopb-0.3.9.5 y nanopb-0.2.9.4.
Backend y comunicaciones de red
La aplicación se apoya en Firebase para alojar la implementación de su backend, realizar la autenticación y subir los archivos de rastreo.
La comunicación se realiza mediante TLS y se valida correctamente. La implementación del servidor se basa en gran parte en la implementación de código abierto de OpenTrace, pero otros endpoints, como las estadísticas y la actualización de perfiles (género, departamento, etc.), no son de código abierto.
La aplicación utiliza Firebase Functions para todas las llamadas a la API, Firebase Storage para subir los archivos de rastreo y Firebase config para gestionar la configuración.
- Ejemplo de un endpoint en Firebase Functions
POST /getTempIDs HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 13
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1
{"data":null}
- Ejemplo de un endpoint en Firebase Storage
POST /v0/b/opentrace-a065c.appspot.com/o?uploadType=resumable&name=streetPassRecords%2F20200608%2FStreetPassRecord_LGE_Nexus%205_2020-06-08_23-36-01.json HTTP/1.1
Authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY3MDIxMiwiZXhwIjoxNTkxNjczODEyLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.BFa9yg_l03TeeFuBL8Obo1NYSN80xSpmtcRlA-lFCXlYuS6kX8azYMfrik6G1S0x9PuxmftBxxd7Yn6rsueJtdoTchrF67XDMbqlnCiqtlNJuPItPKFyaeuB3IgY219yZxO4KWlQYmxy5IOL71jKj_RNS7iQpi0G3fG_lGa01CD6xaafBuSC3MmZyu-yeaeY7kNsdZRF7_YHPQerINR6C9sQH0ZZepcwv5onzh5j3sALfvaQVVTL1VyA6gZEAeZzPYPR_c2n5Izkk0VsNcgqKejHRte-Qh5heJ8MZAzRZep6_rUjMhCxv7DdbmBC_gTBvYs8ZXDdhgdUESPAMfDlvA
X-Firebase-Storage-Version: Android/20.15.15 (040308-306758586)
X-Goog-Upload-Header-Content-Type: application/octet-stream
X-Goog-Upload-Protocol: resumable
x-firebase-gmpid: 1:319155529705:android:f102bb0f7be8a8bac2cd78
X-Goog-Upload-Command: start
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; Nexus 5 Build/NJH47F)
Host: firebasestorage.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate
Subida de archivos
Uno de los problemas más importantes identificados en la implementación del backend afecta a la funcionalidad de subida de los archivos de rastreo. La subida requiere un PIN proporcionado por las autoridades sanitarias, que se utiliza para obtener un token de subida.

La validación del PIN se realiza mediante el método getUploadToken:
/**
* Get upload token by passing in a secret string as `data`
*/
const getUploadToken = async (uid: string, data: any, context: functions.https.CallableContext) => {
console.log("getUploadToken:", "uid", uid, "data", data, "ip", context.rawRequest.ip);
let valid = false;
if (data) {
const uploadCodes = await retrieveUploadCodes();
console.log("getUploadToken:", "obtained ${uploadCodes.length} upload codes");
valid = uploadCodes.find(x => x === data) !== undefined;
console.log("getUploadToken:", "data is ${valid ? "valid" : "not valid"} code");
}
valid = uploadCodes.find(x => x === data) !== undefined;
console.log("getUploadToken:", "data is ${valid ? "valid" : "not valid"} code");
}
if (valid) {
const payload = Buffer.from(JSON.stringify(
{
uid,
createdAt: Date.now() / 1000,
upload: data
}
));
console.log("getUploadToken:", "uid:", "${uid.substring(0, 8)}***", "createdAt:", formatTimestamp(Date.now() / 1000));
// Prepare encrypter
const encryptionKey = await getEncryptionKey();
const customEncrypter = new CustomEncrypter(encryptionKey);
// Encode payload
const payloadData = customEncrypter.encryptAndEncode(payload);
console.log("getUploadToken: Completed. Payload byte size: ${payloadData.length}");
return {
status: "SUCCESS",
token: payloadData.toString("base64")
};
} else {
console.log("getUploadToken:", "Invalid data: ${data}");
throw new functions.https.HttpsError("invalid-argument", "Invalid data: ${data}");
}
Sin embargo, la validación del token NO es específica de cada usuario. En el caso de la aplicación Wiqaytna, el PIN es un número de 6 dígitos. Gracias a la escala de Firebase, todo el espacio de 6 dígitos se atacó por fuerza bruta en cuestión de minutos.
De hecho, el PIN no es necesario para subir el archivo de rastreo: una vez autenticado, es posible subir cualquier archivo con cualquier nombre al repositorio /o; véanse los ejemplos de solicitudes a continuación:
POST /v0/b/opentrace-a065c.appspot.com/o?uploadType=resumable&name=streetPassRecords%2F20200608%2FStreetPassRecord_LGE_Nexus%205_2020-06-08_23-36-01.json&upload_id=AAANsUkiafDezgJ4QQJa9JsncG1RoKvvi08qh40ZhmOrbOU5_tare9a_3kQABvsYS9m1pVmRDCiFS9WxVkq2Aq_NZrM&upload_protocol=resumable HTTP/1.1
Authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY3MDIxMiwiZXhwIjoxNTkxNjczODEyLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.BFa9yg_l03TeeFuBL8Obo1NYSN80xSpmtcRlA-lFCXlYuS6kX8azYMfrik6G1S0x9PuxmftBxxd7Yn6rsueJtdoTchrF67XDMbqlnCiqtlNJuPItPKFyaeuB3IgY219yZxO4KWlQYmxy5IOL71jKj_RNS7iQpi0G3fG_lGa01CD6xaafBuSC3MmZyu-yeaeY7kNsdZRF7_YHPQerINR6C9sQH0ZZepcwv5onzh5j3sALfvaQVVTL1VyA6gZEAeZzPYPR_c2n5Izkk0VsNcgqKejHRte-Qh5heJ8MZAzRZep6_rUjMhCxv7DdbmBC_gTBvYs8ZXDdhgdUESPAMfDlvA
X-Firebase-Storage-Version: Android/20.15.15 (040308-306758586)
X-Goog-Upload-Offset: 0
X-Goog-Upload-Protocol: resumable
x-firebase-gmpid: 1:319155529705:android:f102bb0f7be8a8bac2cd78
X-Goog-Upload-Command: upload, finalize
Content-Length: 32739
Content-Type: application/x-www-form-urlencoded
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; Nexus 5 Build/NJH47F)
Host: firebasestorage.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate
{"token":"klklklk","records":[],"events":[{"id":1,"msg":"Scanning Started","timestamp":1591635248},{"id":2,"msg":"Scanning Stopped","timestamp":1591635256},{"id":3,"msg":"Scanning Started","timestamp":1591635293},{"id":4,"msg":"Scanning Stopped","timestamp":1591635301},
Sin embargo, el PIN se utiliza para generar un token a partir del UID del usuario. El token se añade después al archivo subido y se valida antes de recopilar y analizar los identificadores temporales.
export async function _processUploadedData(filePath: string, validateTokenTimestamp: boolean = true): Promise<{ status: string; message?: string; filePath?: string }> {
const fileName = path.basename(filePath, ".json");
let uid = "", uploadCode = "", step = "";
try {
//
// Step 1: load file content into memory
//
step = "1 - load file";
const { token, records, events } = JSON.parse(await getStorageData(config.upload.bucketForArchive, filePath));
console.log("processUploadedData:", "step ${step}", "File is loaded, record count:", records.length);
//
// Step 2: Validate upload token to get uid
//
step = "2 - validate upload token";
({ uid, uploadCode } = await validateToken(token, validateTokenTimestamp));
console.log("processUploadedData:", "step ${step}", "Upload token is valid, id:", uid);
El riesgo de sobrescribir los archivos de otros usuarios, o simplemente de leer los archivos subidos, se mitiga mediante la política de seguridad de Firebase. Además, los archivos se archivan y se eliminan una vez subidos, lo que limita la posibilidad de ataques similares.
rules_version = "2";
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow create: if request.auth != null; // Only allow write, Cloud Functions have read/write access by default.
}
}
}
La política predeterminada de Firebase habría sido insegura, ya que permite el acceso de lectura y escritura a todos los usuarios autenticados.
Autenticación
La autenticación se realiza mediante el servicio de firebase, que obtiene un token temporal para comunicarse con el backend. El token se renueva continuamente en segundo plano, pero requiere varias solicitudes para completar el handshake de autenticación. Véanse las solicitudes de ejemplo a continuación:
POST /c2dm/register3 HTTP/1.1
Authorization: AidLogin 3751703094547129981:5572975265165158895
app: covid.trace.morocco
gcm_ver: 201515018
User-Agent: Android-GCM/1.5 (hammerhead NJH47F)
Content-Length: 1153
content-type: application/x-www-form-urlencoded
Host: android.clients.google.com
Connection: close
Accept-Encoding: gzip, deflate
X-subtype=319155529705&sender=319155529705&X-app_ver=67&X-osv=25&X-cliv=fiid-20.1.6&X-gmsv=201515018&X-appid=davxJEzSQ-KU19cQzA_uoT&X-scope=*&X-Goog-Firebase-Installations-Auth=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWQiOiJkYXZ4SkV6U1EtS1UxOWNRekFfdW9UIiwicHJvamVjdE51bWJlciI6MzE5MTU1NTI5NzA1LCJleHAiOjE1OTIyNTQ4MjAsImFwcElkIjoiMTozMTkxNTU1Mjk3MDU6YW5kcm9pZDpmMTAyYmIwZjdiZThhOGJhYzJjZDc4In0.AB2LPV8wRAIgMgJx98gRA6_SU6N8nW6wgTZVuddlyyBevprKhZo6dHoCIA4Iy3e5EJyGHeoFc2Qe_iYGdaiJsx_6HifTuSC3v46n&X-gmp_app_id=1%3A319155529705%3Aandroid%3Af102bb0f7be8a8bac2cd78&X-Firebase-Client=fire-auth%2F19.3.1+fire-iid%2F20.1.6+fire-analytics%2F17.4.0+fire-abt%2F19.0.1+fire-android%2F+fire-rc%2F19.1.4+fire-cls%2F17.0.0+fire-core-ktx%2F19.3.0+fire-fcm%2F20.1.6+kotlin%2F1.3.61+fire-installations%2F16.2.2+fire-fn%2F19.0.2+fire-perf%2F19.0.7+fire-gcs%2F19.1.1+fire-core%2F19.3.0&X-firebase-app-name-hash=R1dAH9Ui7M-ynoznwBdw01tLxhI&X-Firebase-Client-Log-Type=1&X-app_ver_name=1.0.67&app=covid.trace.morocco&device=3751703094547129981&app_ver=67&info=k9JSinrwmBIcQKri541rkWV16eIluRY&gcm_ver=201515018&plat=0&cert=ad695288cfbac51f5d7a1c01b70dc9d17bfdee75&target_ver=28
POST /v1/token?alt=proto&key=AIzaSyAoR1Pr0U0xhT52a3FlnLK2OeGEjtUNJic HTTP/1.1
Content-Type: application/x-protobuf
X-Firebase-Locale:
X-Client-Version: Android/GmsCore/X19003001/FirebaseCore-Android
Accept-Language: en-US
X-Android-Package: covid.trace.morocco
X-Android-Cert: AD695288CFBAC51F5D7A1C01B70DC9D17BFDEE75
X-Goog-Spatula: CjMKE2NvdmlkLnRyYWNlLm1vcm9jY28aHHJXbFNpTSs2eFI5ZGVod0J0dzNKMFh2OTduVT0SIHJZIRStW1F0J8MBBVCOImgRlh8OUv/zaX9g98To5sncGP3M3Onkwq6INCCI4u/0m5zAmDQqSQBeaoCcvd7j4G9W0tHHPxFfDghEUwL14YXByaqT5AoHKQjFCAIyjLFGBRfafsYYmjO71FlNW7YtO+W7X97HZvZmUw67FR2vvn8=
User-Agent: Mozilla 5.0 (Linux; U; Android 7.1.2; en_US; Nexus 5; Build/NJH47F); com.google.android.gms/201515018; FastParser/1.1; ApiaryHttpClient/1.0; (gzip) (hammerhead NJH47F); gzip
Content-Length: 222
Host: securetoken.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate
refresh_tokenÌAE0u-Nepqs-8lFOdGMvgfgNcJA9fyrDqMrSXfbVgtY811m5NNcHv5XsmssQdkv3csqJr1LCSQ4m_aSCvPa2KjKNGJsHMAGs-jmbN_GYS64PFOMzq8bY-78vf9u6QMUHBgwDziJlrHQioVk843vPorA3LIyrDPDOCAcHbU8fndIzA5ySj-X7kMA_RK0t5evIOy2cGxrXbhU4M
HTTP/1.1 200 OK
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Date: Tue, 09 Jun 2020 01:33:34 GMT
Content-Type: application/x-protobuf
Content-Disposition: attachment
Vary: Origin
Vary: X-Origin
Vary: Referer
Server: ESF
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
Content-Length: 1957
ÐeyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY2NjQxNCwiZXhwIjoxNTkxNjcwMDE0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.DiTkt4EC3IUM8Qx4BKkX_Wu-DSt_59wX_bS0-8SVHA94M4vmcVQ7Bn4ixk-QkaT-AW6qxzcJyffc726TNaoG6KIYO4AHsdb-1ZO6ipBtetOx53vLxeF7L-5fwqerSzGqXvL4UDbetAWFxQylbcV0YbWzgXBrvBNoawG8osiBgZ-qC-b6VyElMwcm5s7Gt8mv_cdS8c4MB6xBppUYFBBdaUXzfAJqse6w0KR1CwUqhI_SojcuIWF9e0O7du0zngnThoixluqHM9bmYzoWcOD8QKfhLYgRDzet6r_sJCMlc48tzb8r3XeWodktMnIgj__P7N4tglLofjDX65BzISwwZgBearer"ÌAE0u-Nepqs-8lFOdGMvgfgNcJA9fyrDqMrSXfbVgtY811m5NNcHv5XsmssQdkv3csqJr1LCSQ4m_aSCvPa2KjKNGJsHMAGs-jmbN_GYS64PFOMzq8bY-78vf9u6QMUHBgwDziJlrHQioVk843vPorA3LIyrDPDOCAcHbU8fndIzA5ySj-X7kMA_RK0t5evIOy2cGxrXbhU4M*ÐeyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY2NjQxNCwiZXhwIjoxNTkxNjcwMDE0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.DiTkt4EC3IUM8Qx4BKkX_Wu-DSt_59wX_bS0-8SVHA94M4vmcVQ7Bn4ixk-QkaT-AW6qxzcJyffc726TNaoG6KIYO4AHsdb-1ZO6ipBtetOx53vLxeF7L-5fwqerSzGqXvL4UDbetAWFxQylbcV0YbWzgXBrvBNoawG8osiBgZ-qC-b6VyElMwcm5s7Gt8mv_cdS8c4MB6xBppUYFBBdaUXzfAJqse6w0KR1CwUqhI_SojcuIWF9e0O7du0zngnThoixluqHM9bmYzoWcOD8QKfhLYgRDzet6r_sJCMlc48tzb8r3XeWodktMnIgj__P7N4tglLofjDX65BzISwwZg2lXh3IIMEZCgUdC1wAOayVAxrRAm28éÏù¤
POST /getOTPCode HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 229
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1
{"data":{"token":"eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T","os":"ANDROID","phoneNumber":"+212xxxxxxxxx"}}
Durante el análisis del tráfico, confirmamos que Wiqaytna no recopila ninguna PII, pero exige un número de teléfono marroquí para recibir el OTP.
POST /getOTPCode HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 229
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1
{"data":{"token":"eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T","os":"ANDROID","phoneNumber":"+212612345678"}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"1a-XRg53+foeSHn5xZ1DF/JVqySS30"
Function-Execution-Id: uohs4he3srne
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: f52f429cd5b8842c69de05793722650b
Date: Mon, 08 Jun 2020 21:42:38 GMT
Server: Google Frontend
Content-Length: 26
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
{"result":{"result":true}}
POST /verifyOTP HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 25
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1
{"data":{"OTP":"415322"}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"f-ayLlCL3PuzXSThdu78iReSEjl6Y"
Function-Execution-Id: wee1qz85dpid
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: cdc7b708c0b133eeb7de7cd8171a498a
Date: Mon, 08 Jun 2020 21:54:55 GMT
Server: Google Frontend
Content-Length: 15
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
{"result":true}
Permisos
Todos los permisos que solicita la aplicación son legítimos. Se refieren principalmente al Bluetooth y al acceso a Internet. Algunos usuarios expresaron su preocupación por el permiso de ubicación (ACCESS_FINE_LOCATION); sin embargo, es necesario para el acceso Bluetooth en Android; consulte la documentación de Android para más detalles.
**Nuestro análisis ha mostrado que la aplicación no utiliza ni recopila ninguna coordenada de ubicación. **
En Android:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
En iOS:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Le Bluetooth est utilisé pour échanger des codes anonymes avec les autres utilisateurs de Wiqaytna et ainsi déterminer si ils ont été à proximité de vous</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Le Bluetooth est utilisé pour échanger des codes anonymes avec les autres utilisateurs de Wiqaytna et ainsi déterminer si ils ont été à proximité de vous</string>
<key>TRACER_SVC_ID</key>
...
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
</array>
Comunicación entre procesos (IPC)
Las aplicaciones Android utilizan varios esquemas de IPC para comunicarse internamente. Algunos de estos esquemas se registran dinámicamente mediante un Local Broadcast Manager y se anulan después de procesarse.
private fun prepare() {
val deviceAvailableFilter = IntentFilter(ACTION_DEVICE_SCANNED)
localBroadcastManager.registerReceiver(scannedDeviceReceiver, deviceAvailableFilter)
val deviceProcessedFilter = IntentFilter(ACTION_DEVICE_PROCESSED)
localBroadcastManager.registerReceiver(blacklistReceiver, deviceProcessedFilter)
timeoutHandler = Handler()
queueHandler = Handler()
blacklistHandler = Handler()
}
Sin embargo, Local Broadcast Manager está obsoleto y debería sustituirse por LiveData o flujos reactivos.
Rastreo de contactos
Como se ha mencionado, el rastreo de contactos utiliza el protocolo OpenTrace. El protocolo evita los ataques de rastreo y de repetición creando tokens temporales válidos solo durante 15 min, aunque otras implementaciones, como COVIDSafe, modificaron el protocolo a 2 horas, lo que aumenta el riesgo de ataques de repetición.
El protocolo no tiene estado, en el sentido de que ninguno de los identificadores temporales se almacena en el servidor. En cambio, el identificador temporal incluye el UID del usuario cifrado con una clave secreta del backend.
En el registro, se genera un lote de identificadores temporales y se envía a la aplicación móvil; véase a continuación una respuesta de ejemplo:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"11b78-pjrDe0QcfmRJJ5LFAbO0gM2nD5Q"
Function-Execution-Id: c8wiwl18rlh3
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: b98efbdc458ce408e0c6de05fe37421c
Date: Tue, 09 Jun 2020 01:33:49 GMT
Server: Google Frontend
Content-Length: 72568
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
{"result":{"status":"SUCCESS","tempIDs":[{"tempID":"RkL8XSgATVNi4BtqVnD5PEAQ/W4XbRowpSQI8GKuO929Q7Es0DwZRrsst2n2iYNDBupPFXBpeF8SzN7Xxg==","startTime":1591659229,"expiryTime":1591660129},{"tempID":"XMTgQ10Qcawdnkk6pvTnmRY7Lupp9iQZVvi8PQ2Rs34t0lLLnQphIyc0h/QuGMWytD7UqY3wShHT3h741Q==","startTime":1591660129,"expiryTime":1591661029},{"tempID":"4DWjqu4N6R1iJzFI15F1AhHqX3ZPYPwi+7vifSHJa3d7qYSrBgDS1dQWb+yb5PAcha23BqvxGKJWzeK+XA==","startTime":1591661029,"expiryTime":1591661929},{"tempID":"4IzfigFLAmvPjJgnJs74dAJIMhEWQGy1e86XgkcUlTWD4EL1pNKY0r1R8sHVjfIPIvgbbznCW358cVYISw==","startTime":1591661929,"expiryTime":1591662829},{"tempID":"JJ9QS14beseLakhTb6m8D15Ju7fZnz0T9nA5SiiNG1DobcyHVsceZWN/uaQ8gbzhNQScLYg+A63lNfzktQ==","startTime":1591662829,"expiryTime":1591663729},{"tempID":"daMVpZZWqR+SI8mYhQbw07VgdM4KCMcSobQDKzZUn9nq/Y8w5p5BSxeDox7ZTPQufa5zJ35Jf0E7pkzxJw==","startTime":1591663729,"expiryTime":1591664629},{"tempID":"xO+yM4u2AzNVSVWbiLw7xrSOyOVTy0PLW++VTtW7olkZ6cG4G4AFGRj5eDyIpqh4+GPKP9KQzAO8Q4ptSw==","startTime":1591664629,"expiryTime":1591665529},{"tempID":"55wlRGy2k2dpJQtL37dozYOU6Pz/W06LHYoeQ4z7Dkohg4veCAKYwe9LSKDC+zwQAqQXaq14/jud3SVBGA==","startTime":1591665529,"expiryTime":1591666429},{"tempID":"a5N6awZjMKzANIZyZUTFOzl2UnJsFH26Przt0PMwg/YGOBh9oBW7OMART5fZPMnd8fC/sKu3bq8VQzZtBA==","startTime":1591666429,"expiryTime":1591667329},{"tempID":"2MBbQrT3/9bqsHHKnmqkoops/xw64wmButkg0NQMrmw/mYlyaCtknRSW6TOowM07UsUQpLLQDVcUgMvEug==","startTime":1591667329,"expiryTime":1591668229},
Los identificadores temporales se emiten después mediante Bluetooth Low Energy (BLE); a continuación se muestra un ejemplo de trazas de pila recogidas del servicio Bluetooth en Android. La compilación de producción está ofuscada, de ahí los crípticos nombres de métodos de una sola letra:
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at long covid.trace.morocco.services.BluetoothMonitoringService.a()
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at void covid.trace.morocco.services.b.a()
at void covid.trace.morocco.services.b.b()
at int covid.trace.morocco.services.BluetoothMonitoringService$b.a()
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at void covid.trace.morocco.services.b.a()
at void covid.trace.morocco.services.b.a()
at int covid.trace.morocco.services.BluetoothMonitoringService$b.a()
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at boolean covid.trace.morocco.idmanager.TempIDManager.needToUpdate()
at void covid.trace.morocco.c.a$a.c()
With parameters:
1: : java.lang.String = "TempIDManager"
2: : java.lang.String = "Need to update and fetch TemporaryIDs? 1591864491000 vs 1591816764979: false"
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at boolean covid.trace.morocco.idmanager.TempIDManager.needToUpdate()
at void covid.trace.morocco.c.a$a.c()
at boolean covid.trace.morocco.c.a$a.b()
El servicio utiliza el UUID 17E033D3-490E-4BC9-9FE8-2F567643F4D3 y la Characteristic UUID 117BDD58-57CE-4E7A-8E87-7CCCDDA2A804; estos valores se extrajeron del binario y mediante el escaneo de los servicios Bluetooth:

Sin embargo, el UUID es el mismo que el UUID de preproducción que utiliza la aplicación TraceTogether. El equipo de Wiqytna debería haber generado y utilizado su propio UUID.
El descubrimiento es sencillo en Android, pero se complica mucho en iOS. Apple impone restricciones sobre lo que las aplicaciones pueden hacer en segundo plano, lo que limita la posibilidad de anunciar el servicio de rastreo. Apple tiene formas propietarias de usar (o abusar de) el estándar, utilizando un área de desbordamiento en el valor del fabricante para anunciar servicios en formato hash.


El repositorio de Open Trace tiene un largo issue en el que se debaten formas de sortear las limitaciones del Bluetooth de Apple. Por este motivo en concreto, algunos países han optado por usar el protocolo de rastreo de Google y Apple, ya que estos detalles escabrosos los gestiona el sistema operativo, donde Apple tiene pleno control de la pila Bluetooth. Covid Watch es un ejemplo de otra aplicación que utilizó trucos más avanzados para que el descubrimiento funcione mejor entre plataformas.
La characteristic expone un mensaje codificado en JSON con el identificador temporal y otra información de medición para ayudar al cálculo de la distancia:

En la aplicación marroquí, nuestra experimentación reveló dos problemas distintos:
1- En los dispositivos iOS, el identificador temporal no se intercambiaba con otros dispositivos cuando la pantalla estaba bloqueada o la aplicación estaba en segundo plano. Esto se confirmó inspeccionando la base de datos de iOS con el siguiente esquema:
sqlite> .schema ZENCOUNTER
CREATE TABLE ZENCOUNTER ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZV INTEGER, ZRSSI FLOAT, ZTIMESTAMP TIMESTAMP, ZTXPOWER FLOAT, ZMODELC VARCHAR, ZMODELP VARCHAR, ZMSG VARCHAR, ZORG VARCHAR );
Select * from ZENCOUNTER;
2- Tanto en dispositivos Android como iOS, la aplicación siguió emitiendo el mismo identificador temporal incluso después de que debería haber caducado:
1|1591809886424|2|not_found|MAR|Nexus 5|iPhone 7|-84|
2|1591810481878|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-90|
3|1591810486993|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-85|
...
143|1591823491233|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-53|
144|1591823554837|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-59|
145|1591823611469|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-54|
146|1591823672303|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-52|
147|1591823730987|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-58|
148|1591823790687|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-58|
149|1591823851166|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-59|
150|1591823911950|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-57|
151|1591823973664|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-62|
152|1591824032336|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-56|
En el siguiente extracto de la base de datos de Android, el identificador temporal nunca cambió, ni siquiera pasada la marca de tiempo de caducidad. Al revisar el dispositivo iOS, vemos que el identificador temporal debería haber caducado en 1591821662 (marca de tiempo epoch), pero en realidad se emite pasado ese valor:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ADVT_DATA</key>
<data>
eyJtcCI6ImlQaG9uZSA3IiwiaWQiOiIwakhpWWkwb05Gek5VZ1lFV21iREd1Mk1YZHord1MxN0ZwUllwZzNxR3l1OHZKemNYdzJCUm5qMjV2UWMrTmhBMWhQK2FwaldDNlc2N056MVZ3PT0iLCJvIjoiTUFSIiwidiI6Mn0=
</data>
<key>ADVT_EXPIRY</key>
<date>2020-06-11T10:26:02Z</date>
<key>BROADCAST_MSG</key>
<string>0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==</string>
<key>BROAD_MSG_ARRAY</key>
<array>
<dict>
<key>expiryTime</key>
<integer>1591821662</integer>
<key>startTime</key>
<integer>1591820762</integer>
<key>tempID</key>
<string>0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==</string>
</dict>
Criptografía
La aplicación se basa en operaciones criptográficas para generar y validar los identificadores temporales y los tokens de subida.
La aplicación utiliza un único algoritmo y la misma clave para todas las operaciones, pero admite varias claves. La implementación de código abierto menciona el uso de AES con GCM (Galois Counter Mode) o con CBC (Cipher Block Chaining), pero no establece ninguno por defecto. Desconocemos qué implementación utiliza Wiqaytna.

GCM ofrece garantías de seguridad más sólidas, ya que es un modo de cifrado autenticado que ofrece tanto cifrado como integridad. CBC, en cambio, sufre varios ataques conocidos, como Bit Flipping y Oracle Padding.
El hecho de que se utilice la misma clave para varias operaciones que requieren tanto cifrado como descifrado abre la puerta a ataques, como exigir a una API que cifre una entrada que controlamos e inyectarla en otra API que la descifra.
El análisis del código fuente no demostró la presencia de ninguno de estos ataques, ya que ninguna de las entradas de los métodos de cifrado está controlada por el usuario, salvo el PIN de 6 dígitos.
import crypto from "crypto";
import config from "../../config";
class CustomEncrypter {
algorithm: string;
key: Buffer;
constructor(key: Buffer, algorithm: string = config.encryption.defaultAlgorithm) {
this.algorithm = algorithm;
this.key = key;
}
decode(encodedData: Buffer, lengths: number[]): string[] {
const [cipherData, ivData, authTagData] = lengths.map((e, i) => Buffer.alloc(lengths[i]));
encodedData.copy(cipherData, 0, 0, lengths[0]);
encodedData.copy(ivData, 0, lengths[0], lengths[0] + lengths[1]);
encodedData.copy(authTagData, 0, lengths[0] + lengths[1], lengths[0] + lengths[1] + lengths[2]);
return [
cipherData.toString("base64"),
ivData.toString("base64"),
authTagData.toString("base64")
];
}
decrypt(cipherText: string, iv: string, authTag: string) {
const decipher = crypto.createDecipheriv(this.algorithm, this.key, Buffer.from(iv, "base64"));
// Add ts-ignore due to build error TS2339: Property 'setAuthTag' does not exist on type 'Decipher'.
// @ts-ignore
decipher.setAuthTag(Buffer.from(authTag, "base64"));
// @ts-ignore
let plainText = decipher.update(cipherText, "base64", "base64");
plainText += decipher.final("base64");
return plainText;
}
encode(cipherText: string, iv: string, authTag: string): Buffer {
const [cipherData, ivData, authTagData] = [cipherText, iv, authTag].map(e => Buffer.from(e, "base64"));
const buffer = Buffer.alloc(cipherData.length + ivData.length + authTagData.length);
cipherData.copy(buffer, 0);
ivData.copy(buffer, cipherData.length);
authTagData.copy(buffer, cipherData.length + ivData.length);
return buffer;
}
encrypt(data: Buffer, ivLength = 16, authTagLength = 16): string[] {
const dataB64 = data.toString("base64");
const iv = crypto.randomBytes(ivLength);
// @ts-ignore
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv, {authTagLength});
let cipherText = cipher.update(dataB64, "base64", "base64");
cipherText += cipher.final("base64");
return [
cipherText,
iv.toString("base64"),
cipher.getAuthTag().toString("base64")
];
}
encryptAndEncode(data: Buffer, ivLength = 16, authTagLength = 16): Buffer {
const [cipherTextB64, ivB64, authTagB64] = this.encrypt(data, ivLength, authTagLength);
return this.encode(cipherTextB64, ivB64, authTagB64);
}
decodeAndDecrypt(encodedData: Buffer, lengths: number[]): string {
const [decodedCipherTextB64, decodedIvB64, decodedAuthTagB64] = this.decode(encodedData, lengths);
return this.decrypt(decodedCipherTextB64, decodedIvB64, decodedAuthTagB64);
}
}
export default CustomEncrypter;
Varios
El registro (logging) en la aplicación Android está centralizado y desactivado en la compilación de producción. Para el análisis dinámico, se hicieron hooks de las API de registro para recopilar los registros de la aplicación y entender qué ocurría durante el proceso de rastreo:
at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at void covid.trace.morocco.c.a$a.c()
With parameters:
1: : java.lang.String = "BTMService"
2: : java.lang.String = "[TempID] Dont need to update Temp ID in actionScan"
El acceso al sistema de archivos también está restringido al registro en la tarjeta SD, que también está desactivado en la versión de producción.
Accesibilidad
La aplicación está disponible en francés y árabe. Aunque es un buen comienzo, no cubre todos los idiomas oficiales del país (árabe y tamazight), y varios turistas y residentes que siguen atrapados en el país no hablan ni francés ni árabe. Añadir más idiomas, como inglés y español, ayudaría a su adopción.
La aplicación exige un número de teléfono marroquí para registrarse. Aunque se comprende la intención de evitar abusos, los turistas tendrán que comprar un número de teléfono para usar la aplicación.
La aplicación tiene un tamaño reducido gracias a la pila tecnológica (Android e iOS nativos), lo que la hace adecuada para teléfonos de gama baja, y ha adaptado el protocolo de rastreo a las condiciones de red del país.
La aplicación no cuenta con ninguna otra funcionalidad de accesibilidad que facilite su uso por personas con discapacidad.