← All projects

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
Two terminal windows from one run. The server sends p=7919, g=7 and its public value A; the client replies with its public value B; both print the same shared secret and AES key. The server then sends the encrypted time, which the client decrypts, and the server decrypts the client's encrypted reply.
One run with debug output on. Both sides print the same AES key, but only p, g, A and B cross the network.

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:

  1. The server picks a secret a and sends the public prime p, the generator g and its public value A = ga mod p.
  2. The client checks that p is prime and g is a primitive root, and stops if either check fails. It then picks its own secret b and replies with B = gb mod p.
  3. Both sides compute the same shared secret (Ba mod p on the server, Ab mod p on the client) and hash it with SHA-256 to get a 128-bit AES key.
  4. 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.server that 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.py module 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_REUSEADDR so 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, A and B are 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 secrets module rather than random, whose output can be predicted.
  • Parameter validation: the server checks at start-up that p is prime and g is 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: .gitignore excludes key files, plaintext and ciphertext dumps, certificates, private keys and .env files.

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.