gbp-sframe
SFrame (draft-ietf-sframe-enc) E2EE for GAP audio streams in the Group Protocol Stack.
What is SFrame?
SFrame sits inside transport-level encryption (SRTP / DTLS) and provides end-to-end confidentiality for media payloads. An SFU can forward packets based on RTP headers without ever seeing the Opus frame content.
┌──────────────────────────────────────────────────┐
│ Transport encryption │ ← client ↔ SFU
│ ┌────────────────────────────────────────────┐ │
│ │ SFrame (this crate) │ │ ← E2E client ↔ client
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Encoded media (Opus / VP8 / VP9) │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Key derivation
After each MLS epoch change:
- Base key —
MLS.ExportSecret(label, context=epoch_be8, length=32). - Per-sender key —
HKDF-Expand(base_key, "gbp sframe key " ‖ leaf_be4, L). - Per-sender salt —
HKDF-Expand(base_key, "gbp sframe salt " ‖ leaf_be4, 12). - Frame nonce —
salt XOR (CTR_LE64 ‖ 0x00_00_00_00).
The label is application-defined ("gbp/sframe v1" by convention), so
different deployments can use distinct key universes without changing any
protocol parameter.
Usage
use ;
use MlsContext;
// After MLS handshake — both sides derive a session for the current epoch.
let session = from_mls?;
// Sender (leaf_index = 0):
let mut enc = session.encryptor;
let payload = enc.encrypt?;
// Receiver:
let mut dec = session.decryptor;
let = dec.decrypt?;
Ciphersuite
| Suite | Key | Salt | AEAD |
|---|---|---|---|
Aes128Gcm |
16 B | 12 B | AES-128-GCM |
Aes256Gcm |
32 B | 12 B | AES-256-GCM |
Replay protection
A 1024-entry sliding-window replay window is maintained per sender. Duplicate or overly-old counters are rejected before decryption.
SFrame header wire format
┌─┬──────────┬──────────┬───────────────┬───────────────┐
│V│ K (3) │ C (4) │ KID (var) │ CTR (var) │
└─┴──────────┴──────────┴───────────────┴───────────────┘
V= 0 (SFrame v1).K— KID length in bytes minus one.C— CTR length in bytes minus one.KID = (epoch << 16) | leaf_index.CTR— big-endian per-sender monotonic counter.