Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

p2p-chat

CI

Serverless LAN chat with authenticated end-to-end encryption. Peers find each other over UDP broadcast, authenticate with long-term Ed25519 identity keys, and exchange messages over an AES-256-GCM channel keyed by an ephemeral X25519 exchange. No server, no accounts, no bootstrap node.

$ p2pchat --username ilker
· you are ilker — fingerprint 34B3 9D0C 9237 34EA
  listening on tcp/6001, announcing on udp/6000
· ayse appeared on the network (192.168.1.24)
· connected to ayse (192.168.1.24) — encrypted
  first contact; remembering fingerprint B572 1B92 D5B4 9353
  session code 076717 — compare it with them
[14:32] ayse: protokolü bitirdin mi?
[ayse] > _

Quick start

git clone https://github.com/IlkerGcr/p2p-chat.git
cd p2p-chat
python -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -e .
p2pchat --username ilker

Run it on two machines on the same network. They will discover each other within a few seconds. /chat <user> opens a session, then type to send.

Command
/users peers discovered on the network
/chat <user> open an encrypted session and make it active
/msg <user> <text> send one message without switching chats
/verify [user] show the session code and fingerprints
/trust <user> accept a changed fingerprint after checking it
/sessions list open sessions
/history <user> show stored history (requires --save-history)
/whoami your own fingerprint

Useful flags: --chat-port, --discovery-port, --broadcast, --state-dir, --save-history, -v. Every setting also reads from a P2PCHAT_* environment variable.

Architecture

Everything runs on a single asyncio event loop. There are no locks anywhere in the codebase because there is no preemption to guard against.

flowchart TD
    CLI["cli.py<br/>prompt_toolkit UI"]
    NODE["node.py<br/>ChatNode"]
    DISC["discovery.py<br/>UDP announce + listen"]
    REG["peers.py<br/>PeerRegistry"]
    SESS["session.py<br/>handshake + Session"]
    PROTO["protocol.py<br/>length-prefixed frames"]
    CRYPTO["crypto.py<br/>Ed25519 · X25519 · HKDF · AES-GCM"]
    TRUST["trust.py<br/>TOFU fingerprints"]
    HIST["history.py<br/>optional JSONL log"]

    CLI -->|commands| NODE
    NODE -->|events| CLI
    NODE --> DISC
    DISC --> REG
    NODE --> REG
    NODE --> SESS
    NODE --> TRUST
    NODE --> HIST
    SESS --> PROTO
    SESS --> CRYPTO
Loading

Discovery — every peer broadcasts {username, chat_port, fingerprint} on udp/6000 every few seconds. The socket sets SO_REUSEADDR (and SO_REUSEPORT where it exists) so several instances can share a port on one machine. Announcements are unauthenticated and treated only as a hint about where to connect.

Framing — TCP is a byte stream, so every message is prefixed with a 4-byte big-endian length and capped at 1 MiB. The reader uses readexactly, which means a message split across segments or two messages in one segment both work.

Sessions — a connection is opened once, authenticated once, then carries traffic in both directions until either side hangs up. A background task per session reads frames as they arrive, so replies show up while you are typing.

Handshake

Three messages, a SIGMA-style authenticated exchange:

initiator                                            responder
    |  HELLO      id_a, eph_a, nonce_a, username         |
    | -------------------------------------------------> |
    |  HELLO_ACK  id_b, eph_b, nonce_b, sig_b             |
    | <------------------------------------------------- |
    |  HELLO_FIN  sig_a                                   |
    | -------------------------------------------------> |
  • id_x is a long-term Ed25519 public key, generated once and stored in the state directory. Its SHA-256 prefix is the fingerprint users compare.
  • eph_x is a per-session X25519 public key. Compromising an identity key later does not decrypt traffic recorded today — forward secrecy.
  • sig_x signs the transcript: a SHA-256 hash covering both identities, both ephemeral keys and both nonces, each length-prefixed so no two different inputs can hash alike. Binding the ephemerals is what stops a relay from splicing two handshakes together.
  • Traffic keys come from HKDF-SHA256(X25519_output, salt=transcript), split into one key per direction. Records are sealed with AES-256-GCM, nonce = a strictly increasing counter, AAD = the transcript. The receiver rejects any sequence number it has already accepted, so replays fail closed.

Both sides also derive a six-digit session code from the transcript. If the codes match when read aloud, nothing relayed the handshake.

Threat model

Honest about the boundaries, because "encrypted" on its own means very little.

Protected against

  • Passive eavesdropping on the LAN — traffic is AES-256-GCM sealed.
  • Tampering and replay — GCM authentication plus per-direction sequence numbers.
  • Retroactive decryption after identity-key theft — ephemeral X25519 per session.
  • Impersonating a contact you have talked to before — the trust store records the fingerprint on first use and warns loudly if it ever changes.
  • A hostile host flooding malformed announcements or frames — every field is validated and oversized frames are refused before allocation.

Not protected against

  • First-contact MITM. Nothing binds a fingerprint to a human the first time you meet. Compare the session code or fingerprint out of band; that is what those features are for. This is the key distribution problem, and it is not solvable inside the program.
  • Traffic analysis. Message sizes and timing are visible. There is no padding.
  • Denial of service. Anyone on the LAN can spam broadcasts or open connections.
  • A compromised endpoint. History, if enabled, is written in plaintext, and the identity key sits on disk unencrypted (0600 where the OS honours it).
  • Anything beyond the local network. Discovery is broadcast-based by design.

Development

pip install -e ".[dev]"
pytest              # 101 tests
ruff check src tests
mypy                # strict

The suite covers framing against split and coalesced TCP segments, transcript binding, replay and tamper rejection, forged-signature rejection, malformed announcements, trust-store verdicts, and two live nodes exchanging messages over loopback — including that one stalled client cannot block the listener.

License

MIT

About

Serverless LAN chat with authenticated end-to-end encryption

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages