Skip to content

End-to-end encryption

MOQOM encrypts media payloads with SFrame (RFC 9605) carried over MOQT. Only the payload is encrypted, so relays can still cache, prioritise and fan out objects — they just cannot read them. Tampering with an object’s group, object number, priority or properties causes decryption to fail.

Not even MOQOM can see the media. Today your app distributes the room key, and MOQOM’s relays, control plane and cloud never receive it. Encryption adds no perceptible latency: about 54 µs per 1080p keyframe against a 33 ms frame. Its real cost is bytes, 19 per object, which is 0.9% of a 2 KB video object but a larger share of a small audio packet.

Default cipher suite AES-256-GCM (SFrame suite 0x0005)
Also supported AES-128-GCM
Overhead 19 bytes per object
Transport encryption TLS 1.3 on every QUIC connection, always — independent of SFrame

On-device SFrame cost measured at roughly 7.5 µs per Opus packet and 54 µs per 1080p keyframe, against a 33 ms frame budget.

Every room requires end-to-end encryption unless its backend has turned it off. The room’s policy travels to each device in the e2ee claim of the signed client token: "required" or "optional". A token without the claim is treated as "required", so an older control plane or a hand-minted token can only make the SDK stricter, never looser.

In a room that requires encryption, MoqomKit:

  • refuses to publish without a key. startBroadcast and startAudio throw MoqomError.encryptionRequired before anything is announced to the relay;
  • drops unencrypted incoming media. With no key set, media from other participants is discarded and room.events yields .encryptionRequired(participant) once per participant. With a key set, an object that does not decrypt is discarded and counted in room.decryptionStatistics;
  • stops a clear publication if the policy tightens mid-call. If a renewed token says "required" and no key is set, the SDK ends the broadcast and audio and yields .encryptionRequired for the local participant.

The policy covers audio and video. Room metadata, such as the track catalog and the room’s control log, is not media and still travels under TLS only.

Set encryption on the room before going live. Every participant needs the same key material.

let room = try await session.join(room: RoomID("demo"))
let encryption = RoomEncryption.sharedKey(baseKey, keyID: 1) // AES-256-GCM by default
await room.useEncryption(encryption)
try await room.startBroadcast(from: camera).start()

room.encryptionPolicy is the policy the token carried, and room.isEncrypted says whether a key is set. Without the useEncryption call, the startBroadcast above throws MoqomError.encryptionRequired.

RoomEncryption.sharedKey is the simplest scheme that is genuinely end to end: every member holds one base key. Its limits are real — any member can impersonate any other, and removing a member means rekeying everyone — so it suits small, trusted calls. Distribute the key over a channel MOQOM does not see (for example, your own end-to-end encrypted messaging).

room.decryptionStatistics reports objects that failed to decrypt, which is the first thing to check when one participant sees black tiles.

Some rooms need the server to see media: server-side recording, cloud classifiers, or a broadcast that has to reach clients without key distribution. Only a backend can turn encryption off, one room at a time, and it has to say why. Set the room’s e2ee policy to optional with UpdateRoomPolicy, name e2ee in the update mask, and give actor.reason; the control plane refuses the change without a reason.

r := client.Room("live/42")
_, err := r.UpdatePolicy(ctx, moqom.PolicyUpdate{
Policy: moqom.Policy{E2EE: moqom.E2EEOptional},
Fields: []string{"e2ee"},
Actor: moqom.Actor{Reason: "server-side recording for the weekly broadcast"},
})
use moqom_sdk::{Actor, E2eePolicy, Policy, PolicyUpdate};
client.room("live/42").update_policy(PolicyUpdate {
policy: Policy { e2ee: E2eePolicy::Optional, ..Default::default() },
fields: vec!["e2ee".into()],
actor: Actor { reason: "server-side recording for the weekly broadcast".into(), ..Default::default() },
}).await?;

The same rule applies to CreateRoom: creating a room with e2ee optional needs a reason. Either way, the change is written to the room’s moderation log with a policy_change such as e2ee: required -> optional and announced on the room’s event stream as a moderation event.

Tokens minted after the change carry "e2ee": "optional", and devices pick it up at their next token renewal. In an optional room, a participant without a key publishes and receives in the clear. A participant who has called useEncryption still encrypts what it sends and still discards media it cannot decrypt, so mixing keyed and unkeyed participants in one room leaves each unable to see the other. Setting the policy back to required needs no reason, and devices that have no key stop publishing at their next renewal.

An UpdateRoomPolicy call with no update mask applies every field, so a request that leaves e2ee unset puts the room back to required.

Unencrypted End-to-end encrypted
Relay sees Payloads Ciphertext only
Server-side recording / classifiers Possible Not possible — watchdogs see ciphertext
On-device taps (captions, classification) Yes Yes — taps receive plaintext on the device that holds the key
Moderation (kick, ban, force-mute) Yes Yes — moderation acts on sessions, not content

Group key management for E2EE rooms is designed around MLS (RFC 9420) for small rooms and sender keys for large ones, with SFrame underneath both. This is not yet built; until it ships, use sharedKey with your own key distribution.