Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

chator

End-to-end encrypted terminal chat in C, routed over Tor hidden services.

Made by tal7aouy — https://github.com/tal7aouy/chator

Two peers exchange RSA-4096 public keys out of band. One runs a Tor onion service; the other reaches it by speaking SOCKS5 directly to a local Tor daemon. Every message is individually encrypted to the recipient and signed by the sender, so Tor sees ciphertext, the onion service sees only its own peer, and neither endpoint accepts a message it cannot attribute.

  ┌─────────────┐                                     ┌──────────────┐
  │   chator    │                                     │    chator    │
  │   connect   │                                     │    listen    │
  └──────┬──────┘                                     └──────▲───────┘
         │ 1. SOCKS5 CONNECT abc…xyz.onion:9999              │
         │    (ATYP=DOMAIN — Tor resolves, we never do)      │ 4. HiddenServicePort
         ▼                                                   │    forwards to 127.0.0.1:9999
  ┌─────────────┐        ┌──────────────────┐        ┌───────┴──────┐
  │  tor (local)│───────▶│   Tor network    │───────▶│  tor (local) │
  │  :9050      │  2.    │  3 hops + rendez │   3.   │  onion svc   │
  └─────────────┘        └──────────────────┘        └──────────────┘

  payload at every hop:  [CHTR frame][RSA-OAEP wrapped key][PSS sig][AES-GCM ct]

Directory layout

chator/
├── Makefile                 hardened build, tests, static analysis
├── README.md
├── include/
│   ├── common.h             error codes, logging, secure memory, endian helpers
│   ├── crypto.h             envelope format + the scheme's rationale and limits
│   ├── net.h                socket + SOCKS5 API
│   ├── proto.h              wire framing, session object
│   └── ui.h                 ncurses API and its threading contract
├── src/
│   ├── main.c               argument parsing, thread orchestration, lifecycle
│   ├── common.c             logger, ct-compare, secure zeroing
│   ├── crypto.c             OpenSSL EVP: keygen, seal, open
│   ├── net.c                POSIX sockets, byte-level SOCKS5 client
│   ├── proto.c              frame reassembly, replay cache
│   └── ui.c                 split-screen TUI
├── tests/
│   ├── test_chator.c        53 unit/integration checks incl. a mock SOCKS5 proxy
│   └── e2e_smoke.py         two real binaries on ptys through a mock Tor
├── tools/
│   └── torrc.example        annotated Tor configuration
└── build/                   generated

Build

make deps      # prints the packages you need for your platform
make           # -> build/chator
make check     # unit tests + end-to-end smoke test

Dependencies: OpenSSL ≥ 1.1 (libcrypto), ncurses, pthreads, and tor at runtime. No other libraries.

Other targets: make debug (ASan + UBSan), make analyze (clang static analyser), make install PREFIX=~/.local.

Test status

$ make check
53 passed, 0 failed          # unit + integration
e2e OK                       # full stack over a mock Tor

The suite covers envelope round-trip, single-bit tamper detection across every structural region of the envelope, cross-identity rejection, replay rejection, onion-address validation, frame reassembly on a fragmented stream, and a byte-exact SOCKS5 handshake against a mock proxy. It is clean under -Wconversion -Wsign-conversion, ASan, UBSan and the clang analyser.


Setting up a local Tor hidden service

1. Install and configure Tor.

sudo apt install tor          # or: brew install tor

Append tools/torrc.example to your torrc (/etc/tor/torrc, or $(brew --prefix)/etc/tor/torrc on macOS), adjusting HiddenServiceDir for your platform. The essential lines are:

SocksPort 127.0.0.1:9050 IsolateSOCKSAuth
HiddenServiceDir /var/lib/tor/chator/
HiddenServicePort 9999 127.0.0.1:9999

2. Restart Tor and read your address.

sudo systemctl restart tor            # or: brew services restart tor
sudo cat /var/lib/tor/chator/hostname
# -> vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion

That directory also contains hs_ed25519_secret_key. It is the identity of your onion address — back it up, and never copy it anywhere.

3. Generate an identity on each machine and swap public keys.

mkdir -p ~/.chator && chmod 700 ~/.chator
./build/chator genkey --priv ~/.chator/id.pem --pub ~/.chator/id.pub

You will be prompted for a passphrase (the private key is then stored under AES-256). Send id.pub to your peer over a channel you already trust, and compare the printed fingerprint out of band — over the phone, in person. That comparison is the entire trust anchor; Tor authenticates the address, not the person.

4. Talk.

On the hidden-service side:

./build/chator listen  --key ~/.chator/id.pem --peer peer.pub --port 9999

On the client side:

./build/chator connect --key ~/.chator/id.pem --peer peer.pub \
                       --onion vww6yb…pyyd.onion --port 9999

--proxy-port 9150 if you are using the Tor Browser bundle rather than a system daemon.

Testing without Tor

Both halves can be exercised locally. tests/e2e_smoke.py stands up a mock SOCKS5 proxy and drives two real binaries through it, which is the fastest way to check a change end to end. To test by hand, run listen and point a connect at any local SOCKS5 proxy that forwards to the same port.


Design notes

The reasoning behind each decision lives in the header comments; the short version:

Why hand-rolled SOCKS5 (net.c) — an LD_PRELOAD shim like torsocks cannot intercept raw syscalls and silently misses whole classes of traffic; one missed call is a deanonymising connection. Speaking SOCKS5 in-process means there is exactly one place a packet can leave the program. The client requests ATYP=0x03 (DOMAINNAME) and hands the literal .onion string to Tor — it never calls getaddrinfo() on the peer, which is the classic DNS leak.

Why hybrid crypto (crypto.c) — RSA-OAEP wraps a fresh AES-256 key per message; AES-256-GCM protects the body and the header. Encapsulating a symmetric key is the standard construction (PGP, CMS, TLS-RSA all do it) and avoids RSA's size limit and cost.

Why sign as well as encrypt — the GCM tag only proves "whoever knew the session key wrote this", and anyone holding your public key can mint one. Without a signature there is no sender authenticity at all. Signatures are verified before decryption so unauthenticated bytes never reach a private-key operation.

Why both fingerprints are inside the plaintext — encrypt-then-sign is malleable at the identity layer: an attacker can strip the signature and re-sign the untouched ciphertext to claim authorship. Binding sender and recipient fingerprints inside the AEAD-protected plaintext, then checking them after decryption, closes that and also rejects a message replayed from a different conversation.

Why threads rather than poll() — an RSA private-key operation per inbound message is slow enough to stall keyboard echo if done inline in an event loop. The receiver thread absorbs it. ncurses is not thread-safe, so ui.c funnels every curses call through one mutex and no other module may touch curses.

Threat model

Defended against: a passive network observer (sees only Tor traffic), a malicious Tor relay or exit (sees ciphertext), an attacker who learns the .onion address (cannot produce messages that verify), message tampering, truncation, reordering into a different session, and replay.

Not defended against: a compromised endpoint (keylogger, memory scraper); loss of a long-term private key, which retroactively decrypts every captured message — there is no forward secrecy; traffic analysis of message timing and size; and a peer you authenticated incorrectly because you skipped the out-of-band fingerprint check.

Known limitations

These are deliberate scope choices, not oversights:

  • No forward secrecy. The obvious next step is an X25519 ECDH handshake per session (or a Double Ratchet), using the RSA keys only to authenticate the handshake. That also removes the ~1 KB per-message RSA overhead.
  • Signatures are transferable — the protocol is authenticated but not deniable.
  • Replay is bounded, not eliminated: a ±5 min timestamp window plus a 256-entry cache of recent AEAD tags. A persistent counter would be stronger.
  • One peer per session. No group chat, no key discovery, no store-and- forward.
  • UTF-8 is handled bytewise in the input editor: multi-byte characters transmit and display correctly, but backspace deletes one byte, not one glyph. Correct handling needs the wide-character ncurses API.
  • Private keys are not mlock()ed, so they can in principle reach swap.

Licence

MIT.

About

chator — end-to-end encrypted terminal chat over Tor. privacy-focused, Open Source.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages