Security · Cryptography · Lab work
Secure Web Server
A Python client and server that agree a secret key over the network with a Diffie–Hellman key exchange, then use it to encrypt the messages they send each other with AES. It started as a basic HTTP server, and I'm hardening it one lab at a time on the way to a properly secure web server.
- Python
- TCP sockets
- Diffie–Hellman
- AES
- PyCryptodome
How it works
The server (Alice) and client (Bob) talk over a raw TCP socket. Every connection starts with a fresh key exchange, so no key is ever stored or sent:
- The server picks a secret
aand sends the public primep, the generatorgand its public valueA = ga mod p. - The client checks that
pis prime andgis a primitive root, and stops if either check fails. It then picks its own secretband replies withB = gb mod p. - Both sides compute the same shared secret (
Ba mod pon the server,Ab mod pon the client) and hash it with SHA-256 to get a 128-bit AES key. - The server pads the current time, encrypts it and sends it. The client decrypts it and sends back an encrypted acknowledgement, which the server decrypts.
My role & how it evolved
Solo project: I wrote all of the code. I've built it up through weekly lab exercises, and each step fixed the biggest weakness of the one before:
- Starting point: an HTTP server on Python's
http.serverthat answered GET with HTML and POST with the time as JSON. - Week 1: dropped down to raw TCP sockets, with the server sending the time to a new client in plain text.
- Week 2: encrypted both directions with AES-128, using a random key the server wrote to a file for the client to read.
- Week 3: replaced the key file with a Diffie–Hellman exchange and moved the shared maths into a
utils.pymodule used by both sides.
Key features
- Diffie–Hellman written from the maths up: a primality test, a primitive-root check and modular exponentiation with Python's three-argument
pow(). - AES-128 encryption with PKCS#7 padding in both directions, using PyCryptodome.
- One helper module shared by client and server, so both derive the key in exactly the same way.
- A socket server that handles clients in turn and sets
SO_REUSEADDRso it can restart straight away. - Step-by-step debug output (secrets, padded plaintext and ciphertext in hex) so every stage can be checked by hand.
Security
- No key on the wire: only the public values
p,g,AandBare sent. Each side derives the AES key locally, and the server generates a new secret for every connection, so each session gets its own key. - Secure randomness: secret values come from Python's
secretsmodule rather thanrandom, whose output can be predicted. - Parameter validation: the server checks at start-up that
pis prime andgis a primitive root. The client re-checks the values it receives instead of trusting the server. - Key derivation: the shared secret is hashed with SHA-256 and cut to 16 bytes, rather than being used directly as the key.
- Secrets kept out of Git:
.gitignoreexcludes key files, plaintext and ciphertext dumps, certificates, private keys and.envfiles.
What I'd improve next
This version is built to make the maths visible, not to be deployed. These are the changes I'd make first:
- Replace the 13-bit prime (7919) with a standard 2048-bit group (RFC 3526) or X25519. With a prime this small there are fewer than 8,000 possible keys, so an eavesdropper could try them all in moments.
- Swap ECB mode for an authenticated mode such as AES-GCM, so repeated blocks don't show through and tampering is detected. I'd also derive the key with HKDF instead of a bare hash.
- Authenticate the key exchange, for example by signing the server's public value. Unauthenticated Diffie–Hellman is open to a man-in-the-middle.
- Remove the debug output that prints secrets, add length-prefixed message framing instead of relying on a single
recv(1024), then put HTTP back on top.