
Chat de terminal seguro. Servidor de reenvío ciego con cifrado E2E y cero metadatos. PyNaCl XSalsa20-Poly1305 + Ed25519 + secreto hacia adelante. Python multiplataforma.
Chat grupal cifrado de extremo a extremo, mensajes privados y transferencia de archivos en tu terminal. El servidor es un reenviador ciego: no puede leer tus mensajes, no conoce tu nombre de usuario, no conoce la sala en la que estás y no puede relacionar dos mensajes con la misma persona, incluso si está completamente comprometido.
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 es una herramienta de chat de terminal en Python para pequeños grupos de confianza. El servidor nunca descifra nada y nunca ve quién eres: solo maneja tokens opacos y reenvía bytes cifrados.
Tú generas la clave, la compartes fuera de banda, y el servidor no aprende nada sobre tus conversaciones.
Útil para pequeños grupos de confianza que quieren comunicaciones cifradas sin confiar en ningún servidor de terceros, para alojar tu propio chat privado con cifrado de extremo a extremo real, o para cualquiera que quiera entender exactamente qué puede y qué no puede ver un servidor.
python ui/setup.py
python ui/launch.py
`ui/launch.py` te guía para iniciar un servidor o conectarte a uno.
---
### Opción B - Si Python aún no está instalado
| Plataforma | Ejecuta primero esto |
|---|---|
| Linux / macOS / Termux / iSH | `sh install/install.sh` |
| Windows | `install\install.bat` |
Ambos scripts instalan Python si falta, y luego pasan automáticamente el control a `setup.py`.
---
### Opción C - Manual```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
Importante: Cada usuario debe tener su propio archivo de identidad. Dos clientes que compartan el mismo archivo de identidad obtienen el mismo token de bandeja de entrada y el servidor rechazará el segundo como sesión duplicada. El archivo de identidad se genera automáticamente en la primera ejecución; solo pasa una
--identity-pathúnica por usuario.
Descarga Termux desde F-Droid (recomendado): https://f-droid.org/packages/com.termux/
Mantén la sesión activa - instala tmux para que NoEyes siga ejecutándose cuando cambies de aplicación:```bash pkg install tmux -y tmux python ui/launch.py
**Permisos de almacenamiento** - la transferencia de archivos fallará sin esto:```bash
termux-setup-storage
▶.Cada mitad se desplaza de forma independiente. Pulsa ^P para ocultar el panel y ver el chat a ancho completo.
Prefija cualquier mensaje con !tag para colorearlo para todos y activar un sonido de notificación. Las etiquetas viajan dentro de la carga útil cifrada, el servidor nunca las ve.
Ejemplos:``` !danger server is going down in 5 minutes !ok deployment successful !req can someone review my PR?
Los sonidos se reproducen desde la carpeta `sfx/`. Coloca archivos `.wav`, `.mp3`, `.ogg`, `.aiff`, `.flac` o `.m4a` con el nombre correspondiente a la etiqueta (p. ej., `sfx/danger.wav`). Si no se encuentra, se usa la campana del terminal como respaldo. Usa `/notify off` para desactivar todos los sonidos.
---
## Arquitectura
> 🗺️ **[Mapa interactivo de seguridad en vivo](https://ymsniper.github.io/NoEyes/)** — Desglose visual de la arquitectura de cifrado completa, el modelo de amenaza y el enrutamiento de cero metadatos en un diagrama interactivo.```
┌──────────────────────────────────────────────────────────────────────┐
│ 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
Cada cliente calcula dos tokens opacos localmente antes de conectarse:``` inbox_token = blake2s(identity_vk_bytes, digest_size=16) room_token = blake2s((room_name + group_key_hex).encode(), digest_size=16)
The server routes all frames by these tokens only. It never stores display names, room names, or public keys. Sender identity travels **inside** the encrypted payload (sealed sender), not in the routing header.
### Cadena de derivación de claves```
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
Cada archivo de identidad recibe una sal aleatoria única, las tablas arcoíris son inútiles.
---
## Resumen de seguridad
| Capa | Mecanismo | Notas |
|---|---|---|
| Secreto hacia adelante (trinquete) | Sender Keys — cadena KDF BLAKE2b + XSalsa20-Poly1305 por mensaje | Clave única por mensaje, avance rápido para mensajes perdidos |
| Chat grupal | XSalsa20-Poly1305 (secretbox de PyNaCl) | Clave por sala mediante BLAKE2b |
| Mensajes privados | XSalsa20-Poly1305 con clave por pares X25519 | Firmado con Ed25519, verificado con TOFU |
| Transferencia de archivos | ChaCha20-Poly1305 | Clave por transferencia mediante BLAKE2b, firmado con Ed25519, pausar/reanudar entre reconexiones |
| Identidad del remitente | Remitente sellado | Nombre de usuario + firma dentro de la carga cifrada, nunca en el encabezado de enrutamiento |
| Identidad | Par de claves Ed25519 | Archivo de identidad por usuario, cifrado con contraseña mediante BLAKE2b + sal aleatoria |
| Derivación de claves | BLAKE2b (PyNaCl) | Separada por dominio mediante parámetro de personalización, sin tablas arcoíris |
| Enrutamiento del servidor | Tokens opacos blake2s | El servidor nunca almacena nombres de usuario, nombres de sala ni claves públicas |
| Transporte | TLS (activado por defecto) | Fijación de certificados TOFU, la discrepancia de huella dactilar aborta la conexión |
| Integridad DH | Claves públicas DH firmadas con Ed25519 | Previene MITM en el intercambio de claves por pares |
| Protección de reproducción | Deque de ID de mensaje por sala | Las tramas reproducidas se descartan silenciosamente |
| Protección DoS | Límite de conexiones + tiempo de espera de unión + limitación de velocidad | Máximo 200 conexiones, tiempo de espera de unión de 10 s |
| Aislamiento de salas | `BLAKE2b(master_key, room_name)` | Aislado criptográficamente por sala |
### Modelo de amenazas
NoEyes está diseñado para **grupos pequeños y de confianza**. Proporciona una fuerte protección contra:
- Observadores pasivos de la red: todo el tráfico está cifrado con TLS + E2E
- Relay bore.pub comprometido: el relay solo ve bytes cifrados y tiempos de conexión
- Máquina servidor comprometida: el servidor es de conocimiento cero, nada útil en RAM
- MITM en la conexión: fijación de certificados TLS + claves DH firmadas con Ed25519
- Alguien que roba tu dispositivo: la clave de identidad está cifrada con contraseña en reposo
- Ataques de reproducción: protección de reproducción por sala basada en MID
---
## Ejecutar un servidor en línea (bore pub)
Cuando inicias un servidor NoEyes en casa, tu máquina obtiene una IP local. Para que alguien fuera de tu red pueda conectarse, normalmente necesitarías reenviar un puerto en tu router, lo que a menudo falla debido a CGNAT o bloqueos a nivel de operador.
**bore pub** resuelve esto con un túnel seguro desde tu máquina hasta un relay público, dando a tu servidor una dirección pública instantánea sin tocar tu router.
**bore** es una herramienta de túnel TCP de código abierto de [Eric Zhang (@ekzhang)](https://github.com/ekzhang/bore). Cuando ejecutas el servidor NoEyes, este inicia automáticamente:```
bore local 5000 --to bore.pub
El relay asigna un puerto aleatorio e imprime una dirección como bore.pub:12345. Compártela con tu grupo:```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --username alice --identity-path ~/.noeyes/identity_alice.key
Todo sigue cifrado de extremo a extremo; bore solo reenvía bytes en bruto.
### Reconexión automática ante cambios de puerto de bore
bore.pub asigna un **puerto aleatorio en cada reinicio del servidor**. Normalmente esto implicaría volver a compartir la dirección con todos cada vez. NoEyes lo gestiona automáticamente con tres capas de recuperación:
**1. Evento migrate (instantáneo)**
Cuando bore reasigna un puerto, el servidor difunde un evento `migrate` firmado a todos los clientes conectados con el nuevo número de puerto. Los clientes se desconectan silenciosamente, actualizan su puerto y se reconectan automáticamente. Una ventana de silencio de 15 segundos suprime el ruido de entradas/salidas para que la pantalla del chat no parpadee.
**2. Servicio de descubrimiento (clientes que no recibieron el migrate)**
Si un cliente estaba desconectado cuando cambió el puerto, consulta un servicio anónimo gratuito de clave-valor (`keyvalue.immanuel.co`) en cada intento de reconexión. El servidor publica allí el nuevo puerto de bore automáticamente cada vez que bore se reinicia. La clave de búsqueda se deriva de tu clave de grupo; no se necesita cuenta ni registro, es totalmente anónimo.
**3. Puerto en `auth_ok` (recuperación ante caídas)**
Si un cliente lo perdió todo (el servidor se bloqueó, la difusión del migrate nunca se envió), el servidor incluye el puerto actual de bore en la respuesta de protocolo de enlace `auth_ok`. El cliente se autocorrige en la siguiente conexión exitosa.
Los cambios de puerto de bore.pub son transparentes para los usuarios. El chat continúa automáticamente en cuestión de segundos, y las transferencias de archivos se pausan y se reanudan desde donde quedaron.
Para desactivar el descubrimiento (configuración aislada o relay privado):```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery
| Limitación | Detalles |
|---|---|
| Sin garantía de disponibilidad | bore.pub es un servicio voluntario, puede caerse |
| El puerto es aleatorio | Cada inicio del servidor obtiene un puerto diferente; vuelve a compartir la dirección |
| No apto para producción | Para una configuración permanente, usa un VPS con |
Para más de ~10 usuarios, disponibilidad 24/7 o un nombre de host estable, ejecuta en un VPS barato (Hetzner €4/mo, DigitalOcean $4/mo, nivel gratuito de Oracle Cloud):```bash python noeyes.py --server --port 5000 --no-bore
### Notas sobre el firewall
**No** necesitas una regla de firewall cuando usas bore tunnel. Solo necesitas una para conexiones directas (LAN, IP estática, reenvío de puertos manual):```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
## Estructura del Proyecto```
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 (hilos recv + input + sender por cliente), asyncio en el servidortermios para entrada de teclas en crudo⚠️ Solo para uso de investigación y educativo - proyecto experimental.
| Característica | Detalles |
|---|
| Servidor de cero metadatos | El servidor nunca ve nombres de usuario, nombres de salas ni claves públicas, solo tokens opacos |
| Remitente sellado | La identidad del remitente vive dentro de la carga útil cifrada, nunca en la cabecera de enrutamiento |
| Servidor de reenvío ciego | Cero descifrado; el servidor reenvía blobs cifrados que no puede leer |
| Secreto de reenvío | /ratchet start — Protocolo Sender Keys, cada mensaje se cifra con una clave derivada única; los mensajes pasados están a salvo incluso si la clave actual se filtra |
| Chat grupal | Claves XSalsa20-Poly1305 por sala derivadas mediante BLAKE2b; las salas están aisladas criptográficamente |
| Mensajes privados | Handshake X25519 DH en el primer contacto; solo las dos partes poseen la clave por pares |
| Transferencia de archivos | Streaming ChaCha20-Poly1305, cualquier tamaño, bajo uso de RAM, pausa/reanudación entre reconexiones |
| Identidad Ed25519 | Clave de firma autogenerada; todos los mensajes y archivos están firmados |
| TOFU | Las claves vistas por primera vez son de confianza; las discrepancias de claves activan una advertencia de seguridad visible |
| Sal PBKDF2 aleatoria | Cada implementación obtiene una sal aleatoria única; las tablas arcoíris son inútiles |
| TLS + fijación de certificados | Transporte cifrado; el certificado del servidor se fija en el primer contacto mediante TOFU |
| Protección contra reproducción | Deque de ID de mensaje por sala; las tramas reproducidas se descartan silenciosamente |
| Panel lateral dividido | Salas (arriba) y usuarios (abajo) siempre visibles; cada mitad se desplaza de forma independiente |
| Animación de arranque CRT | Efecto fósforo a pantalla completa con sonido al inicio |
| Animación de activación del trinquete | Efecto CRT a pantalla completa con arte de engranaje en braille, parpadeo de glitch, barrido de foco, efectos de sonido sincronizados y transición del marco TUI a rojo |
| Lanzador guiado | Interfaz de menú con teclas de flecha; no se necesita experiencia en línea de comandos |
| Instalador automático de dependencias | Detecta tu plataforma, instala lo que falta y pregunta antes de cambiar nada |
| Comando | Descripción |
|---|
/help | Muestra todos los comandos |
/quit | Desconecta y sale |
/clear | Limpia los mensajes de la pantalla |
/users | Lista los usuarios en la sala actual |
/join <room> | Cambia a una sala (avisa si hay un ratchet activo) |
/leave | Vuelve a la sala general (avisa si hay un ratchet activo) |
/msg <user> <text> | Envía un mensaje privado cifrado E2E |
/send <user> <file> | Envía un archivo cifrado |
/whoami | Muestra tu huella de identidad |
/trust <user> | Confía en la nueva clave de un usuario después de que reinstale |
/notify on|off | Activa o desactiva los sonidos de notificación |
/ratchet start | Propone claves rotatorias de secreto hacia adelante a todos los miembros de la sala (todos deben confirmar) |
/ratchet invite <u> | Reinvita a un usuario al ratchet después de que se reincorpore (provoca un reinicio completo: no se reenvían claves de cadena) |
/proceed | Durante la espera de migración, vota para descartar un peer sin conexión y reanudar |
| Tecla | Acción |
|---|
↑ / ↓ | Desplaza el chat hacia arriba / abajo |
PgUp / PgDn | Desplaza el chat una página |
^P (Ctrl+P) | Muestra / oculta el panel lateral |
^C | Salir |
| Etiqueta | Color | Uso |
|---|
!ok <msg> | 🟢 Verde | Éxito, confirmado, hecho |
!warn <msg> | 🟡 Amarillo | Advertencia, aviso |
!danger <msg> | 🔴 Rojo | Crítico, urgente, emergencia |
!info <msg> | 🔵 Azul | Actualización de estado, para tu información |
!req <msg> | 🟣 Púrpura | Solicitud, requiere acción |
!? <msg> | 🩵 Cian | Pregunta, se solicita información |
--no-bore| Plataforma | Administrador de paquetes utilizado |
|---|
| 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 (instalado automáticamente si falta) |
| Android (Termux) | pkg |
| Windows | winget / Chocolatey / Scoop |