Cigarettes

Pourquoi ce projet

Toutes les messageries "sécurisées" grand public partagent le même point faible : quelque part, il y a un serveur. Un serveur qui route les messages, qui connaît (au minimum) qui parle à qui et quand, et qui constitue un point de défaillance unique aussi bien technique que juridique. Même quand le chiffrement de bout en bout est irréprochable, les métadonnées restent souvent en clair, et l'infrastructure centralisée reste une cible.

Je voulais un outil qui ne fasse aucune concession sur ces deux points : ni serveur central, ni métadonnées réseau exploitables. Le réseau Tor, via ses services onion v3, répond exactement à ce besoin : deux pairs peuvent se joindre directement sans jamais exposer leurs adresses IP respectives, sans DNS, et sans tiers de confiance.

L'autre motivation était plus personnelle : je voulais me confronter sérieusement à la cryptographie appliquée en C++ moderne. Pas l'utiliser à travers une bibliothèque haut niveau qui masque tout, mais manipuler directement OpenSSL 3, implémenter un handshake ECDH, gérer moi-même la dérivation de clés et le chiffrement authentifié, et me poser les questions désagréables que ça implique : où finissent mes clés privées en mémoire ? Que se passe-t-il si un pair m'envoie une trame corrompue ? Comment je gère un reset d'identité sans casser la compatibilité réseau ?

Ce billet de blog détaille l'architecture technique du projet : le transport réseau, le handshake cryptographique, le chiffrement des messages, la gestion des identités, et les protections mises en place contre l'analyse de trafic.

Vue d'ensemble

Cigarettes est un client CLI en C++17, sans dépendance à un quelconque serveur applicatif. Chaque instance :

Le projet est structuré en modules assez indépendants : TorManager pour le cycle de vie de Tor, Network pour la couche transport et le SOCKS5, CryptoManager pour l'identité à long terme, CryptoSession pour le secret de session, HostManager pour le carnet d'adresses local, ProtocolHandler pour la machine à états applicative, et CommandHandler pour l'interface en ligne de commande.

Anonymat réseau : Tor de bout en bout

Le TorManager gère tout le cycle de vie de Tor : détection d'un binaire système compatible, téléchargement et extraction du bundle officiel si aucun n'est trouvé, génération d'un fichier torrc dédié (répertoire de données isolé, port SOCKS, service caché pointant vers le port applicatif local), lancement du processus, puis attente active de la création du fichier hostname du service caché pour récupérer l'adresse .onion générée.

Côté connexion sortante, plutôt que de m'appuyer sur une bibliothèque SOCKS toute faite, j'ai implémenté à la main le sous-ensemble du protocole SOCKS5 nécessaire pour parler à Tor : négociation "no auth", puis requête CONNECT avec adresse de type domaine (le .onion cible) et port 80. La réponse est parsée manuellement selon le type d'adresse retourné (IPv4, IPv6 ou domaine) pour vider correctement le socket avant de considérer la connexion établie.

Côté entrant, un thread listener_loop écoute en permanence sur le port local relié au service caché. Par choix de conception, une seule connexion pair est acceptée à la fois : toute tentative supplémentaire est immédiatement refusée, ce qui simplifie beaucoup la gestion d'état côté protocole applicatif.

Le handshake cryptographique

L'identité à long terme d'un utilisateur repose sur une paire de clés EC sur la courbe secp384r1, générée par CryptoManager si aucune clé privée n'existe encore. Cette clé peut être chiffrée sur disque avec une passphrase (AES-256-CBC via PEM_write_PrivateKey), et le programme détecte si le fichier est chiffré en cherchant le marqueur ENCRYPTED dans l'en-tête du PEM, avant de réclamer une passphrase si nécessaire.

L'empreinte (fingerprint) qui identifie chaque utilisateur sur le réseau est un simple SHA-256 de la clé publique, encodé en hexadécimal. C'est cette empreinte qui sert de socle à toute la confiance applicative : comme une adresse onion v3 est elle-même dérivée cryptographiquement de sa clé, l'association adresse/empreinte enregistrée dans le carnet d'adresses local suffit à détecter une usurpation.

Une fois la connexion Tor établie entre deux pairs, CryptoSession prend le relais pour établir le secret de la session :

  1. Chaque pair génère une paire de clés EC éphémère et un nonce aléatoire.
  2. Les clés publiques éphémères et les nonces sont échangés.
  3. Chaque pair calcule un secret partagé par ECDH (EVP_PKEY_derive) à partir de sa clé privée éphémère et de la clé publique éphémère reçue.
  4. Ce secret est ensuite étiré via HKDF-SHA256, avec un sel constitué de la concaténation des deux nonces et une chaîne d'info fixe servant de contexte de dérivation, pour produire une clé de session de 256 bits.

Le fait de dériver le sel à partir des deux nonces garantit qu'une même paire de clés éphémères ne produira jamais deux fois la même clé de session, même en cas de réutilisation accidentelle d'un nonce d'un seul côté.

Chiffrement des messages et anti-analyse de trafic

Une fois la clé de session dérivée, tous les échanges applicatifs (chat, ping, transfert de fichier) passent par AES-256-GCM : un nonce de 12 octets tiré aléatoirement par message, un tag d'authentification de 16 octets, le tout concaténé dans un seul payload envoyé sur le réseau. Chaque message est donc indépendamment authentifié : toute altération, volontaire ou non, fait échouer le déchiffrement plutôt que de produire silencieusement des données corrompues.

Au niveau du transport, Network::format_message construit une trame avec un type de message sur 8 octets, le payload chiffré, un timestamp sur 8 octets, puis un bourrage aléatoire calculé pour que la taille totale de la trame (en-tête de longueur compris) soit toujours un multiple de 512 octets. L'objectif est de limiter ce qu'un observateur du trafic Tor peut déduire de la simple taille des paquets échangés, une contre-mesure classique contre l'analyse de trafic par empreinte de taille de message.

La réception fonctionne en miroir dans io_loop : accumulation d'un buffer, extraction de trames complètes selon l'en-tête de longueur, retrait du bourrage à partir de sa longueur encodée en fin de trame, puis mise en file des messages complets. Toute trame annoncée avec une taille aberrante (au-delà d'un seuil de sécurité) ou un bourrage incohérent provoque la fermeture immédiate de la connexion plutôt qu'une tentative de récupération, pour éviter qu'un pair malveillant ne puisse exploiter un parsing trop permissif.

Confiance et gestion des identités

HostManager maintient un carnet d'adresses local (known_hosts.json) associant chaque empreinte de clé publique connue à une adresse onion, un pseudonyme, un niveau de confiance et des horodatages de première/dernière rencontre. C'est une bascule volontairement simple de type "trust on first use" : Cigarettes ne cherche pas à remplacer une vérification hors bande de l'empreinte, mais à la rendre pratique une fois qu'elle a eu lieu.

La commande /reset_key permet de régénérer entièrement l'identité cryptographique (donc l'empreinte, et de facto l'adresse onion associée) en cas de compromission suspectée, avec un avertissement explicite que les pairs existants ne pourront plus joindre l'utilisateur à son ancienne adresse.

Durcissement mémoire

Un std::string classique laisse potentiellement des copies fantômes en mémoire après sa libération, ce qui est problématique pour des clés privées ou des passphrases. Le projet s'appuie sur des types dédiés, SecureString et SecureVector, utilisés systématiquement pour tout ce qui transite par la cryptographie (passphrase, nonces, secret partagé, clé de session), avec effacement explicite (OPENSSL_cleanse) dès que ces objets sortent de portée. La clé privée EC elle-même (EVP_PKEY) est libérée proprement dans le destructeur de CryptoManager, sans jamais être dupliquée en dehors de son propriétaire logique.

Transfert de fichiers

Le transfert de fichiers réutilise exactement la même session chiffrée que les messages texte : pas de canal ni de négociation cryptographique séparée. Il est piloté de façon asynchrone par rapport à la boucle d'interface, avec un mécanisme d'acceptation/refus explicite côté destinataire (/accept et /reject), pour éviter qu'un pair ne puisse pousser un fichier sans consentement.

Limites actuelles et pistes

Le modèle de confiance reste volontairement minimaliste : il n'y a pas encore de vérification hors bande automatisée des empreintes, et une seule connexion pair active à la fois est supportée. La bibliothèque partagée (libCigarettes.so) existe déjà dans le build mais n'est pas encore exploitable, l'objectif à terme étant de permettre une intégration dans d'autres projets sans repartir du CLI.

Il ne faut également pas oublier que le réseau TOR n'est pas exempt de défaut, la plupart des noeuds de sortis sont contrôlés par des agences de renseignements, et ce système reste exposés à des failles et à des comportements personnels qui fragilisent l'anonymat donné par le réseau.

Une seconde piste est de créer une version "relai" de Cigarettes qui garde une liste de personnes connectés (avec un heartbeat pour être à jour sur les participants), et qui ne ferait que retransmetre les messages venant d'un utilisateur, aux autres. Cette modification serait simple et rapide à mettre en place. Via cette méthode, la conversation de groupe serait possible sans qu'aucun des participants ne brise l'anonymat offerte par le réseau TOR. Cependant cette solution brise la règle de l'absence de serveur central ; Certes, tout le monde pourrait héberger son relai facilement, mais les participant à la conversation de groupe doivent faire confiance au relai.

Conclusion

Cigarettes reste avant tout un projet d'apprentissage assumé, mais construit avec la même rigueur qu'un outil destiné à être réellement utilisé : les décisions de conception (padding des trames, effacement mémoire, authentification stricte de chaque message) répondent à des menaces concrètes plutôt qu'à une case à cocher. Le code est disponible pour qui veut l'auditer, le critiquer, ou s'en inspirer.