From dc1b5e5ccf32b1e47b9f1d8311546bdfb1760644 Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Tue, 26 May 2026 10:57:39 -0700 Subject: [PATCH] docs: rewrite README with complete setup, security, and file transfer guide Clear, concise documentation covering installation, hosting, connection security (Tailscale/LAN/public), password sharing, file transfer protocol, CLI reference, helper scripts, and architecture overview. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.MD | 279 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 98 deletions(-) diff --git a/README.MD b/README.MD index 4ae118d..f5dd531 100644 --- a/README.MD +++ b/README.MD @@ -1,133 +1,216 @@
-# 🤐 CMD-CHAT +# CMD-CHAT -### encrypted terminal chat. no servers. no logs. ram only. +### end-to-end encrypted terminal chat with file transfer [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
--- -peer-to-peer encrypted chat that runs in your terminal. you host, you control. close the window — everything's gone. +Encrypted chat that runs in your terminal. You host the server, you control the room. Close the window — everything's gone. Messages and files are encrypted client-side before the server ever sees them. -## why +## Features -every "secure" messenger still stores metadata somewhere. this doesn't. it's just two terminals talking over an encrypted tunnel. nothing written to disk, ever. +- **End-to-end encrypted** — messages encrypted with Fernet (AES-128-CBC + HMAC) before leaving your machine +- **TLS by default** — auto-generated self-signed certs, or bring your own +- **SRP authentication** — password never sent over the network (zero-knowledge proof) +- **Encrypted file transfer** — `/send`, `/accept`, `/reject` with SHA-256 verification +- **RAM only** — nothing written to disk on the server +- **Rate limiting** — brute-force protection on auth endpoints +- **No IP leaks** — client IPs never broadcast to other users +- **Password hidden** — prompted securely via `getpass`, never visible in `ps` or shell history -## how it works - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ SRP AUTHENTICATION │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ CLIENT SERVER │ -│ │ │ │ -│ │─────── POST /srp/init {username, A} ───────► │ │ -│ │ (A = client public ephemeral) │ │ -│ │ │ │ -│ │◄──── {user_id, B, salt, room_salt} ───────── │ │ -│ │ (B = server public ephemeral) │ │ -│ │ (room_salt = E2E key derivation) │ │ -│ │ │ │ -│ │ [client derives room_key via HKDF: │ │ -│ │ room_key = HKDF(password, room_salt)] │ │ -│ │ │ │ -│ │ [both sides compute SRP session key │ │ -│ │ using password + ephemeral values] │ │ -│ │ │ │ -│ │─────── POST /srp/verify {user_id, M} ──────► │ │ -│ │ (M = client proof) │ │ -│ │ │ │ -│ │◄────────── {H_AMK, session_key} ──────────── │ │ -│ │ (H_AMK = server proof) │ │ -│ │ │ │ -│ │ [password never transmitted] │ │ -│ │ [MITM can't derive session key] │ │ -│ │ │ │ -├──────────────────────────────────────────────────────────────────┤ -│ E2E ENCRYPTED CHAT │ -├──────────────────────────────────────────────────────────────────┤ -│ │ │ │ -│ │═══════ WebSocket /ws/chat?user_id ═════════► │ │ -│ │ (authenticated session) │ │ -│ │ │ │ -│ │ │ │ -│ ┌─┴─┐ ┌──┴──┐ │ -│ │ C │──── encrypt(msg, room_key) ───────────►│ S │ │ -│ │ L │ │ E │ │ -│ │ I │◄─── ciphertext (broadcast) ────────────│ R │ │ -│ │ E │ │ V │ │ -│ │ N │ decrypt(ciphertext, room_key) │ E │ │ -│ │ T │ │ R │ │ -│ └─┬─┘ └──┬──┘ │ -│ │ │ │ -│ │ [server stores ONLY ciphertext] │ │ -│ │ [server CANNOT read messages] │ │ -│ │ [all clients with same password │ │ -│ │ derive identical room_key] │ │ -│ │ │ │ -│ │ Encryption: Fernet (AES-128-CBC + HMAC) │ │ -│ │ Key derivation: HKDF-SHA256 │ │ -│ │ │ │ -│ │ [on disconnect: keys wiped from RAM] │ │ -│ │ │ │ -└──────────────────────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────────────────────┐ -│ KEY HIERARCHY │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ password ──┬──► SRP ──► session_key (per-user, auth only) │ -│ │ │ -│ └──► HKDF(password, room_salt) ──► room_key (shared) │ -│ │ -│ room_salt: generated once at server start │ -│ room_key: deterministic, same for all clients with same pwd │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - -**SRP (Secure Remote Password)** — password is never sent over the network. both sides prove they know it via zero-knowledge proof, then derive identical session keys. - -## install +## Install ```bash -python -m venv venv && source venv/bin/activate && pip install -r requirements.txt +git clone https://github.com/diorwave/cmd-chat.git +cd cmd-chat +pip install -r requirements.txt ``` -windows: +## Quick Start + +**Host a chat room:** ```bash -python -m venv venv ; .\venv\Scripts\activate ; pip install -r requirements.txt +python3 cmd_chat.py serve 0.0.0.0 3000 ``` -## usage +You'll be prompted for a room password (hidden input). An admin token and TLS cert path will print to the console. -start server: +**Connect to a chat room:** ```bash -python cmd_chat.py serve 0.0.0.0 3000 --password mysecret +python3 cmd_chat.py connect SERVER_IP 3000 yourname --insecure ``` -connect: +`--insecure` is needed for self-signed certs. You'll be prompted for the room password. + +## Securing Your Connection + +### Tailscale (recommended) + +Both parties install [Tailscale](https://tailscale.com). Traffic goes through an encrypted WireGuard tunnel. No port forwarding, works across NATs. ```bash -python cmd_chat.py connect SERVER_IP 3000 username mysecret +# Host +python3 cmd_chat.py serve 0.0.0.0 3000 + +# Friend connects using your Tailscale IP +python3 cmd_chat.py connect 100.x.x.x 3000 theirname --insecure ``` -![Example](example.gif) +Find your Tailscale IP: `tailscale ip -4` -## features +### LAN (same network) -- **ram only** — nothing touches disk -- **rsa + aes** — key exchange + symmetric encryption -- **no central server** — direct p2p connection -- **srp auth** — password never sent over network +Use your local IP. Both devices must be on the same WiFi/network. -## license +```bash +python3 cmd_chat.py connect 192.168.1.x 3000 theirname --insecure +``` + +### Public Internet + +Requires port forwarding on your router (TCP port 3000 to your machine). Use `--cert` and `--key` with a real certificate for production use. + +```bash +python3 cmd_chat.py connect PUBLIC_IP 3000 theirname --insecure +``` + +Find your public IP: `curl ifconfig.me` + +## Sharing the Room Password + +The password must be shared outside the chat. Never send it over an unencrypted channel. + +1. **In person** — tell them verbally +2. **Signal** — disappearing message set to 30 seconds +3. **One-time link** — [onetimesecret.com](https://onetimesecret.com) (self-destructs after one view) +4. **Split it** — send half via Telegram, half via SMS + +## Chat Commands + +| Command | Action | +|---------|--------| +| `/send ` | Propose a file transfer to the room | +| `/accept` | Accept a pending file offer | +| `/reject` | Decline a pending file offer | +| `q` | Disconnect | + +### File Transfer + +Files are chunked (64KB), encrypted with the room key, and relayed through the server as opaque ciphertext. The server never sees file names, contents, or metadata. + +``` +alice> /send report.pdf +bob> "alice wants to send report.pdf (1.2 MB) — /accept or /reject" +bob> /accept + Receiving: 100% (1.2 MB/1.2 MB) + File saved: ./downloads/report.pdf — SHA-256 verified +``` + +- Max file size: 50 MB +- Files saved to `./downloads/` relative to where the client was launched +- SHA-256 integrity check on every transfer + +## CLI Reference + +### Server + +```bash +python3 cmd_chat.py serve [options] +``` + +| Flag | Purpose | +|------|---------| +| `--password`, `-p` | Room password (prompted if omitted) | +| `--cert` | Path to TLS certificate | +| `--key` | Path to TLS private key | +| `--no-tls` | Disable TLS (local dev only) | + +### Client + +```bash +python3 cmd_chat.py connect [options] +``` + +| Flag | Purpose | +|------|---------| +| `--password`, `-p` | Room password (prompted if omitted) | +| `--insecure`, `-k` | Skip TLS cert verification (self-signed certs) | +| `--no-tls` | Connect without TLS | + +### Environment Variable + +Set `CMD_CHAT_PASSWORD` to skip the password prompt for both server and client. + +## Helper Scripts + +### `./lab/host-chat.sh` + +One-command server setup. Detects your IPs (Tailscale, LAN, public), prints the exact connect command your friend needs, then starts the server. + +```bash +./lab/host-chat.sh # TLS on port 4000 +./lab/host-chat.sh --port 5000 # custom port +./lab/host-chat.sh --no-tls # disable TLS +``` + +### `./lab/setup-lab.sh` + +Spins up a tmux session with the server and two chat clients side-by-side for local testing. + +```bash +./lab/setup-lab.sh # default lab +./lab/setup-lab.sh --no-tls --port 4001 # plain HTTP +./lab/setup-lab.sh --user1 alice --user2 bob +./lab/setup-lab.sh --teardown # clean up +``` + +Attach with `tmux attach -t cmd-chat-lab`. Switch panes with `Ctrl+B` then arrow keys. + +## How It Works + +``` +CLIENT SERVER CLIENT + │ │ │ + │── POST /srp/init {A} ──────────► │ │ + │◄── {B, salt, room_salt} ──────── │ │ + │ │ │ + │ derive room_key = HKDF(password, room_salt) │ + │ │ │ + │── POST /srp/verify {M} ────────► │ │ + │◄── {H_AMK, ws_token} ─────────── │ │ + │ │ │ + │══ WSS /ws/chat?ws_token ════════► │ ◄══════════════════════════════│ + │ │ │ + │ encrypt(msg, room_key) ────────► │ ──── ciphertext ────────────► │ + │ │ decrypt(ciphertext, room_key) + │ │ │ + │ server stores ONLY ciphertext │ │ + │ server CANNOT read messages │ │ +``` + +**SRP (Secure Remote Password)** — both sides prove they know the password without transmitting it. A network observer learns nothing. + +**Room Key** — derived independently by each client via `HKDF(password, room_salt)`. All clients with the same password get the same key. The server never has the key. + +**WebSocket Auth** — HMAC-SHA256 token issued after SRP verification. Prevents session hijacking. + +## Admin + +The server prints an admin token at startup. Use it to clear message history: + +```bash +curl -k -X DELETE https://SERVER:3000/clear \ + -H "Authorization: Bearer " +``` + +## License MIT