React 18 + Vite partner/admin interface for the Privacy-Preserving Secure Aggregation system. Partners sign in with their organisational Azure AD account and the browser automatically runs the full four-phase ML-KEM-768 protocol — no manual coordination, no client secrets.
- Node.js 20+
- The .NET API running locally (
AIRoundTableSecretSharingAPI, default port 5149) - An Azure Entra ID account in the configured tenant that belongs to either the Partner or Admin security group
npm install # first time only
npm run devThe UI runs on http://localhost:3000. All /api/* requests are proxied to http://localhost:5149 by Vite.
Protocol overview and sign-in prompt.
Automated four-step wizard for registered partners:
| Step | What happens automatically |
|---|---|
| 1 — Key Generation | Generates an ML-KEM-768 key pair in the browser; posts the encapsulation key (1184 bytes) to the API; waits for all epoch partners to register |
| 2 — Encapsulation | For each partner with a smaller OID, encapsulates and posts a ciphertext (1088 bytes) to the API |
| 3 — Decapsulation | Polls for incoming ciphertexts; decapsulates each using the local private key to derive shared secrets |
| 4 — Submit Data | User enters actual MAU values per country / month; the UI applies HMAC-SHA256 noise and submits masked values |
Key material (key pair, shared secrets, epoch ID) is stored in localStorage keyed by the user's OID. On each page load the stored epoch ID is compared to the current epoch; stale state is cleared automatically when a new epoch is detected.
Available to Admin group members only:
- View all registered partners
- Select a subset (≥ 2) to participate in a new epoch
- Clear all data
Query the aggregate for any country / month combination in the current epoch. Available once all epoch partners have submitted.
MSAL v5 (@azure/msal-browser + @azure/msal-react) with interactive redirect flow. The access token is injected into every API request automatically. The groups claim in the token determines Admin vs Partner access.
The web UI reads its Entra ID and group IDs from Vite environment variables:
| Variable | Purpose |
|---|---|
VITE_MSAL_CLIENT_ID |
SPA app registration client ID |
VITE_MSAL_AUTHORITY |
Entra authority URL |
VITE_MSAL_API_SCOPE |
API scope requested for access tokens |
VITE_ADMIN_GROUP_ID |
Admin security group object ID used for Admin navigation gating |
VITE_PARTNER_GROUP_ID |
Partner security group object ID, kept in sync with backend configuration |
web-ui/
├── src/
│ ├── pages/
│ │ ├── Home.jsx # Landing page (signed out) / epoch dashboard (signed in)
│ │ ├── AdminPanel.jsx # Epoch management (admin only)
│ │ └── Results.jsx # Aggregate results
│ ├── components/
│ │ ├── EpochDashboard.jsx # Open epochs + per-epoch key status; hosts the protocol
│ │ └── EpochProtocol.jsx # Automated 4-step protocol for one epoch
│ ├── utils/
│ │ ├── localCrypto.js # localStorage key/secret persistence + key status
│ │ ├── api.js # All API calls; Entra token injected via acquireApiToken
│ │ ├── crypto.js # ML-KEM-768 via `mlkem` npm package
│ │ └── noise.js # HMAC-SHA256 noise; matches C# & Python exactly
│ ├── App.jsx # Routing; admin flag from access token groups claim
│ ├── main.jsx # MSAL provider setup
│ └── index.css # Global styles
├── index.html
├── package.json
└── vite.config.js # Proxy /api → localhost:5149
The JavaScript noise calculation in noise.js is mathematically identical to SecureNoiseGenerator.cs (C#) and submit.py (Python):
noise = HMAC-SHA256(sharedSecret, "{country}|{month}")
→ first 8 bytes as LE signed int64
→ (seed % (2×maxNoise+1) + (2×maxNoise+1)) % (2×maxNoise+1) − maxNoise
All three runtimes produce the same value for any given shared secret + country + month tuple, which is required for the noise to cancel in the aggregate.