Code Review – L’application Alerte Covid
Encore une nouvelle sĂ©rie đ Je sais… ça fait plusieurs, mais il y a tellement de choses Ă dire… Dans cette nouvelle sĂ©rie, j’aimerais regarder du code fait par d’autres, l’analyser et ressortir les points forts et les points faibles. Bref, faire un code review, mais sans prĂ©tention. Le but Ă©tant de s’amuser autour d’un morceau de code. Donc aujourd’hui, on commence par l’application Alerte Covid.
Qui en est l’auteur?
Ă la base, CovidShield est une application dĂ©veloppĂ©e des bĂ©nĂ©voles chez Shopify. Elle a Ă©tĂ© ensuite forkĂ©e et adaptĂ©e pour le Canada. L’application Alerte Covid qu’on utilise a Ă©tĂ© dĂ©veloppĂ©e par Service NumĂ©rique Canada (SNC). Cette compagnie dĂ©veloppe des solutions numĂ©riques avec le gouvernement fĂ©dĂ©ral. Les Ă©quipes sont mixtes, constituĂ©es de membres de l’entreprise et du ministĂšre avec lequel ils font affaires.
Vous pouvez consulter leur page GitHub sur laquelle ils publient leurs sources réguliÚrement.
Les technologies
L’application est constituĂ©e de 3 parties, du moins ce qu’on peut voir sur leur GitHub, soit :
- L’application mobile, qu’on doit installer sur nos tĂ©lĂ©phones
- Le backend, avec lequel communique l’application mobile
- Le portail, l’application pour ceux qui travaillent en santĂ©
Aujourd’hui, on se concentre sur la premiĂšre. Commençons par la base, depuis un dossier vide :
1 |
git clone https://github.com/cds-snc/covid-alert-app |
PremiĂšre Ă©tape, on constate qu’il y a un package.json à la racine. Un rapide coup d’Ćil nous permet de voir que l’application s’appelle CovidShield et qu’ils ont choisi React Native comme cadre de dĂ©veloppement, qui permet d’Ă©crire un seul code en React pour obtenir un build pour iOS et Android.
Allons donc chercher les prĂ©cieuses dĂ©pendances de l’application :
1 |
npm install |
Les tests unitaires
Bon… « juste » 500 mo de dĂ©pendances. On peut voir aussi qu’ils ont jest comme dĂ©pendance. Jest Ă©tant un framework de tests unitaires, voyons voir leur couverture :
1 |
npm run test |
13 suites de tests contenant 155 tests. Pour ceux qui ne seraient pas habituĂ© avec jest, aprĂšs l’exĂ©cution, il est possible de consulter un rapport d’exĂ©cution. Pour le voir, ouvrez le fichier coverage\lcov-report\index.html :
Personnellement, la couverture des tests me semble assez intĂ©ressante. Bien sĂ»r, on vise 100 %, mais ce n’est pas toujours possible ou pertinent. Un exemple concret de ça serait le bout de code suivant du BackendService :
En utilisant une expression lambda, on crĂ©e une fonction anonyme qui n’est pas testable. Pour la rendre testable, il aurait fallu l’extraire dans une mĂ©thode de la classe, mais ça n’aurait pas Ă©tĂ© pertinent de le tester. Cette unique ligne non testĂ©e explique le presque 2 % manquant au niveau de la couverture de cette classe.
Un point qui me titille, c’est le seul test qui n’est pas exĂ©cutĂ©. Le voici :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
it.skip('stores last update timestamp', async () => { const currentDatetime = new OriginalDate('2020-05-19T07:10:00+0000'); dateSpy.mockImplementation((args: any) => { if (args === undefined) return currentDatetime; return new OriginalDate(args); }); service.exposureStatus.append({ lastChecked: { timestamp: new OriginalDate('2020-05-18T04:10:00+0000').getTime(), period: periodSinceEpoch(new OriginalDate('2020-05-18T04:10:00+0000'), HOURS_PER_PERIOD), }, }); const currentPeriod = periodSinceEpoch(currentDatetime, HOURS_PER_PERIOD); when(server.retrieveDiagnosisKeys) .calledWith(currentPeriod) .mockRejectedValue(null); await service.updateExposureStatus(); expect(storage.setItem).toHaveBeenCalledWith( EXPOSURE_STATUS, expect.jsonStringContaining({ lastChecked: { timestamp: currentDatetime.getTime(), period: currentPeriod - 1, }, }), ); }); |
Par curiositĂ©, je l’ai activĂ© et j’ai rĂ©exĂ©cutĂ© les tests pour voir le rĂ©sultat. Il passe đ
Comment ça marche?
Ă partir d’ici, on va plonger dans le code. J’aimerais comprendre comment ils :
- tracent les rencontres entre 2 personnes
- communiquent avec le backend
- annoncent qu’on doit se faire tester
- valident qu’une personne a vraiment la covid
Le traçage via Bluetooth
Ce qu’on nous explique, c’est que l’application Ă©met un code alĂ©atoire via Bluetooth et que les autres tĂ©lĂ©phones gardent ce code pour une pĂ©riode de 14 jours. Commençons notre recherche dans le ExposureNotificationService.
On peut voir une mĂ©thode start() qui semble intĂ©ressante. Par contre, elle ne fait qu’appeler une fonction start() sur un objet injectĂ© via le constructeur :
1 |
exposureNotification: typeof ExposureNotification |
Ceci m’indique que la partie Bluetooth a Ă©tĂ© relayĂ©e Ă du code natif, et il y a effectivement un bout de code Kotlin qui dĂ©finit cette classe :
1 |
android\app\src\main\java\app\covidshield\module\ExposureNotificationModule.kt |
Puis, on se rend vite compte qu’ils dĂ©lĂšguent le tout Ă une librairie tierce qui est injectĂ©e via le contexte applicatif :
1 2 3 |
private val exposureNotificationClient by lazy { Nearby.getExposureNotificationClient(context.applicationContext) } |
On peut trouver cette librairie dans le rĂ©pertoire lib de l’application Android en Kotlin. Cette librairie est aussi open source. Elle a Ă©tĂ© dĂ©veloppĂ©e par Google.
On peut donc ici comprendre qu’ils n’ont pas voulu rĂ©inventer la roue au risque de faire des erreurs, ce qui est mature comme mentalitĂ©.
Communication avec le backend
Il y a quelques points intĂ©ressants Ă valider au niveau de la sĂ©curitĂ© dans un appel entre une front et un backend. PremiĂšrement, on s’attend Ă ce que cette communication soit chiffrĂ©e afin d’Ă©viter que quelqu’un puisse intercepter un message pour le lire. Ce qui est le cas ici.
D’ailleurs, si une personne a la possibilitĂ© d’intercepter un message, elle pourrait aussi potentiellement modifier la requĂȘte et / ou la rĂ©ponse afin de gĂ©nĂ©rer des comportements inattendus. Il faut donc une protection Ă ce niveau.
Revenons au BackendService. Perso, le nom đ mais bon. En regardant son API, on peut voir qu’il y a plusieurs mĂ©thodes pour communiquer avec le backend. Une premiĂšre mĂ©thode retrieveDiagnosisKeys() permet d’obtenir la liste des codes positifs dans la rĂ©gion. Nous y reviendrons dans la prochaine section.
Une autre mĂ©thode attire mon attention. Elle permet d’obtenir les configurations d’exposition, qui sont disponibles ici. Ce qui donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
{ "minimumRiskScore":1, "attenuationLevelValues":[ 0, 5, 5, 5, 5, 5, 5, 5 ], "attenuationWeight":50, "daysSinceLastExposureLevelValues":[ 1, 1, 1, 1, 1, 1, 1, 1 ], "daysSinceLastExposureWeight":50, "durationLevelValues":[ 0, 0, 0, 0, 5, 5, 5, 5 ], "durationWeight":50, "transmissionRiskLevelValues":[ 1, 1, 1, 1, 1, 1, 1, 1 ], "transmissionRiskWeight":50 } |
Ces configurations servent à mesurer le risque associé à une rencontre avec une autre personne.
Si vous ĂȘtes infectĂ©s et que vous avez saisi le code de SantĂ© Canada dans l’application, vous recevrez une notification pour transmettre les codes que vous avez croisĂ©s, advenant le cas oĂč vous deviez sortir. Ceci est gĂ©rĂ© par la variable needsSubmission de ExposureStatus. On peut donc comprendre que l’application fonctionne aussi pour les rencontres post-diagnostique.
Avant de traiter la fonction de rĂ©cupĂ©ration des cas positifs, traitons la soumissions des numĂ©ros alĂ©atoires croisĂ©s dans le cas oĂč on serait positif. Encore une fois, ils font preuve d’extrĂȘme prudence en implĂ©mentant un HMAC sur le corps de la requĂȘte avant l’envoi. Ceci permet au serveur de valider que le contenu de la requĂȘte n’a pas changĂ© entre l’envoi depuis le tĂ©lĂ©phone de la personne infectĂ©e jusqu’aux serveurs de SantĂ© Canada. De plus, cet encodage du payload utilise un nonce, qui permet d’ajouter un facteur alĂ©atoire afin de complexifier encore plus le processus de dĂ©codage.
Annonce qu’on doit se faire tester
Au dĂ©part, je pensais Ă un push du serveur vers notre appareil. J’Ă©tais un peu inquiet puisque ça signifierait que le serveur connait notre appareil. Mais ce n’est pas le cas đ
1 2 3 4 5 6 7 8 |
async retrieveDiagnosisKeys(period: number) { const periodStr = `${period > 0 ? period : LAST_14_DAYS_PERIOD}`; const message = `${MCC_CODE}:${periodStr}:${Math.floor(getMillisSinceUTCEpoch() / 1000 / 3600)}`; const hmac = hmac256(message, encHex.parse(this.hmacKey)).toString(encHex); const url = `${this.retrieveUrl}/retrieve/${MCC_CODE}/${periodStr}/${hmac}`; captureMessage('retrieveDiagnosisKeys', {period, url}); return downloadDiagnosisKeysFile(url); } |
Par soucis de performance, on rĂ©cupĂšre uniquement les nouvelles clĂ©s. On peut constater ici que l’application fait un GET au serveur, contrairement Ă ce que je pensais initialement. Non seulement ça, mais ils ont aussi mis un HMAC Ă la fin de l’URL en chiffrant les paramĂštres de l’URL. Vous ne pourriez donc pas appeler cette URL sans avoir la clĂ© HMAC qui est Ă©videmment tenue secrĂšte :
1 |
HMAC_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Et zut đ
Validation qu’une personne a rĂ©ellement la covid
Pour Ă©viter que n’importe qui se dĂ©clare positif et sĂšme la zizanie, SantĂ© Canada va crĂ©er un code alĂ©atoire Ă usage unique et vous le remettre. Vous avez la responsabilitĂ© de saisir ce code dans l’application, qui va faire un GET au serveur pour valider que ce code est valide.
Si le code est valide, le serveur va retourner des clĂ©s de soumission afin d’encrypter le payload. L’utilisation de clĂ©s spĂ©ciales provenant du serveur pour la soumission me fait penser que les clĂ©s sont Ă usage unique et surtout alĂ©atoire, donc gĂ©nĂ©rĂ©es Ă la demande. On pourra valider le tout quand on regardera le code du backend.
Une fois les clĂ©s reçues, un ExposureStatus de type ExposureStatusType.Diagnosed est transmis au backend, de la mĂȘme façon que les autres codes d’exposition sont envoyĂ©s (voir plus haut).
Conclusion
Est-ce qu’on peut dire que cette application est sĂ©curitaire? Well… je pense sincĂšrement qu’il n’y a pas moyen de faire mieux en terme de sĂ©curitĂ© pour ce cas d’utilisation prĂ©cis. Ils ont pris toutes les prĂ©cautions pour Ă©viter les faux positifs, le spoofing, le man-in-the-middle et probablement bien d’autres qui m’Ă©chappent.
Je l’ai installĂ©e. Et toi?
Si t’as aimĂ© cet article, je t’invite Ă t’abonner au blogue pour recevoir les nouveaux articles par courriel. Je t’invite aussi Ă le partager dans tes rĂ©seaux sociaux đ
Commentaires
9 commentaires
Excellent article!
Super intéressant, merci pour ce travail.
TrÚs intéressant
Beau travail Sylvain
Cool article! Merci dâavoir creusĂ© un peu dans le code!
Jâai aussi installĂ© lâapp đ
Beau travail et bien expliquĂ©. Bravo đ
Bravo! TrĂšs bien dĂ©cortiquer et vulgariser. C’Ă©tait un vrai plaisir Ă lire!
Excellente analyse ! Clair et concis ! Bravo !
Merci!
Laisser un commentaire