This project is a command-line, peer-to-peer (P2P) messaging application that uses a central relay server to establish connections. It features end-to-end encryption (E2EE) to ensure that only the communicating users can read the messages.
The core of the security model relies on a hybrid encryption scheme:
- Asymmetric Encryption (RSA): Used for securely exchanging symmetric keys and verifying user identities through digital signatures.
- Symmetric Encryption (AES-256-GCM): Used for encrypting the actual messages between clients, providing confidentiality and integrity.
- End-to-End Encryption: Messages are encrypted on the sender's client and decrypted only on the receiver's client. The relay server cannot read message content.
- Secure Registration: Clients register with the server using an encrypted and signed payload, preventing tampering and ensuring authenticity.
- Secure Peer Connection: A secure key exchange protocol establishes a shared secret key between two clients for their conversation.
- Command-Line Interface: A simple and intuitive CLI for connecting to peers and exchanging messages.
- In-memory Relay Server: A lightweight Flask-based server facilitates user discovery and message relaying without persisting message data.
The security protocol follows these main steps:
-
Server Key Generation: The
relay.pyserver starts by generating its own RSA-2048 key pair. It exposes its public key via a public endpoint. -
Client Registration:
- A new client (
client.py) generates its own RSA key pair. - It fetches the server's public key and its SHA256 hash.
- To prove its intent to register with this specific server, the client signs the server's public key hash with its own private key.
- The client creates a payload containing its chosen name and public key. This entire payload is also signed.
- A temporary AES key is generated. The signed payload is encrypted with this AES key.
- Finally, the AES key itself is encrypted using the server's public RSA key.
- The server receives this package, decrypts the AES key using its private RSA key, decrypts the payload, verifies both signatures, and registers the user. It returns a session
secret(UUID) for authenticated actions.
- A new client (
-
Peer-to-Peer E2EE Handshake:
- Client A wants to talk to Client B. Client A requests Client B's public key from the server.
- Client A generates a new shared AES key, which will be used for their private conversation.
- Client A encrypts this shared AES key with Client B's public RSA key. It also signs the key to prove its origin.
- This encrypted/signed key is sent to Client B via the relay server.
- Client B uses its private RSA key to decrypt the shared AES key and verifies the signature using Client A's public key.
- Both clients now possess the same shared secret AES key.
-
Encrypted Messaging:
- All subsequent messages between Client A and Client B are encrypted and decrypted using the shared AES key with the AES-GCM algorithm.
- Python 3.8+
- pip
-
Clone the repository:
git clone https://github.com/mthri/e2ee-chat cd e2ee-chat -
Create and activate a virtual environment:
# For macOS/Linux python3 -m venv venv source venv/bin/activate # For Windows python -m venv venv .\venv\Scripts\activate
-
Install the dependencies:
pip install -r requirements.txt
You will need to run the relay server first, and then run two or more clients in separate terminal windows.
For development, you can run the Flask server directly:
python relay.pyFor a more robust setup, run the server using Gunicorn:
gunicorn --workers 4 --bind 0.0.0.0:5000 relay:appThis command starts the server on port 5000 with 4 worker processes.
⚠️ Very Important Note: In its current implementation, this application is not designed to work with multiple workers. Because all user data, inboxes, and session keys are stored in-memory (RAM), each Gunicorn worker will maintain its own separate, isolated copy of this information.Consequently, a user registered by
worker 1will be completely unknown toworker 2, leading to authentication and messaging failures.To test this project, you must use a single worker:
gunicorn --workers 1 --bind 0.0.0.0:5000 relay:app
Open two new terminal windows (with the virtual environment activated in each).
Terminal 1 (Alice):
python client.py- Enter name:
Alice - The client will register and print your public key hash (your ID). It will look something like this:
Registration successful! Your ID: a1b2c3d4e5f6... Secret: ...
Terminal 2 (Bob):
python client.py- Enter name:
Bob - Bob's client will also register and print his unique public key hash.
Registration successful! Your ID: f0e9d8c7b6a5... Secret: ...
Now, let's make Alice and Bob talk.
In Bob's Terminal:
- Type
/waitto listen for incoming connection requests./wait Waiting for incoming connections...
In Alice's Terminal:
- Use the
/connectcommand with Bob's ID (his public key hash).You will see a confirmation that you are connected to Bob./connect f0e9d8c7b6a5...
Chatting: Once the connection is established, both users will see a confirmation. They can now type messages and press Enter to send them securely.
# Alice's terminal
Hello Bob!
# Bob's terminal
Alice(a1b2c3d4e5f6...): Hello Bob!
> Hey Alice, how are you?
/connect <user_hash>: Initiates a secure connection with another user./wait: Puts your client in a listening mode to accept a connection./disconnect: Ends the current secure session./help: Displays available commands./quitor/exit: Exits the application.