Skip to content

bennycode/sealedcall

Repository files navigation

SealedCall.com — Private Video Calls

End-to-end encrypted video calls where the signaling itself is encrypted via XMTP's decentralized messaging protocol.

Encryption Layers

  1. XMTP MLS — All signaling messages (SDP offers, answers, ICE candidates) are encrypted end-to-end using XMTP's Messaging Layer Security protocol. No server can read them.
  2. DTLS-SRTP — WebRTC's mandatory transport encryption protects all media (audio/video) in transit. Once the peer connection is established, audio and video flow directly between peers.
  3. E2E Media Encryption (Encoded Transform) — On top of DTLS-SRTP, every audio and video frame is encrypted with AES-128-GCM using the WebRTC Encoded Transform API (RTCRtpScriptTransform). The symmetric key is generated by the caller and shared with the callee over the already-encrypted XMTP signaling channel. This means even if a TURN relay or any intermediary were present, it could never decrypt the actual media content.
  4. No central signaling server — Unlike typical WebRTC apps, there is no WebSocket server that could be compromised. XMTP's decentralized network replaces it entirely.

What about STUN servers?

This app uses Google's public STUN servers (stun.l.google.com) for NAT traversal. STUN servers do not compromise E2EE — their only role is helping peers discover their public IP addresses so they can establish a direct connection. They never see any media content, signaling data, or message payloads.

Component What it knows What it can't see
STUN server That two IP addresses are trying to connect Who the users are, what they're saying, any media content
XMTP network Encrypted signaling blobs between two inbox IDs SDP/ICE content (encrypted via MLS)
No one else Everything is E2EE

Even if a TURN relay were added (for networks where direct connections fail), both DTLS-SRTP and the Encoded Transform E2E layer encrypt all media — the relay would only forward opaque encrypted bytes that it cannot decrypt.

Setup

# Install dependencies
npm install

# Start the dev server
npm run dev

Important: The Vite dev server is configured with the required Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers that the XMTP Browser SDK needs for SharedArrayBuffer/WASM support.

How to Make a Call

Person A (Host)

  1. Open the app
  2. Click Start Call — a secure identity is created automatically
  3. Click Start Camera (or skip for audio-only)
  4. Click Copy invite link and send it to the person you want to call

Person B (Joining)

  1. Open the invite link
  2. Click Start Call
  3. Click Start Camera (or skip for audio-only)
  4. Click Call — the connection is established automatically

During the Call

  • Mute / Unmute — toggle your microphone
  • Hide Video / Show Video — toggle your camera for audio-only mode
  • End Call — hang up

Call Flow

  1. User clicks Start Call
  2. An ephemeral Ethereum wallet is created and used to connect an XMTP client
  3. The invite link contains the user's XMTP inbox ID as a ?partner= query parameter
  4. The caller generates an AES-128-GCM key and sends it to the peer over XMTP
  5. The caller creates a WebRTC offer (SDP) and sends it over XMTP
  6. The answerer receives the encryption key and offer, sets up matching encryption transforms, and sends an answer back over XMTP
  7. ICE candidates are exchanged via XMTP until a direct peer connection is established
  8. Audio/video frames flow peer-to-peer, encrypted at three layers (MLS, DTLS-SRTP, AES-128-GCM Encoded Transform)
  9. Chat messages are sent separately through XMTP's messaging service

Project Structure

src/
├── main.tsx              # React entry point
├── App.tsx               # Main UI component
├── styles.css            # Styles
├── LogLevel.ts           # Shared log level type
├── SignalingMessage.ts   # Signaling message types
├── SignalingCodec.ts     # Custom XMTP content type codec
├── createXmtpSigner.ts  # Ephemeral wallet → XMTP signer
├── XmtpSignaling.ts     # XMTP signaling layer
├── ConnectionState.ts   # WebRTC connection state types
├── RTCConfiguration.ts  # STUN server configuration
├── E2EEncryption.ts     # E2E media encryption via Encoded Transform API
└── WebRTCManager.ts     # WebRTC peer connection manager

XMTP Environment

By default, this app connects to the XMTP dev network. To use production:

// In App.tsx, change:
const id = await signaling.connect(xmtpSigner, "production");

Limitations

  • STUN servers (Google) are used for NAT traversal — they see IP addresses but not media content

License

MIT

About

E2EE Video Calls over XMTP × WebRTC

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors