Federated learning where the aggregator can compute on your gradients but can never see them — enforced by encryption, not policy.
Clients train locally on their own data and send only homomorphically-encrypted gradients. The aggregator performs the averaging math directly on ciphertext — it never has access to the secret key needed to decrypt anything, by design rather than by policy.
(screenshot placeholder — add your dashboard screenshot here)
In standard federated learning, clients still send somewhat-sensitive gradient information to a central server in plaintext. PrivacyMesh closes that gap using CKKS homomorphic encryption (via TenSEAL): clients encrypt their gradients before sending them, the aggregator performs the sum/average math directly on ciphertexts, and only the clients — who hold the only copy of the secret key — can decrypt the final result.
The aggregator is not simply trusted not to peek. It is structurally incapable of decrypting anything, because it only ever holds a public-key context with the secret key stripped out. This repo includes a standalone proof of that guarantee (key_separation_poc.py) that deliberately tries to decrypt on the aggregator side and shows TenSEAL refusing.
┌──────────────┐ encrypted gradient ┌──────────────────┐
│ Client 1 │ ───────────────────────────────▶ │ │
│ (local train,│ │ Aggregator │
│ encrypt) │◀───────────────────────────────── │ (sums/averages │
└──────────────┘ encrypted averaged update │ ciphertexts, │
│ never decrypts) │
┌──────────────┐ encrypted gradient │ │
│ Client 2 │ ───────────────────────────────▶ │ │
└──────────────┘◀───────────────────────────────── └──────────────────┘
encrypted averaged update
┌─────────────┐
loss values ────▶ │ Dashboard │ (loss only — never gradients/data)
(plaintext, │ (FastAPI + │
non-sensitive) │ live chart)│
└─────────────┘
- Encryption: CKKS scheme via TenSEAL — supports real-valued arithmetic on ciphertexts (needed since gradients are floats, not integers)
- Training: PyTorch, CPU-only
- Networking: gRPC between clients and aggregator
- Orchestration: Docker + docker-compose (each component is a separate container)
- Monitoring: FastAPI dashboard, receives only loss values (never gradients or raw data) for live visualization
This project isn't just "it runs" — each of these was independently tested and verified before being combined:
- Homomorphic addition is correct — encrypted sum matches plaintext sum within floating-point tolerance (
poc_homomorphic.py) - The aggregator cannot decrypt, even if it tries — public/secret key separation is enforced by the library itself, not by convention (
key_separation_poc.py) - Real multi-layer PyTorch gradients survive the full encrypt → aggregate → decrypt round trip, correctly reconstructed and applied via an optimizer step (
real_gradients_poc.py) - Multi-round training genuinely converges — loss decreases monotonically across 8 rounds for every client tested
- Works across independently networked environments — verified running with the aggregator on native Windows (Docker Desktop) and clients inside WSL2's separate Linux environment, communicating over a real network boundary
- Docker Desktop
- ~2GB free disk space (CPU-only PyTorch, much smaller than the GPU build)
- Generate the shared encryption keys (one-time):
cd shared
docker run --rm -v "${PWD}:/app" -w /app python:3.11-slim sh -c "pip install tenseal numpy && python keygen.py"This produces secret_context.bin and public_context.bin.
- Distribute the keys:
cp secret_context.bin ../client/
cp public_context.bin ../aggregator/Important: secret_context.bin must never end up in the aggregator/ folder — that would defeat the entire security model.
- Run everything:
cd ..
docker compose up --build- Open the live dashboard:
http://localhost:8000
You'll see 8 rounds of federated training run automatically across two simulated clients, with a live-updating loss chart.
privacymesh/
├── docker-compose.yml
├── proto/federated.proto # gRPC message definitions
├── shared/keygen.py # one-time shared key generation
├── aggregator/ # holds ONLY the public key context
│ └── server.py # gRPC server, barrier-synchronized multi-client aggregation
├── client/ # holds the secret key
│ └── client.py # local training loop, encryption, gRPC calls
├── dashboard/ # FastAPI + Chart.js, receives loss values only
│ └── server.py
├── poc_homomorphic.py # standalone proof: encrypted math is correct
├── key_separation_poc.py # standalone proof: aggregator cannot decrypt
└── real_gradients_poc.py # standalone proof: real PyTorch gradients work end-to-end
- Key distribution is simulated, not solved. All clients currently share one encryption context generated by a single trusted
keygen.pyrun. Real-world federated learning with genuinely independent, mutually-untrusting clients would need a proper distributed key generation protocol (multi-party computation) — a legitimate direction for future work. - Barrier synchronization is simple. The aggregator waits for exactly
NUM_CLIENTSsubmissions per round before aggregating. It doesn't yet handle stragglers, dropped clients, or asynchronous rounds — all real considerations in production FL systems. - Cloud deployment is architected but not yet live. The system is designed to deploy identically to any environment (swap
AGGREGATOR_HOSTfor a public IP) — validated across Windows/WSL2 as a stand-in for genuinely separate machines. Deployment to a cloud provider (Oracle Cloud / Fly.io / GCP) is a natural next step once account verification is sorted out.
Built as a self-directed exploration into combining homomorphic encryption with practical machine learning — extending earlier work on privacy-preserving systems (EncryptedHealthLedger, MediProof) from statistics and credential verification into the ML domain.