End-to-end encryption
The server is a blind, ordered relay. It stores and routes opaque key material and ciphertext, and never holds a key or a plaintext — which also means it cannot do anything clever with the content.
The approach #
Rather than inventing cryptography, Ollacore speaks the transport protocol of a mature, independently audited stack — the Megolm/Olm implementation used by Matrix clients. Your client uses ready-made libraries (including prebuilt Android and web packages); the server provides only the routing endpoints.
Encryption is set per room at creation and is immutable thereafter. A room is either encrypted or it is not; it cannot be downgraded later.
First: mint the token with a device_id #
/v1/e2ee/* endpoint refuses a token that has no explicit
device_id. Encryption is per device, so the server will not let you
publish or claim keys under an ambiguous identity. This is the single most common reason a
first E2EE integration returns 400 on every call.
POST /v1/server/session-tokens
{ "room_id": "0f5c…", "principal_id": "alice", "device_id": "pixel-8-abc" }
^^^^^^^^^^^^^^^^^^^^^^^^^
# Without device_id EVERY /v1/e2ee/* call fails:
# 400 { "code": "invalid_request",
# "message": "e2ee endpoints require a token minted with an explicit device_id" } Key distribution #
POST /v1/e2ee/keys/upload
{ "device_id": "pixel-8-abc",
"device_keys": { "algorithms": [...], "keys": { "curve25519:pixel-8-abc": "…" } },
"one_time_keys": { "signed_curve25519:AAAA": { "key": "…" } } }
# Discover devices — Matrix shape: a map of principal → device ids ([] = all)
POST /v1/e2ee/keys/query { "device_keys": { "alice": [], "bob": ["pixel-8-abc"] } }
POST /v1/e2ee/keys/claim { "one_time_keys": { "bob": { "pixel-8-abc": "signed_curve25519" } } }
Note the keys/query shape: it is a map of principal → device ids
(Matrix's device_keys), not a list of principals. An empty array means "all devices
for this principal".
| Property | Guarantee |
|---|---|
| Atomic issuance | Concurrent claims for the same device receive different keys — never the same one twice. |
| Fallback key | When a device's one-time keys run out, sessions remain establishable rather than failing. |
| Replenishment signalling | Clients are told their key pool is depleting so they can top it up before exhaustion. |
| Device discovery | New devices are announced so existing members re-fetch and include them. |
| Access control | Key retrieval requires sharing an active room with the target principal. |
To-device messages #
POST /v1/e2ee/todevice
{ "event_type": "m.room_key",
"messages": { "bob": { "pixel-8-abc": { …content… } } } } # "*" = all of bob's devices
GET /v1/e2ee/todevice # fetch queued messages; ack to delete event_type is required and sits alongside messages; the sender is
stamped from your token, never taken from the body.
MLS key packages
POST /v1/keypackages
{ "device_id": "pixel-8-abc",
"packages": [ "<base64 MLS key package>", … ] } # the field is "packages"
GET /v1/keypackages/count
GET /v1/rooms/{room_id}/keypackages/{principal_id} # consume one, atomically Encrypted key material is deleted only after the recipient acknowledges it, so a client crash cannot permanently lose the ability to decrypt. Undelivered messages for a device that never comes back are aged out after 30 days.
Forward secrecy #
Membership changes emit durable member.added / member.removed events. Your
client rotates the outbound group session on removal — which is what makes a removed member
provably unable to decrypt anything sent afterwards. The server cannot do this for you; it is a
client obligation the events exist to trigger.
Messages in an encrypted room #
Only the encrypted kinds are accepted: mls.commit, mls.proposal,
mls.application, mls.welcome. The body carries ciphertext. Sending a
plaintext kind is refused with a conflict rather than silently accepted.
Deliberate degradation #
In encrypted rooms the server refuses features it cannot honour, rather than quietly weakening privacy:
- Search and reactions are rejected — both need plaintext.
- Push is forced content-free: no sender, no preview.
- Thumbnails are not generated; send an encrypted micro-thumbnail as a second attachment.
- Attachment verification falls back to size and checksum only.
- Messages critical to the cryptographic chain cannot be edited or deleted.
- Call recording is inherently incompatible with E2EE calls and the flags are mutually exclusive.
Encrypted calls #
Zero server changes required: browsers encrypt each frame with a key derived from the group session using insertable streams, and the SFU forwards frames opaquely — it never needed to see them. This is client work only.