Aller au contenu

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.

All E2EE code lives under src/Pia.Server/E2EE/.

FileResponsibility
DeviceEndpoints.csREST API for device register, list, approve, revoke; get/set wrapped UMK
RecoveryEndpoints.csStore / retrieve recovery-wrapped UMK, activate via recovery code
OnboardingSessionService.csIn-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.

EntityPKWhat it holds
ServerDeviceDeviceIdPublic 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.

MethodPathPurpose
POST/api/e2ee/devices/registerRegister a new device as Pending (stores its public keys)
GET/api/e2ee/devicesList the user’s devices
POST/api/e2ee/devices/approveExisting device wraps UMK for a target device
POST/api/e2ee/devices/revokeRevoke a device
GET/api/e2ee/devices/wrapped-umk/{deviceId}Fetch the UMK wrapped for this device
PUT/api/e2ee/devices/wrapped-umkStore a UMK wrapped for a device
MethodPathPurpose
POST/api/e2ee/recovery/wrapped-umkStore recovery-wrapped UMK (Argon2id-derived KEK)
GET/api/e2ee/recovery/wrapped-umkRetrieve it
POST/api/e2ee/recovery/activateActivate 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.

  1. Client generates UMK + ECDH (P-256) + ECDSA (P-256) keypairs.
  2. Client self-wraps the UMK using its own ECDH agreement key.
  3. Client generates a recovery code (Base32, 128-bit entropy).
  4. Client wraps the UMK with the recovery-derived KEK (Argon2id + AES-GCM) and uploads the blob via /recovery/wrapped-umk.
  5. Client registers the device via /devices/register. Server creates the ServerDevice row in Active status (first device is auto-active).
  1. New device registers as Pending (uploads public keys).
  2. Server creates an onboarding session — 10-min expiry, fresh 32-byte challenge.
  3. An existing Active device approves the request: ECDH-wraps the UMK for the new device’s public agreement key and uploads the blob via /devices/approve.
  4. New device fetches its wrapped UMK via /devices/wrapped-umk/{deviceId}, unwraps locally, and is upgraded to Active.
  1. User enters recovery code on a fresh device.
  2. Client derives KEK via Argon2id using the salt and parameters fetched from the server.
  3. Client unwraps the UMK from /recovery/wrapped-umk.
  4. Client proves possession by computing the HMAC and posting it to /recovery/activate.
  5. Server marks a new device Active; client self-wraps the UMK for ongoing use.
  • 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.