
Chat de terminal seguro. Servidor retransmissor cego com criptografia E2E e zero metadados. PyNaCl XSalsa20-Poly1305 + Ed25519 + sigilo progressivo. Python multiplataforma.
Chat em grupo com criptografia de ponta a ponta, mensagens privadas e transferência de arquivos no seu terminal. O servidor é um encaminhador cego: ele não consegue ler suas mensagens, não sabe seu nome de usuário, não sabe em qual sala você está e não consegue associar duas mensagens à mesma pessoa, mesmo que seja totalmente 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
O NoEyes é uma ferramenta de chat de terminal em Python para pequenos grupos de confiança. O servidor nunca descriptografa nada e nunca vê quem você é - ele apenas lida com tokens opacos e encaminha bytes criptografados.
Você gera a chave, a compartilha fora de banda, e o servidor não aprende nada sobre suas conversas.
Útil para pequenos grupos de confiança que desejam comunicações criptografadas sem confiar em nenhum servidor de terceiros, para autohospedar um chat privado com criptografia de ponta a ponta real, ou para qualquer pessoa que queira entender exatamente o que um servidor pode e não pode ver.
python ui/setup.py
python ui/launch.py
`ui/launch.py` orienta você a iniciar um servidor ou conectar-se a um.
---
### Opção B - Se o Python ainda não estiver instalado
| Plataforma | Execute isto primeiro |
|---|---|
| Linux / macOS / Termux / iSH | `sh install/install.sh` |
| Windows | `install\install.bat` |
Ambos os scripts instalam o Python se estiver ausente e, em seguida, passam para `setup.py` automaticamente.
---
### Opção 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 usuário deve ter seu próprio arquivo de identidade. Dois clientes compartilhando o mesmo arquivo de identidade obtêm o mesmo token de caixa de entrada e o servidor rejeitará o segundo como uma sessão duplicada. O arquivo de identidade é gerado automaticamente na primeira execução; basta passar um
--identity-pathexclusivo por usuário.
Baixe o Termux pelo F-Droid (recomendado): https://f-droid.org/packages/com.termux/
Mantenha a sessão ativa - instale o tmux para que o NoEyes continue rodando quando você trocar de aplicativos:```bash pkg install tmux -y tmux python ui/launch.py
**Permissões de armazenamento** - a transferência de arquivos falhará sem isso:```bash
termux-setup-storage
▶.Cada metade rola de forma independente. Pressione ^P para ocultar o painel e obter uma visualização do chat em largura total.
Prefixe qualquer mensagem com uma !tag para colori-la para todos e disparar um som de notificação. As tags viajam dentro da carga útil criptografada; o servidor nunca as vê.
Exemplos:``` !danger server is going down in 5 minutes !ok deployment successful !req can someone review my PR?
Os sons são reproduzidos a partir da pasta `sfx/`. Coloque arquivos `.wav`, `.mp3`, `.ogg`, `.aiff`, `.flac` ou `.m4a` nomeados de acordo com a tag (por exemplo, `sfx/danger.wav`). Se não for encontrado, usa o sino do terminal como fallback. Use `/notify off` para desativar todos os sons.
---
## Arquitetura
> 🗺️ **[Live Interactive Security Map](https://ymsniper.github.io/NoEyes/)** — Detalhamento visual da arquitetura completa de criptografia, modelo de ameaças e roteamento de metadados zero em um diagrama interativo.```
┌──────────────────────────────────────────────────────────────────────┐
│ 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 dois tokens opacos localmente antes de conectar:``` inbox_token = blake2s(identity_vk_bytes, digest_size=16) room_token = blake2s((room_name + group_key_hex).encode(), digest_size=16)
O servidor roteia todos os quadros apenas por esses tokens. Ele nunca armazena nomes de exibição, nomes de salas ou chaves públicas. A identidade do remetente viaja **dentro** do payload criptografado (remetente selado), não no cabeçalho de roteamento.
### Cadeia de derivação de chaves```
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 arquivo de identidade recebe um salt aleatório único, tabelas rainbow são inúteis.
---
## Resumo de Segurança
| Camada | Mecanismo | Observações |
|---|---|---|
| Sigilo de encaminhamento (ratchet) | Sender Keys — BLAKE2b chain KDF + XSalsa20-Poly1305 por mensagem | Chave única por mensagem, avanço rápido para mensagens perdidas |
| Bate-papo em grupo | XSalsa20-Poly1305 (PyNaCl secretbox) | Chave por sala via BLAKE2b |
| Mensagens privadas | XSalsa20-Poly1305 com chave pareada X25519 | Assinado com Ed25519, verificado via TOFU |
| Transferência de arquivos | ChaCha20-Poly1305 | Chave por transferência via BLAKE2b, assinado com Ed25519, pausar/retomar em reconexões |
| Identidade do remetente | Remetente lacrado (sealed sender) | Nome de usuário + assinatura dentro do payload criptografado, nunca no cabeçalho de roteamento |
| Identidade | Par de chaves Ed25519 | Arquivo de identidade por usuário, criptografado com senha via BLAKE2b + salt aleatório |
| Derivação de chave | BLAKE2b (PyNaCl) | Separado por domínio via parâmetro de personalização, sem tabelas rainbow |
| Roteamento do servidor | Tokens blake2s opacos | O servidor nunca armazena nomes de usuário, nomes de sala ou chaves públicas |
| Transporte | TLS (ativado por padrão) | Fixação de certificado TOFU; descasamento de impressão digital aborta a conexão |
| Integridade DH | Chaves públicas DH assinadas com Ed25519 | Previne MITM na troca de chaves par a par |
| Proteção contra replay | Deque de IDs de mensagens por sala | Quadros repetidos são descartados silenciosamente |
| Proteção contra DoS | Limite de conexões + tempo limite de entrada + limitação de taxa | Máximo de 200 conexões, tempo limite de entrada de 10s |
| Isolamento de salas | `BLAKE2b(master_key, room_name)` | Criptograficamente isolado por sala |
### Modelo de ameaças
O NoEyes foi projetado para **pequenos grupos de confiança**. Ele oferece forte proteção contra:
- Observadores passivos de rede - todo o tráfego é criptografado com TLS + E2E
- Relay bore.pub comprometido - o relay vê apenas bytes criptografados e o tempo das conexões
- Máquina servidor comprometida - o servidor é de conhecimento zero, nada útil na RAM
- MITM na conexão - fixação de certificado TLS + chaves DH assinadas com Ed25519
- Alguém roubando seu dispositivo - a chave de identidade é criptografada com senha em repouso
- Ataques de replay - proteção contra replay por sala baseada em MID
---
## Executando um Servidor Online (bore pub)
Ao iniciar um servidor NoEyes em casa, sua máquina recebe um IP local. Para alguém fora da sua rede se conectar, normalmente você precisaria encaminhar uma porta no seu roteador, o que muitas vezes falha devido a CGNAT ou bloqueio em nível de operadora.
O **bore pub** resolve isso com um túnel seguro da sua máquina até um relay público, dando ao seu servidor um endereço público instantâneo sem precisar mexer no roteador.
O **bore** é uma ferramenta de túnel TCP de código aberto criada por [Eric Zhang (@ekzhang)](https://github.com/ekzhang/bore). Quando você executa o servidor NoEyes, ele inicia automaticamente:```
bore local 5000 --to bore.pub
O relay atribui uma porta aleatória e exibe um endereço como bore.pub:12345. Compartilhe isso com o seu grupo:```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --username alice --identity-path ~/.noeyes/identity_alice.key
Tudo continua criptografado de ponta a ponta, o bore apenas encaminha bytes brutos.
### Reconexão automática em mudanças de porta do bore
bore.pub atribui uma **porta aleatória a cada reinicialização do servidor**. Normalmente, isso significaria compartilhar novamente o endereço com todos a cada vez. NoEyes lida com isso automaticamente com três camadas de recuperação:
**1. Evento Migrate (instantâneo)**
Quando o bore reatribui uma porta, o servidor transmite um evento `migrate` assinado com o novo número de porta para todos os clientes conectados. Os clientes desconectam silenciosamente, atualizam a porta e reconectam automaticamente. Uma janela de silêncio de 15 segundos suprime o ruído de entrada/saída para que a tela do chat não fique piscando.
**2. Serviço de descoberta (clientes que perderam o migrate)**
Se um cliente estava offline quando a porta mudou, ele consulta um serviço gratuito e anônimo de chave-valor (`keyvalue.immanuel.co`) a cada tentativa de reconexão. O servidor publica a nova porta do bore lá automaticamente sempre que o bore reinicia. A chave de consulta é derivada da sua chave de grupo, sem necessidade de conta ou registro, totalmente anônimo.
**3. Porta em `auth_ok` (recuperação de falha)**
Se um cliente perdeu tudo (o servidor travou, a transmissão do migrate nunca foi enviada), o servidor inclui a porta atual do bore na resposta de handshake `auth_ok`. O cliente se autocorrige na próxima conexão bem-sucedida.
As mudanças de porta do bore.pub são transparentes para os usuários. O chat continua automaticamente em segundos, e as transferências de arquivos pausam e retomam de onde pararam.
Para desativar a descoberta (configuração air-gapped ou relay privado):```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery
| Limitação | Detalhes |
|---|---|
| Sem garantia de disponibilidade | bore.pub é um serviço voluntário, pode ficar fora do ar |
| A porta é aleatória | Cada início de servidor recebe uma porta diferente, compartilhe novamente o endereço |
| Não para produção | Para uma configuração permanente, use uma VPS com |
Para mais de ~10 usuários, disponibilidade 24/7 ou um hostname estável, execute em uma VPS barata (Hetzner €4/mês, DigitalOcean $4/mês, camada gratuita da Oracle Cloud):```bash python noeyes.py --server --port 5000 --no-bore
### Notas sobre firewall
Você **não** precisa de uma regra de firewall ao usar o bore tunnel. Você só precisa de uma para conexões diretas (LAN, IP estático, encaminhamento manual de portas):```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
---
## Estrutura do Projeto```
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 por cliente), asyncio no servidortermios para entrada bruta de teclas⚠️ Apenas para Uso em Pesquisa e Educação - projeto experimental.
| Recurso | Detalhes |
|---|
| Servidor de metadados zero | O servidor nunca vê nomes de usuário, nomes de salas ou chaves públicas, apenas tokens opacos |
| Remetente selado | A identidade do remetente vive dentro do payload criptografado, nunca no cabeçalho de roteamento |
| Servidor de encaminhamento cego | Zero descriptografia, o servidor encaminha blobs criptografados que não consegue ler |
| Sigilo de encaminhamento | /ratchet start — protocolo Sender Keys, cada mensagem é criptografada com uma chave derivada exclusiva, mensagens passadas permanecem seguras mesmo se a chave atual vazar |
| Chat em grupo | Chaves XSalsa20-Poly1305 por sala derivadas via BLAKE2b, salas criptograficamente isoladas |
| Mensagens privadas | Handshake X25519 DH no primeiro contato, chave pareada que somente as duas partes possuem |
| Transferência de arquivos | Streaming ChaCha20-Poly1305, qualquer tamanho, baixo uso de RAM, pausa/retomada entre reconexões |
| Identidade Ed25519 | Chave de assinatura gerada automaticamente, todas as mensagens e arquivos são assinados |
| TOFU | Chaves vistas pela primeira vez são confiáveis; incompatibilidades de chave disparam um aviso de segurança visível |
| Sal PBKDF2 aleatório | Cada implantação obtém um sal aleatório exclusivo, tabelas arco-íris são inúteis |
| TLS + fixação de certificado | Transporte criptografado, certificado do servidor fixado no primeiro contato via TOFU |
| Proteção contra replay | Deque de IDs de mensagem por sala, quadros repetidos são descartados silenciosamente |
| Painel lateral dividido | Salas (topo) e usuários (base) sempre visíveis, cada metade rola de forma independente |
| Animação de inicialização CRT | Efeito fósforo em tela cheia com som na inicialização |
| Animação de ativação do ratchet | Efeito CRT em tela cheia com arte de engrenagem em braille, flicker de glitch, varredura de holofote, SFX sincronizado e transição do chrome da TUI para vermelho |
| Lançador guiado | Menu de interface com teclas de seta, sem necessidade de experiência com linha de comando |
| Instalador automático de dependências | Detecta sua plataforma, instala o que falta, pergunta antes de alterar qualquer coisa |
| Comando | Descrição |
|---|
/help | Mostrar todos os comandos |
/quit | Desconectar e sair |
/clear | Limpar mensagens da tela |
/users | Listar usuários na sala atual |
/join <room> | Mudar para uma sala (avisa se estiver em ratchet ativo) |
/leave | Voltar para a sala geral (avisa se estiver em ratchet ativo) |
/msg <user> <text> | Enviar uma mensagem privada criptografada de ponta a ponta (E2E) |
/send <user> <file> | Enviar um arquivo criptografado |
/whoami | Mostrar sua impressão digital de identidade (fingerprint) |
/trust <user> | Confiar na nova chave de um usuário após ele reinstalar |
/notify on|off | Alternar sons de notificação |
/ratchet start | Propor chaves rolantes de sigilo progressivo (forward secrecy) a todos os membros da sala (todos devem confirmar) |
/ratchet invite <u> | Reinvitar um usuário ao ratchet depois que ele voltar (aciona reinício completo — nenhuma chave de cadeia é encaminhada) |
/proceed | Durante a espera de migração, votar para remover um par offline e retomar |
| Tecla | Ação |
|---|
↑ / ↓ | Rolar o chat para cima / para baixo |
PgUp / PgDn | Rolar o chat uma página |
^P (Ctrl+P) | Mostrar / ocultar o painel lateral |
^C | Sair |
| Etiqueta | Cor | Uso |
|---|
!ok <msg> | 🟢 Verde | Sucesso, confirmado, concluído |
!warn <msg> | 🟡 Amarelo | Aviso, atenção |
!danger <msg> | 🔴 Vermelho | Crítico, urgente, emergência |
!info <msg> | 🔵 Azul | Atualização de status, para sua informação |
!req <msg> | 🟣 Roxo | Solicitação, requer ação |
!? <msg> | 🩵 Ciano | Pergunta, pedindo entrada (input) |
--no-bore| Plataforma | Gerenciador de pacotes usado |
|---|
| 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 automaticamente se ausente) |
| Android (Termux) | pkg |
| Windows | winget / Chocolatey / Scoop |