End-to-end encrypted group chat, private messages, and file transfer β right in your terminal. The server is a blind forwarder: it cannot read a single byte of your messages, even if fully compromised.
noeyes_demo.mp4
Security features demo β group chat, private messages, identity verification, TOFU key trust:
Install demo 1 β sh install.sh (no Python required):
Install demo 2 β python ui/setup.py (guided wizard):
NoEyes is a Python terminal chat tool for small groups who need real privacy. Unlike every mainstream chat app, the server never decrypts anything β it only sees encrypted bytes and routing headers, then forwards them blindly. You generate the key, share it out-of-band, and the server learns nothing about your conversations.
Who is it for?
- Developers and teams who want encrypted comms without trusting a third-party server
- Anyone who wants to self-host a private chat with true end-to-end encryption
- Security-minded users who want to understand exactly what a server can and cannot see
| Feature | Details |
|---|---|
| Blind-forwarder server | Zero decryption β server sees only routing metadata |
| Group chat | Per-room Fernet keys derived via HKDF β rooms cryptographically isolated |
| Private messages | X25519 DH handshake on first contact β pairwise key only the two parties hold |
| File transfer | AES-256-GCM streaming β any size, low RAM usage |
| Ed25519 identity | Auto-generated signing key β all private messages and files are signed |
| TOFU | First-seen keys trusted; key mismatches trigger a visible security warning |
| Random PBKDF2 salt | Each deployment gets a unique random salt β rainbow tables are useless |
| Split sidebar panel | Rooms (top) and users (bottom) always visible side by side β each half scrolls independently |
| Free text selection | No mouse capture β drag to copy text freely on all platforms including Termux |
| CRT boot animation | Full-screen phosphor effect with sound on startup β works on all platforms |
| Guided launcher | Arrow-key menu UI β no command-line experience needed |
| Auto dependency installer | Detects your platform, installs what's missing, asks before changing anything |
| Self-updater | One command to pull the latest version from GitHub |
# 1. Run the setup wizard β installs Python, pip, and cryptography automatically
python ui/setup.py
# 2. Launch NoEyes
python ui/launch.pyui/launch.py walks you through starting a server or connecting to one β no commands to memorize.
| Platform | Run this first |
|---|---|
| Linux / macOS / Termux / iSH | sh install/install.sh |
| Windows | install\install.bat |
Both scripts install Python if missing, then hand off to setup.py automatically.
install.bat works from both CMD and PowerShell β no need to run install.ps1 directly.
# 1. Install the one dependency
pip install cryptography
# 2. Generate a shared key β share this file with all participants out-of-band
python noeyes.py --gen-key --key-file ./chat.key
# 3. 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
# 4. Connect clients
python noeyes.py --connect SERVER_IP --port 5000 --username alice --key-file ./chat.key
python noeyes.py --connect SERVER_IP --port 5000 --username bob --key-file ./chat.keyDownload Termux from F-Droid (recommended) or the Play Store. F-Droid link: https://f-droid.org/packages/com.termux/
The F-Droid version is more up to date and receives faster security patches.
Keep the session alive β Install tmux so NoEyes keeps running when you switch apps:
pkg install tmux -y
tmux
python ui/launch.py
# Press Volume Down + D to detach (keeps running in background)
# tmux attach to come backStorage permissions β file transfer will fail if u don't grant storage access(also clone NoEyes inside /storage/shared/ to access files easily):
termux-setup-storage| Command | Description |
|---|---|
/help |
Show all commands |
/quit |
Disconnect and exit |
/clear |
Clear screen |
/users |
List users in current room |
/nick <n> |
Change your display name |
/join <room> |
Switch to a room (created automatically) |
/leave |
Return to the general room |
/msg <user> <text> |
Send an E2E-encrypted private message |
/send <user> <file> |
Send an encrypted file |
/whoami |
Show your identity fingerprint |
/trust <user> |
Trust a user's new key after they reinstall |
/anim on|off |
Toggle the decrypt animation |
/notify on|off |
Toggle notification sounds |
| Key | Action |
|---|---|
\u2191 / \u2193 |
Scroll chat up / down |
PgUp / PgDn |
Scroll chat one page |
^P (Ctrl+P) |
Show / hide the sidebar panel |
^C |
Quit |
The chat window has a narrow sidebar on the left that always shows two sections:
- Top half \u2014 ROOMS \u2014 all rooms you have joined this session. The active room is highlighted with
\u25b6. - Bottom half \u2014 USERS \u2014 everyone currently in your active room.
Each half scrolls independently with the mouse wheel. If there are more items than fit, a +N more indicator appears at the bottom of that half.
The panel is display-only \u2014 no clickable elements, so you can drag to select and copy text freely on all platforms including Termux (no Shift+drag needed). Use /join <room> and /msg <user> to interact.
Press ^P to hide the panel and get a full-width chat view. Press again to bring it back. Works identically on Linux, Windows Terminal, Termux, and Terminus \u2014 no function key required.
Prefix any message with a !tag to color it for everyone and trigger a notification sound on the receiver's machine. Tags travel inside the encrypted payload β the server never sees them.
| Tag | Color | Sound | Use for |
|---|---|---|---|
!ok <msg> |
π’ Green | ok | Success, confirmed, done |
!warn <msg> |
π‘ Yellow | warn | Warning, heads up, be careful |
!danger <msg> |
π΄ Red | danger | Critical, urgent, emergency |
!info <msg> |
π΅ Blue | info | Status update, FYI |
!req <msg> |
π£ Purple | req | Request, needs someone's action |
!? <msg> |
π©΅ Cyan | ask | Question, asking for input |
Examples:
!danger server is going down in 5 minutes
!req can someone review my PR?
!ok deployment successful
!warn disk at 90% on prod
!? anyone know why the build is failing?
Sounds play from sounds/ folder next to noeyes.py. Drop in .wav, .mp3, .ogg, .aiff, .flac, or .m4a files named after the sound type (e.g. sounds/danger.wav). If no file is found, falls back to the terminal bell. Use /notify off to disable all sounds.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Alice βββββββββββββββββββββββββββββββββββββββββ Bob β
β β Encrypted payload (opaque) β β
β β β β β
β βββββββββββββΊ SERVER ββ΄ββββββββββββββββββββββββββ β
β β β
β Blind forwarder: β
β reads routing header only β
β { "type":"chat", "room":"general" } β
β forwards encrypted bytes verbatim β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
WHAT THE SERVER SEES: WHAT THE SERVER CANNOT SEE:
Β· Usernames Β· Message content
Β· Room names Β· File contents
Β· Event types (join/leave) Β· Private message bodies
Β· Frame byte length Β· DH key exchange values
Β· Ed25519 signatures
chat.key (shared secret)
β
ββ HKDF("general") βββΊ room_key["general"] (isolated per room)
ββ HKDF("dev") βββΊ room_key["dev"]
ββ HKDF("ops") βββΊ room_key["ops"]
X25519 DH (per user pair, automatic on first /msg)
alice_ephemeral + bob_ephemeral βββΊ shared_secret
β
HKDF-SHA256
β
pairwise_key (private messages)
β
HKDF(transfer_id) βββΊ aes_gcm_key (files)
passphrase + random_salt (32 bytes, os.urandom)
β
ββ PBKDF2-HMAC-SHA256 (390,000 iterations)
β
derived_key βββΊ saved to ~/.noeyes/derived.key
β
loaded directly on every subsequent run
(no PBKDF2 re-derivation, no static salt)
Every deployment gets a unique random salt β precomputed rainbow tables are useless. After the first run, share the key file, not the passphrase.
| Layer | Mechanism | Notes |
|---|---|---|
| Group chat | Fernet (AES-128-CBC + HMAC-SHA256) | Per-room key via HKDF |
| Private messages | Fernet with X25519 pairwise key | Ed25519 signed, TOFU verified |
| File transfer | AES-256-GCM | Per-transfer key, Ed25519 signed |
| Identity | Ed25519 keypair | Auto-generated at ~/.noeyes/identity.key |
| Key derivation | PBKDF2-HMAC-SHA256 + random salt | Unique salt per deployment β no rainbow tables |
| Server | Blind forwarder | Zero decryption β server never holds any keys |
| Room isolation | HKDF(master_key, room_name) |
Cryptographically isolated |
| Transport | TLS (on by default) | TOFU cert pinning β MITM triggers visible warning |
| Replay protection | Per-room message ID deque | Replayed frames silently dropped |
| Rate limiting | Separate chat / control buckets | DH flood cannot exhaust chat quota |
NoEyes/
βββ noeyes.py Entry point and CLI argument parser
βββ requirements.txt pip dependencies (just: cryptography)
β
βββ core/
β βββ encryption.py All crypto: Fernet, HKDF, X25519, Ed25519, AES-256-GCM
β βββ identity.py Ed25519 keypair generation and TOFU pubkey store
β βββ utils.py Terminal output, ANSI colours, decrypt animation
β βββ config.py Configuration loading and CLI parsing
β
βββ network/
β βββ server.py Async blind-forwarder server (zero decryption)
β βββ client.py Terminal chat client (E2E, DH, TOFU, file transfer)
β
βββ ui/
β βββ launch.py β
Guided launcher β arrow-key menu UI
β βββ setup.py β
Dependency wizard β auto-installs everything needed
β
βββ install/
β βββ install.sh Bootstrap for Linux / macOS / Termux / iSH
β βββ install.bat β
Bootstrap for Windows (CMD and PowerShell)
β βββ install.ps1 Called automatically by install.bat
β βββ install.py Cross-platform Python installer
β
βββ tests/
β βββ selftest.py Automated test suite
β
βββ docs/
β βββ README.md
β βββ CHANGELOG.md
β
βββ update.py Self-updater β pulls latest from GitHub
βββ sfx/ Notification sounds
setup.py automatically detects your platform and installs what's missing:
| Platform | Package manager used |
|---|---|
| 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 (auto-installed if missing) |
| Android (Termux) | pkg |
| Windows | winget / Chocolatey / Scoop |
When you start a NoEyes server at home, your machine gets a local IP (e.g. 192.168.1.5). For someone outside your network to connect, you would normally need to open a port on your router and expose your public IP. In practice this almost always fails because:
- Many ISPs (especially mobile data providers) put customers behind CGNAT β you don't even have a real public IP to forward
- Even with a home router you control, the firewall rules are fiddly and the IP changes
- Mobile networks routinely block inbound connections at the carrier level, regardless of what your router does
bore pub solves this by creating a secure tunnel from your machine to a public relay, giving your server an instant public address without touching your router.
bore is an open-source TCP tunnel tool written in Rust by Eric Zhang (@ekzhang).
When you run the NoEyes server, it automatically tries to start:
bore local 5000 --to bore.pub
This punches a tunnel from your local port 5000 to bore.pub, a free public relay. The relay assigns you a random port and prints an address like:
bore.pub:12345
You share that address with your friends β they connect with:
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.keyEverything is still end-to-end encrypted. bore only forwards raw bytes β it cannot read your messages.
Credit: bore is created and maintained by Eric Zhang. Source: https://github.com/ekzhang/bore
| Limitation | Details |
|---|---|
| No uptime guarantee | bore.pub is a volunteer service β it can go down at any time |
| Shared bandwidth | Heavy traffic can affect other bore users |
| Not for production | For a team or community, host your own server |
| Port is random | Each server start gets a different port β reshare the address |
| No authentication | Anyone who knows your bore.pub address can attempt to connect (your key file still protects all content) |
| Situation | Recommendation |
|---|---|
| More than ~10 concurrent users | VPS |
| Server always online 24/7 | VPS |
| Stable hostname | VPS |
| Short session / demo | bore.pub is fine |
Cheap VPS options: Hetzner (β¬4/mo), DigitalOcean ($4/mo), Vultr ($2.50/mo), Oracle Cloud (free tier)
# On the VPS β no bore needed, it has a real public IP
python noeyes.py --server --port 5000 --no-borepython noeyes.py --server --port 5000 --no-boreWhen you start a server, NoEyes can automatically add a firewall rule to open the server port so clients can connect directly. The launcher will ask whether you want this and explain when it is needed.
You do NOT need a firewall rule if you are using bore tunnel β clients connect via bore.pub and never touch your machine's firewall directly.
You need a firewall rule if clients connect to your IP directly (LAN, static IP, or manual port forwarding).
# Skip firewall rule (always safe when using bore tunnel)
python noeyes.py --server --port 5000 --no-firewall
# Skip both bore and firewall rule (VPS with its own firewall managed separately)
python noeyes.py --server --port 5000 --no-bore --no-firewallpython update.py # update to latest version
python update.py --check # just check β don't change anything# Generate once, share out-of-band (USB / Signal / encrypted email)
# NEVER share over NoEyes itself or in plaintext
python noeyes.py --gen-key --key-file ./chat.key
# Backup your identity key
cp ~/.noeyes/identity.key /backup/identity.key
# View who you currently trust (TOFU store)
cat ~/.noeyes/tofu_pubkeys.json- Language: Python 3.9+
- Encryption:
cryptographylibrary β Fernet, X25519, Ed25519, AES-256-GCM, HKDF, PBKDF2 - Networking: Raw TCP sockets with a custom length-prefixed framing protocol
- Concurrency:
threading(recv + input threads per client),asyncioon the server - Terminal: ANSI escape codes,
termiosfor raw keypress input