E2EE architecture
Ce contenu n’est pas encore disponible dans votre langue.
The server is intentionally blind to plaintext when E2EE is enabled. It stores ciphertext payloads, wrapped key material, and metadata needed to route those bytes — nothing more. All encryption and decryption happens on the client.
For the user-facing model (key hierarchy, recovery codes, formats), see End-to-end encryption. This page focuses on the server side.
Server-side components
Section titled “Server-side components”All E2EE code lives under src/Pia.Server/E2EE/.
| File | Responsibility |
|---|---|
DeviceEndpoints.cs | REST API for device register, list, approve, revoke; get/set wrapped UMK |
RecoveryEndpoints.cs | Store / retrieve recovery-wrapped UMK, activate via recovery code |
OnboardingSessionService.cs | In-memory session store for device pairing — 10-minute expiry, 32-byte random server challenges |
OnboardingSessionService is registered as Singleton and is in-memory by design: pairing sessions are short-lived and don’t need to survive a restart. In a load-balanced deployment, route all /api/e2ee/* traffic for a given pairing to one node, or replace the implementation with a shared store.
Server entities
Section titled “Server entities”| Entity | PK | What it holds |
|---|---|---|
ServerDevice | DeviceId | Public agreement key, public signing key, device fingerprint, status (Pending / Active / Revoked) |
ServerWrappedUmk | (UserId, DeviceId) | UMK wrapped for one specific device via ECDH-derived KEK |
PiaUser (E2EE columns) | — | IsE2EEEnabled, UmkVersion, RecoveryWrappedUmkCiphertext, RecoveryKdf*, RecoveryWrapVersion |
Note what isn’t there: no plaintext UMK, no plaintext DEKs, no plaintext per-record payloads.
Endpoints
Section titled “Endpoints”Device endpoints
Section titled “Device endpoints”| Method | Path | Purpose |
|---|---|---|
POST | /api/e2ee/devices/register | Register a new device as Pending (stores its public keys) |
GET | /api/e2ee/devices | List the user’s devices |
POST | /api/e2ee/devices/approve | Existing device wraps UMK for a target device |
POST | /api/e2ee/devices/revoke | Revoke a device |
GET | /api/e2ee/devices/wrapped-umk/{deviceId} | Fetch the UMK wrapped for this device |
PUT | /api/e2ee/devices/wrapped-umk | Store a UMK wrapped for a device |
Recovery endpoints
Section titled “Recovery endpoints”| Method | Path | Purpose |
|---|---|---|
POST | /api/e2ee/recovery/wrapped-umk | Store recovery-wrapped UMK (Argon2id-derived KEK) |
GET | /api/e2ee/recovery/wrapped-umk | Retrieve it |
POST | /api/e2ee/recovery/activate | Activate a device by proving possession of the UMK derived from the recovery code |
/recovery/activate requires an HMAC-SHA256 proof: HMAC(HKDF(UMK, "activation"), serverChallenge). The server compares the proof against its own computation; the UMK never leaves the device in plaintext during this exchange.
Onboarding workflows
Section titled “Onboarding workflows”First device
Section titled “First device”- Client generates UMK + ECDH (P-256) + ECDSA (P-256) keypairs.
- Client self-wraps the UMK using its own ECDH agreement key.
- Client generates a recovery code (Base32, 128-bit entropy).
- Client wraps the UMK with the recovery-derived KEK (Argon2id + AES-GCM) and uploads the blob via
/recovery/wrapped-umk. - Client registers the device via
/devices/register. Server creates theServerDevicerow inActivestatus (first device is auto-active).
Additional devices
Section titled “Additional devices”- New device registers as
Pending(uploads public keys). - Server creates an onboarding session — 10-min expiry, fresh 32-byte challenge.
- An existing
Activedevice approves the request: ECDH-wraps the UMK for the new device’s public agreement key and uploads the blob via/devices/approve. - New device fetches its wrapped UMK via
/devices/wrapped-umk/{deviceId}, unwraps locally, and is upgraded toActive.
Recovery activation
Section titled “Recovery activation”- User enters recovery code on a fresh device.
- Client derives KEK via Argon2id using the salt and parameters fetched from the server.
- Client unwraps the UMK from
/recovery/wrapped-umk. - Client proves possession by computing the HMAC and posting it to
/recovery/activate. - Server marks a new device
Active; client self-wraps the UMK for ongoing use.
Security invariants the server enforces
Section titled “Security invariants the server enforces”- The UMK is never accepted in plaintext on any endpoint — only as wrapped blobs.
- All wrapped blobs include AAD that binds them to a context (
pia-umk-wrap-v1:{targetDeviceId}for device wraps,pia-dek-wrap-v1:{entityType}:{entityId}for record DEKs). A blob from one context can’t be replayed in another. - Onboarding sessions use 32-byte random challenges and expire after 10 minutes.
- Device fingerprint = SHA-256(AgreementPublicKey) truncated to 64 bits, displayed as
XXXX-XXXX-XXXX-XXXX. Used in the admin Devices view and in the desktop’s pairing UI.