[01]Contexte
Privy est né de l'envie de concevoir une messagerie instantanée confidentielle, sans dépendre d'un géant centralisé. L'objectif était le principe de divulgation nulle de connaissances : les serveurs ne devaient être que des relais aveugles de paquets chiffrés, sans jamais pouvoir lire les conversations ni accéder aux clés symétriques.
Fonctions : inscription par pseudonyme avec génération de la paire de clés sur l'appareil, invitations sécurisées, échanges en temps réel avec indicateurs de frappe, application Android via Capacitor et ébauche de partage de fichiers.
[02]Architecture
Client Vue 3 / Vite avec Pinia, encapsulé en application Android via Capacitor. Côté serveur : une API REST Express (authentification JWT, profils, invitations), un serveur WebSocket (ws) pour le relais des messages chiffrés et MySQL 5.7, le tout orchestré avec Docker.
1. Identité
À l'inscription, le navigateur génère une paire RSA-OAEP 2048 bits (SHA-256) via l'API Web Crypto. La clé publique est envoyée à l'API et stockée côté serveur.
2. Enveloppe
L'initiateur génère une clé AES-GCM 256 bits, la chiffre avec la clé publique du destinataire et l'envoie dans une invitation. Le destinataire la déchiffre avec sa clé privée locale.
3. Messages
Chaque message est chiffré localement en AES-GCM, avec un IV aléatoire de 12 octets placé devant le texte chiffré, puis encodé en Base64. Le serveur WebSocket ne reçoit qu'un blob opaque.
[03]Ce que l'audit a révélé
Voici les 9 failles majeures relevées par l'audit, avec le constat et son impact.
9 failles affichées
- FAILLE 01 • SÉCURITÉ
Clé privée RSA en clair dans le localStorage
La clé privée RSA et les clés de conversation sont écrites en clair dans le localStorage : une seule faille XSS suffit à les extraire et à anéantir le chiffrement de bout en bout.
- FAILLE 02 • PERFORMANCE
pbkdf2Sync bloque l'Event Loop Node.js
Le hachage des mots de passe utilise pbkdf2Sync (1 000 itérations), qui mobilise le thread unique de Node.js à chaque appel : sous charge, l'API ne répond plus.
- FAILLE 03 • RÉSEAU
Revalidation HTTP des sessions toutes les 30 s
Le serveur WebSocket n'a accès ni à la base ni à un cache partagé : il revalide chaque session par une requête HTTP toutes les 30 s, soit environ 2 000 requêtes par minute pour 1 000 clients, pour vérifier des jetons déjà signés.
- FAILLE 04 • ARCHITECTURE
Modèle relationnel limité au 1 contre 1
La table Conversations repose sur deux colonnes creator_id et participant_id : les salons de groupe sont impossibles sans reconstruire le schéma et réécrire les contrôleurs.
- FAILLE 05 • PERFORMANCE
Déconnexion en O(R) sur le nombre de salons
À la déconnexion, le serveur WebSocket parcourt tous les salons en mémoire pour détacher le socket : un coût linéaire au lieu d'un coût constant.
- FAILLE 06 • BASE DE DONNÉES
Fichiers découpés en blocs de 255 octets dans MySQL
Un fichier de 5 Mo aurait demandé plus de 20 000 lignes en base. La fonctionnalité a été abandonnée en cours de développement et ses routes désactivées.
- FAILLE 07 • CONFORMITÉ
Permissions Android sur-provisionnées
Le manifeste déclarait la localisation précise, la caméra et le micro, ajoutés lors d'essais pour résoudre un problème d'accès au stockage : une violation du moindre privilège, qui aurait provoqué un rejet à la soumission sur le Google Play Store.
- FAILLE 08 • FIABILITÉ
Jeton périmé et reconnexion infinie
Le jeton JWT n'était lu qu'une fois, à l'évaluation du module : après la connexion, les autres composables gardaient une valeur nulle et provoquaient des erreurs 401 jusqu'au rechargement de la page. Le WebSocket tentait aussi de se reconnecter toutes les 5 s après une déconnexion volontaire.
- FAILLE 09 • SÉCURITÉ
Secrets versionnés, valeurs triviales
Les fichiers .env sont suivis par Git depuis le premier commit, avec des valeurs triviales. Déployée telle quelle, cette configuration aurait permis de forger des jetons valides pour n'importe quel utilisateur. Je les ai volontairement laissés en place dans la rétrospective, comme exemple visible d'une erreur que je ne commets plus.
[04]Ce que je ferais aujourd'hui
Si Privy devait être réécrit selon les standards actuels, voici ce que je changerais.
| Élément | Avant (PoC) | Après (cible) |
|---|---|---|
| Clés | Avant (PoC) Clé privée en clair dans localStorage | Après (cible)Clés non extractibles dans IndexedDB |
| Mots de passe | Avant (PoC) pbkdf2Sync bloquant | Après (cible)Argon2id asynchrone |
| Session WebSocket | Avant (PoC) Requête HTTP toutes les 30 s | Après (cible)Validation locale du JWT |
| Salons | Avant (PoC) Colonnes creator / participant (1 contre 1) | Après (cible)Table ConversationMembers |
| Fichiers | Avant (PoC) Micro-blocs de 255 octets dans MySQL | Après (cible)Stockage objet S3 / MinIO |
| État du front | Avant (PoC) Variables globales de modules JS | Après (cible)Store Pinia, intercepteurs Axios centralisés |
| Permissions Android | Avant (PoC) GPS, caméra, micro déclarés | Après (cible)INTERNET seul, demandes au runtime |
| Secrets | Avant (PoC) .env versionnés, valeurs triviales | Après (cible).env.example, secrets forts injectés, détection en CI |
[05]Ce que ça a déclenché
La certification CCNA, le durcissement de ce portfolio (CSP avec nonce, déploiement signé HMAC, isolation réseau) et la veille CVE intégrée au site.
[06]Limites
- Le projet est archivé : c'est un PoC éducatif, qui ne doit jamais être déployé en production.
- Il repose sur des dépendances obsolètes (dont MySQL 5.7, en fin de vie) et présente les failles décrites ci-dessus. Les valeurs des .env versionnés sont publiques.