Security

COVID-19 Contact Tracing App Wiqaytna Mobile Application Security Review

Mobile security testing of Covid-19 Contact Tracing Application Wiqaytna

Summary

These series of articles are a review of contact tracing applications in collaboration with RandoriSec. The first one is a review of the Moroccan Wiqaytna mobile application on both iOS and Android. The review will tackle the inner working of the application and the contact tracing protocol. It will review the application on its process transparency, security, privacy, tracing capabilities, and accessibility.

Context

The coronavirus COVID-19 pandemic is the defining global health crisis of our time and probably the greatest challenge of this decade. Since its emergence in Asia late last year, the virus has spread to almost every continent. Cases are rising daily in Africa, the Americas, and Europe.

This pandemic has unfortunately taken the lives of hundreds of thousands, caused a complete lockdown of many countries, pushed businesses to close, and even go bankrupt and blocked many people in foreign countries outside of their homes, including one of the authors.

In the absence of an efficient remedy or vaccines, Countries are racing to slow the spread of the virus by testing and treating patients, limiting travel, quarantining citizens, and canceling large gatherings such as sporting events, concerts, and schools.

Technology is also used to combat COVID-19 by taking advantage of the 4.5 billion mobile phones. The idea of tracing a person's movements using his mobile phone to find people they might have infected gained a lot of popularity, and by now most countries have issued a mobile application implementing contact tracing.

So, what is contact tracing? It simply consists of recording all people you might have encountered. If a person is found to be infected, the other persons are alerted to be tested or quarantined.

The Golden Standard

Before reviewing any of the contact tracing applications, it is first necessary to agree on a set of expectations or a standard that these applications must meet.

As far as we know, there hasn’t been any standard set anywhere. While some organizations speak about privacy, others discuss the efficiency of the tracing protocol and the guarantees it provides, there is no spec or guide that we could rely on.

We, therefore, had to draft our expectations based on a set of inputs we have collected and our best common sense.

These requirements are high level and are intentionally implementation agnostic:

  • Transparency
    • The tracing protocol is documented
    • The application architecture and design is documented
    • The application has a program to address privacy and security issues (Bug Bounty)
    • The application is open source to ease review and community vetting
  • Privacy
    • The application does not collect PII
    • The application does not track users or collect location data
    • The application uses a limited set of permissions on the device, mostly restricted to Bluetooth discovery and internet access
    • The application offers users control of their data for wipeout and sharing
  • Security
    • The application enforces a reduced attack surface, no ads, no payment or extraneous features
    • The application uses vetted and up-to-date 3rd party dependencies
    • The application communicates over a secure encrypted channel
    • The application is secure and does not leak any tracing information to any party
  • Tracing Efficiency
    • Tracing protocol doesn’t allow to track individuals
    • Sharing contact information is opt-in
    • Tracing does not share/collect location or PII data
    • Tracing doesn’t allow to retrace infected people
    • Tracing checks is not centralized and controlled by the user
    • Tracing works on Android and iOS even when the application is in the background or the screen is off
  • Accessibility
    • The application is accessible in several languages, including non-local users
    • The application can be used by non-residents (blocked in the country due to travel ban)
    • Application is usable by people with disabilities (color blindness, vision impairment, etc)
    • Application is usable by low-end phone (application size, battery consumption, low bandwidth, etc)

Methodology

Our test methodology consisted first of compiling a large list of Contact Tracing applications, focusing on governmental applications first, and prioritizing apps by the number of downloads.

As some applications require resident information, like passport number, local phone number, we focused on the applications we could test.

Each application has been first scanned using Ostorlab Security Scanner for security vulnerabilities and then manually reviewed on both Android and iOS. Each app has been reviewed for the 5 topics (Transparency, Privacy, Security, Tracing Efficiency, and Accessibility).

The full review is still an ongoing process due to the large number of applications.

TL;DR

The general findings were not satisfactory. Several applications implement custom tracing protocols that share Bluetooth and location data on a continuous basis, require PII, have a wide attack surface leading to vulnerabilities or abuse, some communicate over cleartext, and most have no transparency processes or documentation.

The upcoming section will dig deeper into a single application that uses the OpenTrace protocol The goal is not to single out a specific application but to share an example of our technical approach.

For the Wiqaytna Application:

Overall, the application does not suffer from critical vulnerabilities, it is privacy aware and the team has done their homework to make sure the application is adapted to the peculiarities of the Moroccan market.

  • Privacy-aware choice of tracing protocol
  • No collection of Personally Identifiable Information (PII) or location data
  • Sane implementation and technical stack choices
  • Real effort to make the process transparent and documented

Summary Issues:

Item Category Severity
iOS didn’t collect temporary ids when the app is not in the foreground Contact Tracing High
The same temp id is used even past the expiration period Contract Tracing / Privacy High
Missing unit test Code Quality / Security High
Vulnerable Dependency on iOS Security Medium
PIN validation is brute forceable Security Medium
Bluetooth service must be unique, instead it is using Singapore TraceTogther Test UUID Contract Tracing Low
Missing open source server implementation Transparency Low
Language support is limited to 2 languages, extend to other official languages and foreign languages Accessibility Low
Use of deprecated IPC mechanisms Code Quality / Security Low


alt text
mobile application security testing

Dig Deeper: Wiqaytna App

Transparency

The Moroccan application is one of the few open-source applications we had listed. Most of the source code is accessible at the Github repo except for the Backend source which just points to the OpenTrace repository. Analysis of the compiled application shows that the compiled version does match the open-source version.

alt text
mobile application security testing

The application relies on the OpenTrace protocol and implementation for contact tracing. The protocol is well documented, used by several other applications, like the Singaporian and Austrian apps.

The protocol has however suffered from several issues and has some tracing limitations on iOS.

alt text
mobile application security testing

The OpenTrace implementation is however not included as a library in the Moroccan application, but the code is baked directly into it. This creates a risk of divergence from the standard implementation, makes it harder to add fixes and updates. The Wiqaytna team needs to keep an eye on the changes and update to the repo to ensure all relevant changes are added.

OpenTrace is one of several published tracing protocols, TCN, Robert, Apple and Google tracing are other implementations. Some applications have gone the way of using their own implementations, leading to critical privacy issues.

alt text
protocols covid mobile application
Source:https://en.wikipedia.org/wiki/COVID-19_apps

While all of these protocols use temporary ids to protect against tracking, the main divergence is in the trace resolution process, meaning finding if a person was in contact with another infected individual.

OpenTrace uses a central tracing implementation, while TCN, Apple and Google implementations for instance are local.

While the latter - local, provides more privacy guarantees, it has implementation challenges on the bandwidth and battery consumption.

The reasoning behind the protocol choice by the Wiqaytna team was published in a white paper/

alt text
mobile application security testing

The white paper goes over several aspects of the applications, including the testing process, we have however found no unit tests or integration tests in the repo. The team has also added a security policy to report security issues.

alt text
mobile application security testing

Security, Privacy and Contact Tracing

Evaluating the security of a mobile application is a large topic. For the purpose of this article, we will only focus on the most interesting bits. Several aspects of the application have been reviewed automatically using Ostorlab vulnerability analysis.

The mobile application has a reduced attack surface. In comparison, several of the other contact tracing apps we have reviewed add more features like QRCode, include payment and ads libraries, expose URL schemes, content providers, Javascript interfaces.

3rd Party Dependencies

There are very few dependencies added to the applications, however at the time of the audits, one of the dependencies in the iOS application suffered from a known vulnerability. The dependency is fingerprinted from the Frameworks list and can also be confirmed in the Podfile.lock source code.

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

The vulnerable dependency is a transitive dependency from the Firebase libraries. Exploitability is however unlikely. The vulnerability affects nanopb version 0.3.9011. The description of the CVE-2020-5235 is:

There is a potentially exploitable out of memory condition In Nanopb before 0.4.1, 0.3.9.5, and 0.2.9.4. When nanopb is compiled with PB_ENABLE_MALLOC, the message to be decoded contains a repeated string, bytes or message field and realloc() runs out of memory when expanding the array nanopb can end up calling free() on a pointer value that comes from uninitialized memory. Depending on the platform this can result in a crash or further memory corruption, which may be exploitable in some cases. This problem is fixed in nanopb-0.4.1, nanopb-0.3.9.5, nanopb-0.2.9.4.

Backend and Network Communications

The application relies on Firebase to host its backend implementation, perform authentication, and upload tracing files.

Communication is over TLS and is properly validated. The server implementation is mostly based on the OpenTrace open-source implementation, but several other endpoints, like stats and update profiles (gender, department, etc) are not open-source.

The application uses Firebase Functions for all API calls, Firebase Storage to upload tracing files, and Firebase config to manage configuration.

  • Example of an endpoint on 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}
  • Example of an endpoint on 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
File Upload

One of the most important issues identified in the backend implementation affects the tracing upload functionality. The upload requires a PIN provided by the health authorities and used to retrieve an upload token.

alt text
mobile application security testing

The PIN validation is done through the getUploadToken method:

/**
 * 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}");
  }

The token validation is however NOT user specific. In the case of the Wiqaytna Application, the PIN is a 6 digits number. Thanks to the scale of Firebase, the whole 6 digits space was brute forced in a matter of minutes.

The PIN is actually not required to upload the tracing file, once authenticated, it is possible to upload any file with any name to the /o repo, see request examples below:

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

However the PIN is used to generate a token from the user’s UID. The token is then added to the uploaded file and validated before collecting and parsing the temporary ids.

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

The risk of overriding other users files, or simply reading uploaded files is mitigated using the Firebase security policy. Files are also archived and deleted once uploaded, limiting the possibility for any similar attacks.

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

The Firebase default policy would have been insecure as it allows read and write access to all authenticated users.

Authentication

The authentication is done using firebase service that retrieves a temporary token to communicate with the backend. The token is continuously refreshed in the background but requires several requests to complete the authentication handshake. See sample requests below:

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

During the traffic analysis, we confirmed that Wiqaytna does not collect any PII, but requires a moroccan phone number to receive the 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

The list of permissions requested by the application are all legitimate. They mainly concern Bluetooth and Internet access. Some users raised the concern about Location permission (ACCESS_FINE_LOCATION),it is however needed for Android Bluetooth access, see Android documentation for more details.

**Our analysis has shown that the application is not using or collecting any location coordinates. **

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

On 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>
Inter Process Communication (IPC)

The Android applications use several IPC schemes to communicate internally. Some of these schemes are dynamically registered using a local Broadcast Manager and then unregistered after processing.

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

Local Broadcast manager is however deprecated and should be replaced with LiveData or reactive streams.

Contact Tracing

As mentioned above, contact tracing uses the OpenTrace protocol. The protocol prevents tracking and reply attacks by creating temporary tokens valid for 15 min only, although other implementations, like COVIDSafe tweaked the protocol to 2 hours, increasing the risk of replay attacks.

The protocol is stateless, in the sense none of the temporary ids are stored on the server. Instead the temporary id includes the user’s UID encrypted using a backend secret key.

At registration, a batch of temp ids are generated and sent to the mobile application, see sample response below:

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

The temporary ids are then beaconed using Bluetooth Low Energy (BLE), the following is an example of stack traces collected from the Bluetooth service on Android. Production build is obfuscated, hence the cryptic one letter method names:

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

The service uses the UUID 17E033D3-490E-4BC9-9FE8-2F567643F4D3 and Characteristic UUID 117BDD58-57CE-4E7A-8E87-7CCCDDA2A804, these values are extracted from the binary and by scanning bluetooth services:

alt text
mobile application security testing

The UUID is however the same as the staging UUID used by the TraceTogether application. The Wiqytna team should have generated and used their own UUID.

Discoverability is straightforward on Android, but gets very hairy on iOS. Apple enforces restrictions on what applications can do while in background, limiting the possibility to advertise the tracing service. Apple has proprietary ways of using/abusing the standard by using an overflow area in the manufacturer value to advertise services in a hashed format.

alt text
mobile application security testing

alt text
mobile application security testing

The Open Trace repo has a long issue discussing ways to work around Apple Bluetooth limitations. For this particular reason, some countries have opted to use the Google and Apple tracing protocol as these gory details are handled by the OS, where Apple has full control of the bluetooth stack. Covid Watch is an example of another application that used more advanced tricks to have discovery work better cross platforms.

The characteristics exposes a JSON encoded message with the temporary id as well as other measurement information to help with distance calculation:

alt text
mobile application security testing

For the Moroccan application, our experimentation revealed two different issues:

1- On iOS devices, temporary id was not exchanged with other devices when the screen is locked or the application is set in the background. This was confirmed by inspecting the iOS database with the following schema:

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- On both Android and iOS devices, the application kept beaconing the same temporary id even after it should have expired:

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|

In the following extract from the Android database, the temporary id never changed, even past the expiry timestamp. Checking the iOS device, we can see that the temporary id should have expired at 1591821662 (epoch timestamp), but it is in reality beaconed past that value:

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

The application relies on cryptographic operations to generate and validate temporary ids and upload tokens.

The application uses a single algorithm and the same key for all operations, but has support for multiple keys. The open-source implementation mentions the use of the AES with either GCM (Galois Counter Mode) or CBC (Cipher Block Chaining), but doesn’t set any by default. It is unknown to us what implementation is used by Wiqaytna.

alt text
mobile application security testing

GCM offers stronger security guarantees as it is an authenticated encryption mode offering both encryption and integrity. CBC on the other hand suffers from several known attacks, like Bit Flipping and Oracle Padding Attacks.

The fact that the same key is used for several operations that require both encryption and decryption opens the door to attacks,like requiring an API to encrypt input we control and inject it to an API that decrypts.

Analysis of the source code didn’t demonstrate the presence of any of these attacks as all the inputs to the encryption methods are not user-controlled, except for the 6 digits PIN.

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

Logging on the Android application is centralized and disabled in the production build. For dynamic analysis, the logging API were hooked to collect application logs and understand what was happening during the tracing process:

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"

Access to the filesystem is also restricted to logging to the SDCard, which is also disabled in the production version.

Accessibility

The application is available in French and Arabic. While this is a good start, this doesn’t cover all official languages in the country (Arabic and Tamazight) and several tourists and residents still stuck in the country are neither French or Arabic speakers. Adding more languages like English and Spanish would help adoption.

The application requires a moroccan phone number to register. While we can see the intent of avoiding abuses, tourists will need to purchase a phone number to use the application.

The application is small in size thanks to the technology stack (Native Android and iOS), which is suitable for low end phones and has adapted the tracing protocol to network conditions in the country.

The application doesn't have any other accessibility features to help getting used by persons with disabilities.