Aujourd'hui, tout le monde veut créer la prochaine alternative à WhatsApp, Signal ou Telegram. Sécurisée, chiffrée de bout en bout (E2EE), légère, infaillible.
Quand j'ai décidé de construire l'application de messagerie Anonyme en utilisant React Native (Expo) et Supabase, je me suis dit : "Combien est-ce que ça peut être difficile ? On génère deux clés, on chiffre, on envoie sur des WebSockets, on déchiffre. Terminé."
Mais la cryptographie dans l'écosystème mobile JavaScript... c'est une autre paire de manches. Entre l'asynchronie capricieuse et les limites des outils de dev, voici comment cette architecture, théoriquement simple, a failli me rendre fou.
L'Échec de l'Algorithme Maison & L'Enfer d'Expo Go
L'histoire commence bien avant les premières lignes de code. Cela faisait un an que je réfléchissais à un algorithme de chiffrement personnalisé avec un camarade. Le plan était parfait sur le papier. Mais dès qu'on l'a intégré au projet mobile : black-out. Le déchiffrement refusait catégoriquement de fonctionner.
Pendant une semaine complète, j'ai revu mes calculs mathématiques, retourné le code dans tous les sens... en vain. Pour couronner le tout, à force de recharger l'application pour tester mes modifications algorithmiques, j'ai totalement épuisé ma limite d'utilisation sur Expo Go. Bloqué au milieu du tunnel.
J'ai dû me rendre à l'évidence : pour avancer, il fallait temporairement laisser tomber notre algo maison et basculer sur des standards plus basiques (comme X25519 avec TweetNaCl.js). Mais ce n'est que partie remise, ça finira par fonctionner.
Cette frustration m'a poussé à une réflexion cruciale : la sécurité d'une application ne dépend pas uniquement de la complexité de son algorithme de chiffrement. J'ai donc réorienté mon énergie vers des fonctionnalités de sécurité terrain massives pour blinder l'application.
1. Le Cauchemar du TextEncoder Manquant
En cryptographie standard, tout fonctionne avec des tableaux d'octets (Uint8Array). Ton mot de passe ? Uint8Array. Ton message secret "Salut !" ? Uint8Array.
Pour passer d'une chaîne de texte JavaScript vers un Uint8Array, tous les navigateurs utilisent TextEncoder. Le problème ? React Native ne possède pas de TextEncoder ou TextDecoder natif en V8/JSC.
Je me retrouvais face à des écrans rouges explosifs à chaque tentative de chiffrement car l'application ne savait pas comment convertir des strings JavaScript. J'ai dû implémenter le polyfill text-encoding-polyfill au sommet de l'arbre d'exécution (_layout.tsx) juste pour forcer l'environnement mobile à lire de simples phrases :
import 'react-native-get-random-values';
import 'text-encoding-polyfill';
2. Le Base64 : Un Espion Infiltré
Une fois les données chiffrées, il fallait bien envoyer ces bits illisibles via Supabase. La méthode classique : encoder le Uint8Array chiffré en Base64 dans une base Postgres.
Mais au moment de récupérer les messages via les requêtes temps-réel (subscribe) de Supabase, mes boîtes de dialogue affichaient... un null massif.
Pourquoi ? L'encodage Base64 n'est pas un standard si universel. Des caractères générés par certaines librairies venaient corrompre la vérification de longueur de TweetNaCl. J'ai été forcé d'ajouter une couche de nettoyage draconienne et une stricte validation d’encodage via le module buffer :
const cleanBase64 = (str: string) => {
return str.replace(/-/g, '+').replace(/_/g, '/').replace(/\\s/g, '');
};
const decodedData = Buffer.from(cleanBase64(encrypted_content), 'base64');
3. La Base de Données Éphémère & "Tu ne supprimeras point"
Pour pousser le concept "Zéro Trace" à l'extrême, j'ai configuré la base de données pour qu'elle agisse comme une simple boîte aux lettres : le serveur ne stocke le message chiffré que si le destinataire est hors ligne. Dès que le client est connecté, récupère le payload et le déchiffre localement, l'application exécute un supabase.delete.
C'est là que le Row Level Security (RLS) de Supabase est entré en guerre contre moi. Les règles de sécurité serveur empêchaient silencieusement l’utilisateur de déclencher l'action DELETE sur les lignes qui ne lui appartenaient pas "strictement" en droit total. La fonction échouait sans crasher, gardant le flag is_read = false sur le serveur et faisant s'emballer mon compteur de messages non-lus sur l'accueil.
La Solution Architecturale :
N'ayant pas de contrôle total sur le Back-End du cloud, j'ai appliqué une double stratégie :
-
La rustine serveur : Un
supabase.update({is_read: true})pour contourner la barrière du Delete total. -
La timeline locale : J'ai utilisé
AsyncStoragepour sauvegarder la milliseconde précise de fermeture d'un chat (last_opened_chat). L'application filtre désormais d'elle-même : si un message sur le serveur est daté d'avant cette marque temporelle, il est considéré comme consommé et ignoré, peu importe l'état du flag sur le serveur.
4. Chronomètres Éphémères et Décalage Spatio-Temporel
Je voulais intégrer un mode "Message Éphémère" avec un délai d'auto-suppression personnalisable (par exemple 30 secondes), digne de la NSA.
Première version : J'insérais la limite sur le serveur au moment de l'envoi (created_at + 30s). Résultat catastrophique : si le destinataire n'ouvrait pas l'app pendant une minute, le message périssait sur le serveur avant même d'arriver sur son téléphone ! Pire : si l'expéditeur se déconnectait, il perdait son propre historique car le chronomètre avait enterré le message à distance.
J'ai totalement modifié la chorégraphie du minuteur. Le temps ne doit pas s'écouler pendant le voyage ; il ne s'active qu'à destination :
const expirySeconds = msg.type?.split(':')[1] ? parseInt(msg.type.split(':')[1], 10) : null;
// Le chronomètre ne commence... EXACTEMENT MAINTENANT chez le destinataire.
const localExpiresAt = new Date(Date.now() + expirySeconds * 1000).toISOString();
Le serveur n'a aucune idée de la "Date Limite". C'est conféré dans le payload sécurisé transféré au destinataire. Le message attend sagement, et explose sous les yeux de l'utilisateur 30 secondes après l'ouverture.
Le Blindage Final : Zéro Capture d'Écran
Pour parfaire cette suite de sécurité, il restait une faille humaine : la capture d'écran. Chiffrer de bout en bout ne sert à rien si l'utilisateur peut photographier la discussion.
J'ai intégré des modules natifs pour bloquer instantanément toute tentative de screenshot ou d'enregistrement vidéo dès qu'on entre dans l'écran de discussion. Sur Android, l'application force le système à retourner un écran noir complet.
Conclusion
Construire un système End-To-End Encrypted va bien au-delà de la simple théorie mathématique. C'est affronter les décalages de synchro des bases de données temps réel, débugger les lacunes du moteur JavaScript de React Native sur des modules noyaux comme Buffer, et réajuster ses ambitions quand l'environnement de dev lâche prise.
Mais à la fin, quand tes requêtes passent, que les clignotants de l'authentification asymétrique passent au vert et que ton système est scellé ?
C'est là que réside toute la beauté de l'ingénierie logicielle.
(N'hésitez pas à me suivre, d'autres aventures de debugs brutaux arrivent bientôt !)


Top comments (0)