
Chat sécurisé en terminal. Serveur de retransmission aveugle sans métadonnées, chiffré de bout en bout. PyNaCl XSalsa20-Poly1305 + Ed25519 + secret de transmission. Python multiplateforme.
Chat de groupe chiffré de bout en bout, messages privés et transfert de fichiers dans votre terminal. Le serveur est un relayeur aveugle : il ne peut pas lire vos messages, ne connaît pas votre nom d'utilisateur, ne connaît pas le salon dans lequel vous vous trouvez et ne peut lier aucun message à une même personne, même s'il est entièrement compromis.
https://github.com/user-attachments/assets/d9faabfb-73bd-46dd-92b2-23f63daf5b06
https://github.com/user-attachments/assets/e8e0220d-cd7d-45a2-9443-9a5f20b57f12
https://github.com/user-attachments/assets/15fb383d-a02a-433e-bbd9-8ebadecf9481
https://github.com/user-attachments/assets/bca10cb1-6959-425d-96d6-fc1fbf845538
NoEyes est un outil de chat en terminal Python pour les petits groupes de confiance. Le serveur ne déchiffre jamais rien et ne voit jamais qui vous êtes – il ne gère que des jetons opaques et transmet des octets chiffrés.
Vous générez la clé, vous la partagez hors bande, et le serveur n'apprend rien de vos conversations.
Utile pour les petits groupes de confiance qui souhaitent des communications chiffrées sans faire confiance à un serveur tiers, héberger eux-mêmes un chat privé avec un véritable chiffrement de bout en bout, ou toute personne qui souhaite comprendre exactement ce qu'un serveur peut ou ne peut pas voir.
python ui/setup.py
python ui/launch.py
`ui/launch.py` vous guide pour démarrer un serveur ou s'y connecter.
---
### Option B - Si Python n'est pas encore installé
| Plateforme | Exécutez d'abord |
|---|---|
| Linux / macOS / Termux / iSH | `sh install/install.sh` |
| Windows | `install\install.bat` |
Les deux scripts installent Python s'il est manquant, puis passent automatiquement à `setup.py`.
---
### Option C - Manuel```bash
# 1. Install dependencies
pip install cryptography PyNaCl
# 2. On the server machine — generate the access key
python noeyes.py --generate-access-key
# Prints an access code hex string — share with clients via USB
# 3. On a client machine — generate chat.key from the access code
python noeyes.py --generate-chat-key <ACCESS_CODE_HEX> --key-file ./chat.key
# Distribute chat.key to all other clients via USB. Never put it on the server.
# 4. Start the server (does NOT need the key file)
python noeyes.py --server --port 5000
# Start without bore tunnel (LAN / static IP / custom tunnel)
python noeyes.py --server --port 5000 --no-bore
# Start without adding a firewall rule (not needed when using bore tunnel)
python noeyes.py --server --port 5000 --no-firewall
# 5. Connect clients - each person needs their own identity file
python noeyes.py --connect SERVER_IP --port 5000 --username alice --key-file ./chat.key --identity-path ~/.noeyes/identity_alice.key
python noeyes.py --connect SERVER_IP --port 5000 --username bob --key-file ./chat.key --identity-path ~/.noeyes/identity_bob.key
Important : Chaque utilisateur doit avoir son propre fichier d'identité. Deux clients partageant le même fichier d'identité obtiennent le même jeton de boîte de réception et le serveur rejettera le second comme une session en double. Le fichier d'identité est généré automatiquement lors de la première exécution, transmettez simplement un
--identity-pathunique par utilisateur.
Téléchargez Termux depuis F-Droid (recommandé) : https://f-droid.org/packages/com.termux/
Gardez la session active - installez tmux pour que NoEyes continue de fonctionner lorsque vous changez d'application :```bash pkg install tmux -y tmux python ui/launch.py
**Permissions de stockage** - le transfert de fichiers échouera sans cela :```bash
termux-setup-storage
▶.Chaque moitié défile indépendamment. Appuyez sur ^P pour masquer le panneau et avoir une vue plein écran du chat.
Préfixez tout message avec une !tag pour le colorer pour tout le monde et déclencher un son de notification. Les étiquettes voyagent à l'intérieur de la charge utile chiffrée, le serveur ne les voit jamais.
Exemples :``` !danger server is going down in 5 minutes !ok deployment successful !req can someone review my PR?
Sounds play from `sfx/` folder. Drop in `.wav`, `.mp3`, `.ogg`, `.aiff`, `.flac`, or `.m4a` files named after the tag (e.g. `sfx/danger.wav`). Falls back to terminal bell if not found. Use `/notify off` to disable all sounds.
---
## Architecture
> 🗺️ **[Carte de sécurité interactive en direct](https://ymsniper.github.io/NoEyes/)** — Répartition visuelle de l'architecture complète de chiffrement, du modèle de menace et du routage sans métadonnées dans un diagramme interactif.```
┌──────────────────────────────────────────────────────────────────────┐
│ Alice ──────────────────────────────────────────── Bob │
│ │ Encrypted payload (opaque) │ │
│ │ │ │ │
│ └────────────► SERVER ─┴◄──────────────────────────┘ │
│ │ │
│ Zero-metadata blind forwarder: │
│ routes by opaque inbox tokens only │
│ { "to": "3f9a1c...", "type": "privmsg" } │
│ forwards encrypted bytes verbatim │
└──────────────────────────────────────────────────────────────────────┘
WHAT THE SERVER SEES: WHAT THE SERVER NEVER SEES:
· Encrypted bytes it can't read · Usernames or display names
· Opaque inbox tokens (blake2s) · Room names
· Opaque room tokens (blake2s) · Who is messaging whom
· Frame byte length · Message content
· Connection timing · File contents
· Ed25519 public keys
· DH key exchange values
Chaque client calcule localement deux jetons opaques avant de se connecter :``` inbox_token = blake2s(identity_vk_bytes, digest_size=16) room_token = blake2s((room_name + group_key_hex).encode(), digest_size=16)
Le serveur achemine toutes les trames uniquement par ces jetons. Il ne stocke jamais les noms d'affichage, les noms de salle ou les clés publiques. L'identité de l'expéditeur voyage **à l'intérieur** de la charge utile chiffrée (expéditeur scellé), et non dans l'en-tête de routage.
### Chaîne de dérivation des clés```
chat.key (shared secret)
│
├─ BLAKE2b("general") ──► room_key["general"] (isolated per room)
├─ BLAKE2b("dev") ──► room_key["dev"]
└─ BLAKE2b("ops") ──► room_key["ops"]
X25519 DH (per user pair, automatic on first /msg)
alice_ephemeral + bob_ephemeral ──► shared_secret
│
BLAKE2b
│
pairwise_key (private messages)
│
BLAKE2b(transfer_id) ──► chacha20_key (files)
password + random_salt (32 bytes, os.urandom) │ └─ BLAKE2b(password, key=salt, person="identity_v2") │ derived_key ──► encrypts Ed25519 signing key at rest
Chaque fichier d'identité reçoit un sel aléatoire unique, les tables arc-en-ciel sont inutiles.
---
## Résumé de la sécurité
| Couche | Mécanisme | Notes |
|---|---|---|
| Secret de transmission (ratchet) | Clés d'expéditeur — BLAKE2b chaîne KDF + XSalsa20-Poly1305 par message | Clé unique par message, avance rapide pour les messages manqués |
| Discussions de groupe | XSalsa20-Poly1305 (PyNaCl secretbox) | Clé par salon via BLAKE2b |
| Messages privés | XSalsa20-Poly1305 avec clé par paire X25519 | Signé Ed25519, vérifié TOFU |
| Transfert de fichiers | ChaCha20-Poly1305 | Clé par transfert via BLAKE2b, signé Ed25519, pause/reprise après reconnexion |
| Identité de l'expéditeur | Expéditeur scellé | Nom d'utilisateur + signature dans le payload chiffré, jamais dans l'en-tête de routage |
| Identité | Paire de clés Ed25519 | Fichier d'identité par utilisateur, chiffré par mot de passe avec BLAKE2b + sel aléatoire |
| Dérivation de clés | BLAKE2b (PyNaCl) | Séparé par domaine via paramètre de personnalisation, pas de tables arc-en-ciel |
| Routage serveur | Jetons blake2s opaques | Le serveur ne stocke jamais les noms d'utilisateur, noms de salon ou clés publiques |
| Transport | TLS (activé par défaut) | Épinglage de certificat TOFU, échec de correspondance d'empreinte interrompt la connexion |
| Intégrité DH | Clés publiques DH signées Ed25519 | Empêche l'homme du milieu sur l'échange de clés par paire |
| Protection contre la relecture | Deque d'ID de message par salon | Les messages rejoués sont silencieusement ignorés |
| Protection anti-DoS | Limite de connexions + délai d'attente de jointure + limitation de débit | Max 200 connexions, délai d'attente de jointure de 10 s |
| Isolation des salons | `BLAKE2b(clé_maîtresse, nom_salon)` | Cryptographiquement isolé par salon |
### Modèle de menace
NoEyes est conçu pour **de petits groupes de confiance**. Il offre une protection solide contre :
- Observateurs réseau passifs – tout le trafic est chiffré TLS + E2E
- Relais bore.pub compromis – le relais ne voit que des octets chiffrés et le timing des connexions
- Machine serveur compromise – le serveur est sans connaissance, rien d'utile dans la RAM
- Attaque de l'homme du milieu sur la connexion – épinglage de certificat TLS + clés DH signées Ed25519
- Vol de votre appareil – la clé d'identité est chiffrée par mot de passe au repos
- Attaques par relecture – protection par ID de message par salon
---
## Exécution d'un serveur en ligne (bore pub)
Lorsque vous démarrez un serveur NoEyes chez vous, votre machine obtient une adresse IP locale. Pour qu'une personne extérieure à votre réseau puisse se connecter, vous devez normalement rediriger un port sur votre routeur, ce qui échoue souvent à cause du CGNAT ou du blocage au niveau de l'opérateur.
**bore pub** résout ce problème avec un tunnel sécurisé depuis votre machine vers un relais public, donnant à votre serveur une adresse publique instantanée sans toucher à votre routeur.
**bore** est un outil de tunnel TCP open source par [Eric Zhang (@ekzhang)](https://github.com/ekzhang/bore). Lorsque vous exécutez le serveur NoEyes, il démarre automatiquement :```
bore local 5000 --to bore.pub
Le relais attribue un port aléatoire et affiche une adresse comme bore.pub:12345. Partagez-la avec votre groupe :```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --username alice --identity-path ~/.noeyes/identity_alice.key
Tout est toujours chiffré de bout en bout, bore ne fait que transférer des octets bruts.
### Reconnexion automatique lors des changements de port bore
bore.pub attribue un **port aléatoire à chaque redémarrage du serveur**. Normalement, cela signifierait devoir repartager l'adresse avec tout le monde à chaque fois. NoEyes gère cela automatiquement avec trois niveaux de récupération :
**1. Événement de migration (instantané)**
Lorsque bore réattribue un port, le serveur diffuse un événement `migrate` signé à tous les clients connectés avec le nouveau numéro de port. Les clients se déconnectent silencieusement, mettent à jour leur port et se reconnectent automatiquement. Une fenêtre de silence de 15 secondes supprime le bruit des entrées/sorties pour que l'écran de discussion ne clignote pas.
**2. Service de découverte (clients ayant manqué la migration)**
Si un client était hors ligne lorsque le port a changé, il interroge un service gratuit de clé-valeur anonyme (`keyvalue.immanuel.co`) à chaque tentative de reconnexion. Le serveur y publie automatiquement le nouveau port bore à chaque redémarrage. La clé de recherche est dérivée de votre clé de groupe, aucun compte ou inscription requis, totalement anonyme.
**3. Port dans `auth_ok` (récupération après panne)**
Si un client a tout manqué (serveur planté, diffusion de migration jamais envoyée), le serveur inclut le port bore actuel dans la réponse de handshake `auth_ok`. Le client se corrige lui-même lors de la prochaine connexion réussie.
Les changements de port bore.pub sont transparents pour les utilisateurs. La discussion continue automatiquement en quelques secondes, et les transferts de fichiers s'interrompent et reprennent là où ils s'étaient arrêtés.
Pour désactiver la découverte (configuration isolée ou relais privé) :```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery
| Limitation | Détails |
|---|---|
| Aucune garantie de disponibilité | bore.pub est un service bénévole, il peut tomber en panne |
| Le port est aléatoire | Chaque démarrage de serveur obtient un port différent, partagez à nouveau l'adresse |
| Pas pour la production | Pour une configuration permanente, utilisez un VPS avec |
Pour plus de ~10 utilisateurs, une disponibilité 24/7 ou un nom d'hôte stable, exécutez sur un VPS bon marché (Hetzner 4€/mois, DigitalOcean 4$/mois, Oracle Cloud gratuit) :```bash python noeyes.py --server --port 5000 --no-bore
### Notes sur le pare-feu
Vous n'avez **pas** besoin d'une règle de pare-feu lors de l'utilisation du tunnel bore. Vous n'en avez besoin que pour les connexions directes (LAN, IP statique, redirection de port manuelle) :```bash
python noeyes.py --server --port 5000 --no-firewall # bore tunnel, skip firewall rule
python noeyes.py --server --port 5000 --no-bore --no-firewall # VPS, manage firewall separately
python noeyes.py --generate-access-key
python noeyes.py --generate-chat-key <ACCESS_CODE_HEX> --key-file ./chat.key
python ui/launch.py # → Generate Key
cp ~/.noeyes/identity.key /backup/identity.key
cat ~/.noeyes/tofu_pubkeys.json
## Structure du projet```
NoEyes/
├── noeyes.py Entry point and CLI argument parser
├── requirements.txt pip dependencies (just: cryptography)
│
├── core/
│ ├── encryption.py All crypto: XSalsa20-Poly1305, ChaCha20-Poly1305, X25519, Ed25519, BLAKE2b
│ ├── ratchet.py Sender Keys forward secrecy: SenderChain + RatchetState
│ ├── animation.py CRT boot and ratchet activation animations with SFX
│ ├── sounds.py Cross-platform sound playback (WAV/MP3, Linux/macOS/Windows)
│ ├── identity.py Ed25519 keypair generation and TOFU pubkey store
│ ├── utils.py Terminal output, ANSI colours, TUI chrome
│ └── config.py Configuration loading and CLI parsing
│
├── network/
│ ├── server.py Async zero-metadata blind-forwarder server
│ ├── client.py Terminal chat client (E2E, DH, TOFU, file transfer)
│ ├── client_ratchet.py RatchetMixin — /ratchet command flow, migration wait
│ ├── client_dh.py X25519 DH handshake mixin
│ ├── client_send.py Outgoing message encryption (static + ratchet paths)
│ ├── client_recv.py Incoming frame routing and decryption
│ └── client_commands.py Input loop, command dispatch, help
│
├── ui/
│ ├── launch.py Guided launcher, arrow-key menu UI
│ └── setup.py Dependency wizard, auto-installs what's needed
│
├── install/
│ ├── install.sh Bootstrap for Linux / macOS / Termux / iSH
│ ├── install.bat Bootstrap for Windows (CMD and PowerShell)
│ ├── install.py Cross-platform Python installer
│ └── uninstall.py Remove all NoEyes dependencies for clean reinstall
│
├── docs/
│ ├── README.md This file
│ └── CHANGELOG.md Version history
│
├── update.py Self-updater, pulls latest from GitHub
└── sfx/ Notification sounds
PyNaCl (XSalsa20-Poly1305, BLAKE2b) + cryptography (ChaCha20-Poly1305, X25519, Ed25519, TLS)threading (threads recv + input + sender par client), asyncio côté serveurtermios pour la saisie brute des touches⚠️ Usage réservé à la recherche et à l’éducation – projet expérimental.
| Fonctionnalité | Détails |
|---|
| Serveur zéro-métadonnée | Le serveur ne voit jamais les noms d'utilisateur, les noms de salon ou les clés publiques, seulement des jetons opaques |
| Expéditeur scellé | L'identité de l'expéditeur se trouve à l'intérieur de la charge utile chiffrée, jamais dans l'en-tête de routage |
| Serveur relayeur aveugle | Aucun déchiffrement, le serveur transmet des blobs chiffrés qu'il ne peut pas lire |
| Secret de transmission | /ratchet start — Protocole Sender Keys, chaque message chiffré avec une clé dérivée unique, les messages passés restent protégés même si la clé actuelle fuit |
| Salon de groupe | Clés XSalsa20-Poly1305 par salon dérivées via BLAKE2b, salons isolés cryptographiquement |
| Messages privés | Échange de clés X25519 DH lors du premier contact, clé par paire détenue uniquement par les deux parties |
| Transfert de fichiers | Streaming ChaCha20-Poly1305, taille quelconque, faible utilisation de RAM, pause/reprise après reconnexion |
| Identité Ed25519 | Clé de signature générée automatiquement, tous les messages et fichiers sont signés |
| TOFU | Les clés vues pour la première fois sont approuvées ; les discordances de clés déclenchent un avertissement de sécurité visible |
| Sel PBKDF2 aléatoire | Chaque déploiement reçoit un sel aléatoire unique, les tables arc-en-ciel sont inutiles |
| TLS + épinglage de certificat | Transport chiffré, certificat serveur épinglé lors du premier contact via TOFU |
| Protection contre la relecture | File d'attente d'ID de message par salon, les trames rejouées sont silencieusement ignorées |
| Panneau latéral divisé | Salons (en haut) et utilisateurs (en bas) toujours visibles, chaque moitié défile indépendamment |
| Animation de démarrage CRT | Effet de phosphore plein écran avec son au démarrage |
| Animation d'activation du ratchet | Effet CRT plein écran avec art braille à engrenages, scintillement glitch, balayage de projecteur, effets sonores synchronisés et transition chrome TUI vers le rouge |
| Lanceur guidé | Interface de menu avec touches fléchées, aucune expérience en ligne de commande nécessaire |
| Installateur automatique de dépendances | Détecte votre plateforme, installe ce qui manque, demande avant de modifier quoi que ce soit |
| Commande | Description |
|---|
/help | Afficher toutes les commandes |
/quit | Se déconnecter et quitter |
/clear | Effacer les messages de l'écran |
/users | Lister les utilisateurs dans la salle actuelle |
/join <room> | Changer de salle (avertit si en ratchet actif) |
/leave | Revenir à la salle générale (avertit si en ratchet actif) |
/msg <user> <text> | Envoyer un message privé chiffré de bout en bout |
/send <user> <file> | Envoyer un fichier chiffré |
/whoami | Afficher votre empreinte d'identité |
/trust <user> | Approuver la nouvelle clé d'un utilisateur après une réinstallation |
/notify on|off | Activer/désactiver les sons de notification |
/ratchet start | Proposer des clés roulantes à secret parfait à tous les membres de la salle (tous doivent confirmer) |
/ratchet invite <u> | Réinviter un utilisateur au ratchet après qu'il ait rejoint (déclenche un redémarrage complet — aucune clé de chaîne transmise) |
/proceed | Pendant l'attente de migration, voter pour abandonner un pair hors ligne et reprendre |
| Touche | Action |
|---|
↑ / ↓ | Faire défiler le chat vers le haut / bas |
PgUp / PgDn | Faire défiler le chat d'une page |
^P (Ctrl+P) | Afficher / masquer le panneau latéral |
^C | Quitter |
| Étiquette | Couleur | Utilisation |
|---|
!ok <msg> | 🟢 Vert | Succès, confirmé, fait |
!warn <msg> | 🟡 Jaune | Avertissement, attention |
!danger <msg> | 🔴 Rouge | Critique, urgent, urgence |
!info <msg> | 🔵 Bleu | Mise à jour de statut, pour info |
!req <msg> | 🟣 Violet | Demande, nécessite une action |
!? <msg> | 🩵 Cyan | Question, demande d'avis |
--no-bore| Plateforme | Gestionnaire de paquets utilisé |
|---|
| Ubuntu / Debian / Mint | apt-get |
| Fedora / RHEL / CentOS | dnf / yum |
| Arch / Manjaro | pacman |
| Alpine / iSH (iOS) | apk |
| openSUSE | zypper |
| Void Linux | xbps-install |
| macOS | Homebrew (installé automatiquement si absent) |
| Android (Termux) | pkg |
| Windows | winget / Chocolatey / Scoop |