A terminal LAN messaging application written in Python. Supports direct messages, broadcasts and store-and-forward delivery for offline users. Connections are secured with mutual TLS. Both the server and every client must present a certificate signed by the server's CA before any data can be exchanged.
- Python 3.8 or later
- OpenSSL
cryptographylibrary
pip install cryptography- Optional: pip install
richfor colorfull terminal output (recommended but not required )
pip install richlan-messenger/
│
├── run_server.py # Entry point to start the server
├── run_client.py # Entry point to start the client
├── tests.py # tests
│
├── server/
│ ├── server.py # TCP server, session management, message routing
│ ├── database.py # SQLite layer managment (users + store-and-forward messages)
│ ├── lanmsg.db # Created automatically on first run
│ └── provision_user.py # Admin tool: register users and generate cert bundles
│
├── client/
│ ├── connection.py # TCP connection, background receive thread, callbacks
│ └── client.py # Terminal UI, command parser stuff
│
└── shared/
├── protocol.py # Message types & packet framing (shared by both sides client-server)
├── authentication.py # Password salting and hashing, verification
└── tls.py # SSL context, cert generation
Security
-
Mutual TLS (mTLS) - both server and client authenticate with certificates; unauthenticated connections are rejected at the handshake
-
Admin-controlled user provisioning - new users are added by an admin via
provision_user.py -
Certificate fingerprint pinning - each login is verified against the SHA-256 fingerprint of the cert issued at provisioning time
-
Cert expiry checking - expired certificates are explicitly rejected at login (in addition to the TLS handshake)
-
Secure password storage - PBKDF2-HMAC-SHA256 with random salt and 200,000 iterations (TODO: look into higher iterations)
-
Timing-safe authentication - constant-time password comparison and dummy hashing for unknown usernames avoid timing-based attacks
Misc
-
Store-and-forward - messages sent to offline users are stored in the database and delivered automatically when they reconnect
-
Password login - verifys a user's identity using a chosen password, stored as a salted hash
-
Real-time delivery - messages to online users are pushed instantly over open TCP sockets
-
Broadcast messages - send to all online users at once or message is stored for anyone currently offline
-
User registry - persistent user accounts in SQLite
-
On/Offline notifications - join/leave notifications and a
/userscommand showing who is online -
Rich terminal UI - coloured output and formatted tables if
richis installed, plain text fallback if not
python run_server.py [--host HOST] [--port PORT]
--host HOST Address to listen on. Default: 0.0.0.0
--port PORT Port number. Default: 54321On first run, the server generates a self-signed CA and a signed server certificate automatically. These are only created once and reused on subsequent starts.
Run this on the server machine for each user:
python server/provision_user.py <username> [--host <SERVER_LAN_IP>] [--port PORT]You will be prompted for a temporary password. This creates a ready-to-distribute bundle at provision_out/<username>/:
provision_out/<username>/
├── ca.crt # Server CA certificate
├── <username>.crt # User's client certificate
├── <username>.key # User's private key -- keep this secret
├── connect.sh # Quick-connect script (Linux/macOS)
└── README.txt # Instructions for the user
Copy this folder to the user's machine via USB or another trusted channel.
# Using the provided script
cd provision_out/<username>/
./connect.sh
# Or manually
python run_client.py \
--host <SERVER_LAN_IP> \
--port 54321 \
--ca .../ca.crt \
--cert .../<username>.crt \
--key .../<username>.key/login <username>
Then immediately change the temporary password:
/passwd
Once connected to the server you can use the commands below.
| Command | Alias | Description |
|---|---|---|
/login <name> |
Log in (fetches pending messages automatically) | |
/logout |
Log out without disconnecting | |
/msg <user> <text> |
@<user> <text> |
Send a direct message |
/broadcast <text> |
/bc, /all |
Send to all online users |
/fetch |
/inbox |
Manually pull pending messages |
/users |
/who, /list |
Show all users + online status |
/help |
Show help | |
/passwd |
Change password | |
/quit |
/exit, /q |
Disconnect and exit |
More thorough information on how the application works.
python run_server.py [--host 0.0.0.0] [--port 54321]On first run the server will:
- Generate a self-signed CA (
server/CA/ca.crtandca.key) if one does not exist. - Generate a server certificate signed by that CA (
server/server.crtandserver.key). - Create the SQLite database (
server/lanmsg.db).
The CA and server cert are only generated once. Subsequent starts reuse the existing files.
The server logs its certificate fingerprint on startup:
[INFO] Cert fingerprint (SHA-256): AA:BB:CC:...
All users must be provisioned by an administrator directly on the server machine using provision_user.py ensuring only explicitly authorised identities can connect.
provision_user.py does the following in one step:
- Registers the user in the database with a hashed temporary password.
- Generates a client certificate signed by the server's CA.
- Computes and stores the users certificate's SHA-256 fingerprint in the database.
- Writes a distributable bundle to
provision_out/<username>/containing everything the user needs to connect.
| Field | Description |
|---|---|
username |
Case-insensitive, unique |
password_salt |
32-byte random salt (hex) |
password_hash |
PBKDF2 digest (hex) |
cert_fingerprint |
SHA-256 fingerprint of the issued client cert (hex) |
created_at |
UTC timestamp |
Use the connection script from your bundle:
# Linux / macOS
cd provision_out/<username>/
./connect.shOr connect manually:
python run_client.py \
--host <SERVER_LAN_IP> \
--port 54321 \
--ca .../ca.crt \
--cert .../<username>.crt \
--key .../<username>.keyPasswords are hashed with PBKDF2-HMAC-SHA256 using a 32-byte random salt and 200,000 iterations. Verification uses hmac.compare_digest for constant-time comparison. Unknown usernames trigger a dummy hash computation to prevent timing-based user enumeration.
Messages are stored in SQLite immediately on receipt. If the recipient is online, the server attempts live delivery and marks the message as delivered. If the recipient is offline, the message remains pending and is delivered automatically when they next log in, or can be retrieved manually with /fetch.
Every connection requires both sides to present a valid certificate:
- The server presents
server.crt, signed by the CA, with SANs covering its LAN IP and hostname. - The client presents
<username>.crt, signed by the same CA. - The TLS handshake rejects any connection where either side cannot present a CA-signed certificate - unauthenticated clients cannot connect at all.
TLS 1.2 is the minimum version enforced.
The mTLS handshake authenticates the certificate. On top of that, _handle_login performs three additional checks using the cryptography library on the raw DER-encoded certificate:
-
Expiry - the certificate's
notAfterfield is checked against the current UTC time. The TLS handshake also enforces this, but it is checked explicitly (defence-in-depth). -
CN match - the Common Name in the certificate Subject must match the username supplied in the login packet.
-
Fingerprint match - the SHA-256 fingerprint of the presented certificate is compared against the fingerprint stored in the database at provisioning time. This binds each login to the exact certificate that was issued, so a different CA-signed certificate for the same CN is rejected.