Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Voice and Video Calls

Konstruct carries 1:1 real-time calls over WebRTC. This chapter specifies how call setup is protected by the same end-to-end cryptography as chat, and how the call media is encrypted between the two devices — and states honestly what the connectivity layer still exposes.

Status: audio 1:1 is implemented; video is wired through the call-entry UI but the media layer is not yet enabled (construct-ios Services/Calls/CallsFeature.swift:14, isVideoEnabled = false). Group calls / SFU are out of scope (Chapter 7).

10.1 Two planes: signalling and media

A WebRTC call has two data planes:

  • the signalling plane — the SDP offer/answer that negotiates codecs and keys, and the ICE candidates that discover a network path;
  • the media plane — the actual encrypted audio/video (SRTP).

Konstruct's property is that the signalling plane rides the existing end-to-end-encrypted message path, and because the media keys are agreed inside that signalling, the media plane is end-to-end encrypted between the two devices too.

10.2 Signalling over the E2EE path

SDP offers/answers and ICE candidates are not sent to the server in the clear. They are encrypted with the peer's Double Ratchet session (Chapter 5) as call-signal messages and are sealed like any other in-scope user traffic (Chapter 8) — so the server can read neither the SDP (which carries the DTLS fingerprints and ICE ufrag/pwd) nor the sender identity. On the ordinary message path, the real call content type (12) rides inside the encrypted KNST frame; SealedInner.content_type and the outer envelope remain UNSPECIFIED (construct-ios Services/Calls/CallManager.swift:1295-:1300). The receiver's core names a decrypted call signal by that byte and hands its body to the client as CallSignalDecrypted (construct-core 0.29.0, Chapter 5 §5.8.4); until then the core read only the envelope's type, so a sealed call signal surfaced as an ordinary decrypted message and each client had to look inside the frame itself. The client-side framing that carries the suite id and PQ ratchet fields for a call signal is CallSignalCrypto (Services/Calls/CallSignalCrypto.swift:87-:104); orchestration is in Services/Calls/CallManager.swift.

Two delivery routes exist, both E2EE:

  • the message path — the first offer is typically delivered E2EE ahead of the answer (a backgrounded callee is woken by a PushKit VoIP token that carries no call content);
  • a low-latency signal stream (SignalingService.Signal, opened via SignalingServiceClient) for real-time exchange once both peers are live. The server relays these frames without being able to decrypt them.

ICE candidate batching. ICE candidates are flushed in size-bounded batches to stay under the signalling rate limit and, critically, under the padded-frame size cap — Suite-3 PQ blobs per candidate can otherwise overflow a single frame and drop the whole batch, so the client splits a large flush into chunks (CallManager.swift, Shared_Proto_Signaling_V1_IceCandidate).

TURN credentials for relayed connectivity are fetched per call (SignalingServiceClient.getTurnCredentials).

10.3 Media encryption

Call media uses WebRTC's standard DTLS-SRTP. The DTLS handshake that establishes the SRTP keys authenticates itself with certificate fingerprints — and those fingerprints are exchanged inside the SDP, which travelled over the E2EE signalling channel above. An active network attacker therefore cannot substitute its own DTLS fingerprints (that would require forging an E2EE-authenticated SDP), so for a 1:1 call the media is end-to-end encrypted between the two devices. No application-layer media re-encryption (e.g. SFrame) is required for the two-party case; SFrame would only be needed for a future group/SFU topology where a server forwards media.

The DTLS handshake is post-quantum since 2026-10-01 (iOS, construct-ios 008c7f80): DTLS 1.3 with the hybrid key exchange X25519MLKEM768, so the SRTP keys come from a secret a recording adversary needs both X25519 and ML-KEM-768 to recover. libwebrtc carries the group behind the field trial WebRTC-EnableDtlsPqc, which the client turns on before it builds its peer-connection factory (webrtc-sdk 150.7871.01). The fingerprints that authenticate the handshake travel in the SDP, inside the post-quantum session, so an active attacker cannot steer the group down without replacing a fingerprint.

Observed on the wire (two clients, 2026-10-01): ClientHello supported_versions DTLS 1.3, key shares X25519MLKEM768 (1216 bytes) and X25519 (32 bytes); ServerHello X25519MLKEM768 (1120 bytes); TLS_AES_128_GCM_SHA256.

Limits:

  • The group is chosen by the two endpoints. A peer whose build does not set the trial agrees on X25519 alone, silently — libwebrtc does not expose the negotiated group to the application, so the client cannot refuse such a call. Both current builds set it; the Android client has no calls yet and will set it when it does.
  • A test (WebRTCFieldTrialsTests) fails if a WebRTC update removes the trial from the binary; that is the only warning such an update would give.
  • Video, when it ships, rides the same DTLS-SRTP transport and is covered without further work.

Changed 2026-10-01 (PQC-4). Until then this section said the protection was classical: the fingerprints were authenticated by the post-quantum session, but the SRTP keys came from an ECDHE-only DTLS handshake a future quantum computer could break.

Neither the Konstruct server nor a TURN relay used for NAT traversal can decrypt the media: a TURN relay forwards only opaque SRTP.

10.4 What the connectivity layer still exposes

Honest counterpart to §10.1 — even with signalling sealed and media DTLS-SRTP-encrypted:

ExposureNote
That a call is being set up, and its timingThe signal exchange and TURN-credential fetch are observable as events (sealed, but present).
Participants' network addressesICE exchanges candidate IP:port pairs so the devices can find a path; a TURN relay sees both peers' addresses. This is connection metadata, not call content.
Media timing / volumeInherent to real-time media; not hidden.
Call content (audio/video)Not exposed — DTLS-SRTP, keyed via E2EE signalling. Post-quantum since 2026-10-01: the DTLS 1.3 key exchange is X25519MLKEM768 (§10.3), unless the peer's build does not offer it.

Because ICE reveals network addresses to establish direct connectivity, a privacy-maximising user who wants to hide their address from the peer or a relay should force TURN-relayed connectivity (so only the relay's address is shared with the peer) or route through a network-layer anonymiser; this is a connectivity/anonymity trade-off, not a content-confidentiality one.

10.5 References

  • Client (construct-ios): Services/Calls/CallManager.swift, Services/Calls/CallSignalCrypto.swift, Services/Calls/WebRTCSession.swift, Services/Calls/CallsFeature.swift; Networking/gRPC/Services/SignalingServiceClient.swift.
  • Protos: konstruct-msg/construct-protos signaling package (IceCandidate, Signal).
  • Sealing of call signals: Chapter 8; message encryption: Chapter 5.
  • Design detail (construct-docs): client/specs/CALLS_CLIENT_SPEC.md.