Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Revue de sécurité de l'application mobile Wiqaytna, application de traçage des contacts COVID-19

Test de sécurité mobile de Wiqaytna, l'application de traçage des contacts Covid-19

Résumé

Cette série d'articles passe en revue des applications de traçage des contacts, en collaboration avec RandoriSec. Le premier article est consacré à l'application mobile marocaine Wiqaytna, sur iOS comme sur Android. La revue abordera le fonctionnement interne de l'application et le protocole de traçage des contacts. Elle évaluera l'application sur la transparence de son processus, sa sécurité, sa confidentialité, ses capacités de traçage et son accessibilité.

Contexte

La pandémie de coronavirus COVID-19 est la crise sanitaire mondiale déterminante de notre époque et probablement le plus grand défi de cette décennie. Depuis son apparition en Asie à la fin de l'année dernière, le virus s'est propagé à presque tous les continents. Les cas augmentent chaque jour en Afrique, dans les Amériques et en Europe.

Cette pandémie a malheureusement coûté la vie à des centaines de milliers de personnes, provoqué le confinement total de nombreux pays, poussé des entreprises à fermer et même à faire faillite, et bloqué de nombreuses personnes à l'étranger, loin de chez elles, y compris l'un des auteurs.

En l'absence de remède efficace ou de vaccins, les pays s'efforcent de ralentir la propagation du virus en dépistant et en soignant les patients, en limitant les déplacements, en plaçant les citoyens en quarantaine et en annulant les grands rassemblements tels que les événements sportifs, les concerts et les cours dans les écoles.

La technologie est également mise à contribution pour lutter contre le COVID-19, en tirant parti des 4,5 milliards de téléphones mobiles. L'idée de suivre les déplacements d'une personne grâce à son téléphone mobile pour retrouver les personnes qu'elle a pu infecter a gagné beaucoup en popularité, et la plupart des pays ont désormais publié une application mobile de traçage des contacts.

Alors, qu'est-ce que le traçage des contacts ? Il consiste simplement à enregistrer toutes les personnes que vous avez pu croiser. Si une personne est déclarée infectée, les autres sont alertées afin de se faire tester ou de se mettre en quarantaine.

Le standard de référence

Avant d'examiner une application de traçage des contacts, il faut d'abord s'accorder sur un ensemble d'attentes, ou un standard, que ces applications doivent respecter.

À notre connaissance, aucun standard n'a été défini nulle part. Si certaines organisations parlent de confidentialité et d'autres discutent de l'efficacité du protocole de traçage et des garanties qu'il offre, il n'existe ni spécification ni guide sur lesquels nous puissions nous appuyer.

Nous avons donc dû rédiger nos attentes à partir d'un ensemble d'éléments recueillis et de notre bon sens.

Ces exigences sont de haut niveau et volontairement indépendantes de l'implémentation :

  • Transparence
    • Le protocole de traçage est documenté
    • L'architecture et la conception de l'application sont documentées
    • L'application dispose d'un programme pour traiter les problèmes de confidentialité et de sécurité (Bug Bounty)
    • L'application est open source afin de faciliter la revue et le contrôle par la communauté
  • Confidentialité
    • L'application ne collecte pas de PII
    • L'application ne suit pas les utilisateurs et ne collecte pas de données de localisation
    • L'application utilise un ensemble limité de permissions sur l'appareil, essentiellement restreintes à la découverte Bluetooth et à l'accès à Internet
    • L'application offre aux utilisateurs le contrôle de leurs données, pour leur suppression et leur partage
  • Sécurité
    • L'application impose une surface d'attaque réduite, sans publicité, sans paiement ni fonctionnalité superflue
    • L'application utilise des dépendances tierces vérifiées et à jour
    • L'application communique via un canal chiffré sécurisé
    • L'application est sécurisée et ne divulgue aucune information de traçage à quelque partie que ce soit
  • Efficacité du traçage
    • Le protocole de traçage ne permet pas de suivre des individus
    • Le partage des informations de contact est soumis à consentement (opt-in)
    • Le traçage ne partage ni ne collecte de localisation ou de PII
    • Le traçage ne permet pas de retrouver les personnes infectées
    • Les vérifications de traçage ne sont pas centralisées et sont contrôlées par l'utilisateur
    • Le traçage fonctionne sur Android et iOS, même lorsque l'application est en arrière-plan ou que l'écran est éteint
  • Accessibilité
    • L'application est accessible en plusieurs langues, y compris pour les utilisateurs non locaux
    • L'application peut être utilisée par des non-résidents (bloqués dans le pays à cause d'une interdiction de voyager)
    • L'application est utilisable par les personnes en situation de handicap (daltonisme, déficience visuelle, etc.)
    • L'application est utilisable sur des téléphones d'entrée de gamme (taille de l'application, consommation de batterie, faible bande passante, etc.)

Méthodologie

Notre méthodologie de test a d'abord consisté à compiler une longue liste d'applications de traçage des contacts, en privilégiant les applications gouvernementales, puis en les classant par nombre de téléchargements.

Comme certaines applications exigent des informations de résident, comme un numéro de passeport ou un numéro de téléphone local, nous nous sommes concentrés sur les applications que nous pouvions tester.

Chaque application a d'abord été analysée avec Ostorlab Security Scanner pour y rechercher des vulnérabilités de sécurité, puis examinée manuellement sur Android et sur iOS. Chaque application a été évaluée selon 5 thèmes (transparence, confidentialité, sécurité, efficacité du traçage et accessibilité).

La revue complète est toujours en cours en raison du grand nombre d'applications.

En bref

Les constats généraux ne sont pas satisfaisants. Plusieurs applications implémentent des protocoles de traçage personnalisés qui partagent en continu des données Bluetooth et de localisation, exigent des PII, présentent une large surface d'attaque exposant à des vulnérabilités ou à des abus ; certaines communiquent en clair, et la plupart n'ont ni processus de transparence ni documentation.

La section suivante examine plus en détail une application qui utilise le protocole OpenTrace. L'objectif n'est pas de pointer une application en particulier, mais de partager un exemple de notre approche technique.

Pour l'application Wiqaytna :

Dans l'ensemble, l'application ne souffre d'aucune vulnérabilité critique, elle respecte la confidentialité, et l'équipe a fait ses devoirs pour que l'application soit adaptée aux particularités du marché marocain.

  • Choix d'un protocole de traçage respectueux de la confidentialité
  • Aucune collecte d'informations personnelles identifiables (PII) ni de données de localisation
  • Choix d'implémentation et de pile technique raisonnables
  • Véritable effort pour rendre le processus transparent et documenté

Résumé des problèmes :

Élément Catégorie Sévérité
iOS ne collectait pas les identifiants temporaires lorsque l'application n'est pas au premier plan Traçage des contacts Élevée
Le même identifiant temporaire est utilisé même après la période d'expiration Traçage des contacts / Confidentialité Élevée
Tests unitaires manquants Qualité du code / Sécurité Élevée
Dépendance vulnérable sur iOS Sécurité Moyenne
La validation du code PIN peut être attaquée par force brute Sécurité Moyenne
Le service Bluetooth doit être unique, alors qu'il utilise l'UUID de test de TraceTogether (Singapour) Traçage des contacts Faible
Implémentation serveur open source manquante Transparence Faible
La prise en charge linguistique est limitée à 2 langues ; l'étendre aux autres langues officielles et aux langues étrangères Accessibilité Faible
Utilisation de mécanismes IPC obsolètes Qualité du code / Sécurité Faible


texte alternatif
test de sécurité des applications mobiles

Analyse approfondie : l'application Wiqaytna

Transparence

L'application marocaine est l'une des rares applications open source que nous avons répertoriées. La plus grande partie du code source est accessible sur le dépôt Github, à l'exception du code du backend, qui pointe simplement vers le dépôt OpenTrace. L'analyse de l'application compilée montre que la version compilée correspond bien à la version open source.

texte alternatif
test de sécurité des applications mobiles

L'application s'appuie sur le protocole et l'implémentation OpenTrace pour le traçage des contacts. Le protocole est bien documenté et utilisé par plusieurs autres applications, comme les applications singapourienne et autrichienne.

Le protocole a cependant connu plusieurs problèmes et présente certaines limites de traçage sur iOS.

texte alternatif
test de sécurité des applications mobiles

L'implémentation OpenTrace n'est cependant pas incluse comme bibliothèque dans l'application marocaine : son code y est directement intégré. Cela crée un risque de divergence par rapport à l'implémentation standard et rend plus difficile l'ajout de correctifs et de mises à jour. L'équipe Wiqaytna doit surveiller les changements et mettre à jour son dépôt pour s'assurer que toutes les modifications pertinentes sont reprises.

OpenTrace est l'un des nombreux protocoles de traçage publiés ; TCN, Robert et les protocoles de traçage d'Apple et de Google sont d'autres implémentations. Certaines applications ont choisi d'utiliser leurs propres implémentations, ce qui a entraîné de graves problèmes de confidentialité.

texte alternatif
protocoles des applications mobiles covid
Source :https://en.wikipedia.org/wiki/COVID-19_apps

Si tous ces protocoles utilisent des identifiants temporaires pour se protéger contre le suivi, la principale divergence porte sur le processus de résolution des traces, c'est-à-dire sur la manière de déterminer si une personne a été en contact avec un individu infecté.

OpenTrace utilise une implémentation de traçage centralisée, tandis que les implémentations TCN, Apple et Google, par exemple, sont locales.

Si cette dernière, locale, offre davantage de garanties de confidentialité, elle pose des difficultés d'implémentation en termes de bande passante et de consommation de batterie.

Le raisonnement qui a guidé le choix du protocole par l'équipe Wiqaytna a été publié dans un livre blanc/

texte alternatif
test de sécurité des applications mobiles

Le livre blanc passe en revue plusieurs aspects de l'application, y compris le processus de test ; nous n'avons cependant trouvé aucun test unitaire ni test d'intégration dans le dépôt. L'équipe a également ajouté une politique de sécurité pour signaler les problèmes de sécurité.

texte alternatif
test de sécurité des applications mobiles

Sécurité, confidentialité et traçage des contacts

Évaluer la sécurité d'une application mobile est un vaste sujet. Pour les besoins de cet article, nous ne nous concentrerons que sur les points les plus intéressants. Plusieurs aspects de l'application ont été examinés automatiquement grâce à l'analyse de vulnérabilités d'Ostorlab.

L'application mobile présente une surface d'attaque réduite. En comparaison, plusieurs des autres applications de traçage des contacts que nous avons examinées ajoutent davantage de fonctionnalités comme les QRCode, incluent des bibliothèques de paiement et de publicité, et exposent des schémas d'URL, des content providers et des interfaces Javascript.

Dépendances tierces

Très peu de dépendances ont été ajoutées à l'application, mais au moment des audits, l'une des dépendances de l'application iOS souffrait d'une vulnérabilité connue. La dépendance est identifiée à partir de la liste des Frameworks et peut aussi être confirmée dans le code source du 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 dépendance vulnérable est une dépendance transitive des bibliothèques Firebase. L'exploitabilité est cependant peu probable. La vulnérabilité affecte nanopb version 0.3.9011. La description de la CVE-2020-5235 est la suivante :

Il existe une condition de mémoire insuffisante potentiellement exploitable dans Nanopb avant les versions 0.4.1, 0.3.9.5 et 0.2.9.4. Lorsque nanopb est compilé avec PB_ENABLE_MALLOC, que le message à décoder contient un champ répété de type string, bytes ou message, et que realloc() manque de mémoire lors de l'extension du tableau, nanopb peut finir par appeler free() sur une valeur de pointeur issue d'une mémoire non initialisée. Selon la plateforme, cela peut provoquer un plantage ou une corruption de mémoire supplémentaire, qui peut être exploitable dans certains cas. Ce problème est corrigé dans nanopb-0.4.1, nanopb-0.3.9.5 et nanopb-0.2.9.4.

Backend et communications réseau

L'application s'appuie sur Firebase pour héberger l'implémentation de son backend, réaliser l'authentification et téléverser les fichiers de traçage.

La communication se fait en TLS et est correctement validée. L'implémentation du serveur repose en grande partie sur l'implémentation open source d'OpenTrace, mais plusieurs autres points de terminaison, comme les statistiques et la mise à jour des profils (genre, département, etc.), ne sont pas open source.

L'application utilise Firebase Functions pour tous les appels d'API, Firebase Storage pour téléverser les fichiers de traçage et Firebase config pour gérer la configuration.

  • Exemple de point de terminaison sur 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}
  • Exemple de point de terminaison sur 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
Téléversement de fichiers

L'un des problèmes les plus importants identifiés dans l'implémentation du backend concerne la fonctionnalité de téléversement des fichiers de traçage. Le téléversement exige un code PIN fourni par les autorités sanitaires, utilisé pour récupérer un jeton de téléversement.

texte alternatif
test de sécurité des applications mobiles

La validation du code PIN est effectuée par la méthode 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}");
  }

La validation du jeton n'est cependant PAS propre à l'utilisateur. Dans le cas de l'application Wiqaytna, le code PIN est un nombre à 6 chiffres. Grâce à l'échelle de Firebase, tout l'espace des 6 chiffres a été attaqué par force brute en quelques minutes.

Le code PIN n'est en réalité pas nécessaire pour téléverser le fichier de traçage : une fois authentifié, il est possible de téléverser n'importe quel fichier, sous n'importe quel nom, dans le dépôt /o ; voir les exemples de requêtes ci-dessous :

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

Le code PIN sert cependant à générer un jeton à partir de l'UID de l'utilisateur. Le jeton est ensuite ajouté au fichier téléversé et validé avant la collecte et l'analyse des identifiants temporaires.

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);

Le risque d'écraser les fichiers d'autres utilisateurs, ou simplement de lire les fichiers téléversés, est atténué par la politique de sécurité de Firebase. Les fichiers sont en outre archivés et supprimés une fois téléversés, ce qui limite la possibilité d'attaques similaires.

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 politique par défaut de Firebase aurait été non sécurisée, car elle autorise l'accès en lecture et en écriture à tous les utilisateurs authentifiés.

Authentification

L'authentification est réalisée avec le service firebase, qui récupère un jeton temporaire pour communiquer avec le backend. Le jeton est actualisé en continu en arrière-plan, mais plusieurs requêtes sont nécessaires pour terminer l'échange d'authentification. Voir des exemples de requêtes ci-dessous :

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__P7N4tglLofjDX65BzISwwZgBearer"Ì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"}}

Lors de l'analyse du trafic, nous avons confirmé que Wiqaytna ne collecte aucune PII, mais exige un numéro de téléphone marocain pour recevoir l'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}
Permissions

Toutes les permissions demandées par l'application sont légitimes. Elles concernent principalement le Bluetooth et l'accès à Internet. Certains utilisateurs se sont inquiétés de la permission de localisation (ACCESS_FINE_LOCATION) ; elle est cependant nécessaire pour l'accès Bluetooth sur Android, voir la documentation Android pour plus de détails.

Notre analyse a montré que l'application n'utilise ni ne collecte aucune coordonnée de localisation.

Sur 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"/>

Sur 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>
Communication inter-processus (IPC)

Les applications Android utilisent plusieurs schémas d'IPC pour communiquer en interne. Certains de ces schémas sont enregistrés dynamiquement à l'aide d'un Local Broadcast Manager, puis désenregistrés après traitement.

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()
}

Le Local Broadcast manager est cependant obsolète et devrait être remplacé par LiveData ou des flux réactifs.

Traçage des contacts

Comme indiqué plus haut, le traçage des contacts utilise le protocole OpenTrace. Le protocole empêche le suivi et les attaques par rejeu en créant des jetons temporaires valables 15 min seulement, alors que d'autres implémentations, comme COVIDSafe, ont modifié le protocole pour le porter à 2 heures, ce qui augmente le risque d'attaques par rejeu.

Le protocole est sans état, en ce sens qu'aucun des identifiants temporaires n'est stocké sur le serveur. L'identifiant temporaire inclut à la place l'UID de l'utilisateur, chiffré à l'aide d'une clé secrète du backend.

Lors de l'enregistrement, un lot d'identifiants temporaires est généré et envoyé à l'application mobile ; voir un exemple de réponse ci-dessous :

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

Les identifiants temporaires sont ensuite diffusés en Bluetooth Low Energy (BLE) ; voici un exemple de stack traces collectées depuis le service Bluetooth sur Android. Le build de production est obfusqué, d'où les noms de méthodes cryptiques d'une seule lettre :

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()

Le service utilise l'UUID 17E033D3-490E-4BC9-9FE8-2F567643F4D3 et la Characteristic UUID 117BDD58-57CE-4E7A-8E87-7CCCDDA2A804 ; ces valeurs sont extraites du binaire et obtenues en scannant les services Bluetooth :

texte alternatif
test de sécurité des applications mobiles

L'UUID est cependant le même que l'UUID de préproduction utilisé par l'application TraceTogether. L'équipe Wiqytna aurait dû générer et utiliser son propre UUID.

La découverte est simple sur Android, mais devient très épineuse sur iOS. Apple impose des restrictions sur ce que les applications peuvent faire en arrière-plan, ce qui limite la possibilité d'annoncer le service de traçage. Apple a des façons propriétaires d'utiliser/détourner le standard, en se servant d'une zone de débordement dans la valeur du fabricant pour annoncer des services sous forme hachée.

texte alternatif
test de sécurité des applications mobiles

texte alternatif
test de sécurité des applications mobiles

Le dépôt Open Trace contient une longue discussion sur les moyens de contourner les limitations Bluetooth d'Apple. Pour cette raison précise, certains pays ont opté pour le protocole de traçage de Google et Apple, car ces détails épineux sont gérés par l'OS, Apple ayant un contrôle total sur la pile Bluetooth. Covid Watch est un exemple d'une autre application qui a utilisé des astuces plus avancées pour améliorer la découverte entre plateformes.

La characteristic expose un message encodé en JSON contenant l'identifiant temporaire ainsi que d'autres informations de mesure destinées à aider au calcul de la distance :

texte alternatif
test de sécurité des applications mobiles

Pour l'application marocaine, notre expérimentation a révélé deux problèmes distincts :

1- Sur les appareils iOS, l'identifiant temporaire n'était pas échangé avec les autres appareils lorsque l'écran est verrouillé ou que l'application est en arrière-plan. Nous l'avons confirmé en inspectant la base de données iOS avec le schéma suivant :

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- Sur Android comme sur iOS, l'application continuait à diffuser le même identifiant temporaire, même après son expiration :

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|

Dans l'extrait suivant de la base de données Android, l'identifiant temporaire n'a jamais changé, même après l'horodatage d'expiration. En vérifiant l'appareil iOS, on constate que l'identifiant temporaire aurait dû expirer à 1591821662 (horodatage epoch), mais qu'il est en réalité diffusé au-delà de cette valeur :

<?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>
Cryptographie

L'application s'appuie sur des opérations cryptographiques pour générer et valider les identifiants temporaires et les jetons de téléversement.

L'application utilise un seul algorithme et la même clé pour toutes les opérations, mais prend en charge plusieurs clés. L'implémentation open source mentionne l'utilisation d'AES avec GCM (Galois Counter Mode) ou CBC (Cipher Block Chaining), mais n'en définit aucun par défaut. Nous ne savons pas quelle implémentation est utilisée par Wiqaytna.

texte alternatif
test de sécurité des applications mobiles

GCM offre des garanties de sécurité plus solides, car c'est un mode de chiffrement authentifié qui assure à la fois le chiffrement et l'intégrité. CBC, en revanche, souffre de plusieurs attaques connues, comme le Bit Flipping et les Oracle Padding Attacks.

Le fait que la même clé soit utilisée pour plusieurs opérations qui exigent à la fois le chiffrement et le déchiffrement ouvre la porte à des attaques, par exemple en demandant à une API de chiffrer une entrée que nous contrôlons pour l'injecter dans une API qui la déchiffre.

L'analyse du code source n'a mis en évidence aucune de ces attaques, car toutes les entrées des méthodes de chiffrement ne sont pas contrôlées par l'utilisateur, à l'exception du code PIN à 6 chiffres.

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;
Divers

La journalisation de l'application Android est centralisée et désactivée dans le build de production. Pour l'analyse dynamique, les API de journalisation ont été hookées afin de collecter les logs de l'application et de comprendre ce qui se passait pendant le processus de traçage :

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"

L'accès au système de fichiers est également restreint à la journalisation sur la carte SD, elle aussi désactivée dans la version de production.

Accessibilité

L'application est disponible en français et en arabe. C'est un bon début, mais cela ne couvre pas toutes les langues officielles du pays (l'arabe et le tamazight), et plusieurs touristes et résidents encore bloqués dans le pays ne parlent ni français ni arabe. L'ajout d'autres langues comme l'anglais et l'espagnol favoriserait l'adoption.

L'application exige un numéro de téléphone marocain pour s'enregistrer. Si l'on comprend l'intention d'éviter les abus, les touristes devront acheter un numéro de téléphone pour utiliser l'application.

L'application est de petite taille grâce à la pile technologique choisie (Android et iOS natifs), ce qui convient aux téléphones d'entrée de gamme, et elle a adapté le protocole de traçage aux conditions réseau du pays.

L'application ne dispose d'aucune autre fonctionnalité d'accessibilité pour faciliter son utilisation par les personnes en situation de handicap.

Tags :

covid-19, privacy, MAST