Secure Messenger: Technical Summary
How the end-to-end encrypted messenger from my Spring 2026 independent study in applied cryptography is put together: the frontend, the backend, the encryption, and how it is tested and deployed.
At a Glance
| Layer | Technology | Role |
|---|---|---|
| Frontend | React 19, Vite, JavaScript | The chat interface, plus every key generation, encryption, and decryption step. Hosted on Vercel. |
| Cryptography | Written from scratch with JavaScript BigInt | RSA, 3-prime RSA, and elliptic curve Diffie–Hellman, built on hand-written modular arithmetic and Miller–Rabin primality testing. |
| Backend | Python, FastAPI, SQLAlchemy, Alembic | A REST API that stores accounts, public keys, and ciphertext. Hosted on Render. |
| Database | PostgreSQL (SQLite for local development) | Users, public keys, conversations, and encrypted messages. |
| Passwords | bcrypt (via passlib) | Passwords are stored only as salted bcrypt hashes. |
| Testing | Vitest, pytest, Cypress, ESLint | 20 cryptography unit tests, 27 backend API tests, and a browser-level end-to-end test. |
| CI | GitHub Actions | Runs the backend tests, linting, the frontend tests, and a production build on every push and pull request. |
How a Message Travels
The guiding rule is that plaintext and private keys never leave the browser. The server only ever stores and relays public keys and ciphertext.
Writes the message, encrypts it with the recipient's public key (or the shared secret), and sends only the ciphertext.
Stores the ciphertext and the users' public keys. It never sees plaintext or private keys.
Fetches the ciphertext and decrypts it with a private key that only it holds.
In more detail:
- Sign up and log in. The server stores a bcrypt hash of the password, never the password itself.
- Start a conversation and choose a method: RSA, 3-prime RSA, or elliptic curve Diffie–Hellman (ECDH). The method is fixed for the life of the conversation.
- Generate keys in the browser. For the RSA methods, each user has one key pair per method; the public half is uploaded and the private half stays in the browser. For ECDH, the server creates fresh public curve parameters for the conversation, each participant picks a secret number in the browser, and only the resulting public point is uploaded.
- Wait for the other person's key. Until both public keys exist, the conversation shows a waiting state and checks the server every three seconds.
- Encrypt and send. With RSA, only the recipient's private key can open a message, so the sender's browser also encrypts a second copy under the sender's own public key; that is what lets you read your own sent history. With ECDH, both sides derive the same shared secret, so a single ciphertext works for both.
- Decrypt on arrival. When a conversation loads, each message is decrypted locally as it is displayed.
The mathematics of each method, with small worked examples, is on the Encryption Methods page.
Frontend
The client is a single-page React app built with Vite, with components for logging in, registering, choosing who to talk to, the conversation view, and the message box. All of the cryptography lives in client/src/crypto/:
- A pluggable method registry (
methods.js). Each encryption method is one entry that knows how to generate keys, encrypt, decrypt, and serialize its keys. The interface never branches on a method's name, so adding a new method means writing one module and adding one entry. - RSA and multi-prime RSA (
rsaFamily.js). One implementation parameterized by the number of primes: two 512-bit primes for standard RSA, three 342-bit primes for 3-prime RSA, both giving a modulus of about 1024 bits. - Elliptic curve arithmetic (
ecc.js). Point addition, point doubling, and double-and-add scalar multiplication, written directly from the group law. - Big-integer number theory (
bigint-utils.js). JavaScript has no built-in modular exponentiation, modular inverse, or primality test, so these are implemented by hand: square-and-multiply exponentiation, the extended Euclidean algorithm, and a 20-round Miller–Rabin test, with randomness from the browser's cryptographic random number generator. - Key storage (
keyStorage.js). Private keys are kept in the browser's local storage, in one place, so every component reads and writes them the same way.
Because RSA can only encrypt numbers smaller than its modulus, the message box shows a running character count against the maximum length the current keys allow.
Backend
The server is a FastAPI application with a thin route layer over service modules, using SQLAlchemy for the database and Alembic for schema migrations. Its main endpoints are:
| Endpoint | Purpose |
|---|---|
POST /users, POST /login | Create an account; log in and receive your stored public keys. |
GET /users, GET /users/{username} | Find people to message. |
POST /users/{id}/keys | Upload a public key for an RSA-family method. |
POST /conversations | Start a conversation with a chosen method; for ECDH the server also generates the curve parameters. |
POST /conversations/{id}/keys | Submit your public point for an ECDH conversation. |
POST /messages | Store an encrypted message (plus the sender's own copy, for RSA). |
GET /conversations/{a}/{b} | Paginated message history between two users, still encrypted. |
The data model has five tables: users, per-method public keys, conversations (with the chosen method and any curve parameters), per-conversation public keys, and messages, which hold only ciphertext. In production the API runs on Render with a managed PostgreSQL database; every deploy applies any pending migrations before the server starts, and cross-origin requests are limited to an allow-list of frontend addresses.
Testing and Deployment
- Cryptography unit tests (Vitest, 20 tests) check the modular arithmetic, prime generation, elliptic curve point arithmetic, RSA and 3-prime RSA round trips, and that both sides of an ECDH exchange arrive at the same shared secret.
- Backend tests (pytest, 27 tests) exercise authentication, password hashing, users, conversations, key submission, and messages against a test database.
- End-to-end test (Cypress) drives a real browser: two new users sign up, set up an RSA conversation, and one sends a message the other decrypts. It runs locally against the development servers.
- Continuous integration (GitHub Actions) runs on every push and pull request, with one job for the backend tests and one that lints, tests, and builds the frontend.
- Hosting. The frontend is deployed on Vercel and the backend on Render.
Design Notes
This is a learning project. Every algorithm is implemented from scratch so that the mathematics is visible, which also means it is not hardened the way production messengers are:
- RSA is used in its “textbook” form, without the randomized padding (such as OAEP) that real systems require, and with 1024-bit moduli where current guidance calls for at least 2048 bits.
- The elliptic curves are deliberately tiny (the field prime is below 10,000), and the shared secret becomes a single-byte key for an XOR cipher. They demonstrate the Diffie–Hellman idea rather than provide real security.
- Private keys live in the browser's local storage, so clearing site data or switching devices loses access to old messages.
A production version would use audited libraries, for example X25519 key agreement with AES-GCM encryption.