Featured Developer Sponsor • Zero-Token Protection
RFC 9605 Standard
MLS Key Ratchet
Insertable Streams
WebRTC SFrame (RFC 9605) & Key Ratchet Studio
Simulate authenticated end-to-end media frame encryption, SFrame header bit-packing, HKDF epoch keys, and SFU transparent routing.
Active MLS Epoch
Epoch 0
Ratchets derived: 0
Frame Counter (CTR)
#0
Header wire size: 3 Bytes
Total SFrame Wire Overhead
19 Bytes
Hdr (3B) + Tag (16B)
Encoded Media Packet Layout & SFrame Bitfield
RTP Header (12B - Cleartext for SFU)
SFrame Header (RFC 9605 Bitfield)
Unencrypted Metadata (SVC / Layering)
AES-GCM Encrypted Video Payload
Authentication Tag (AAD Integrity)
Byte Inspector
Hover or click any byte block above to decode RFC 9605 bitfield offsets and cryptographic meanings.
Anti-Replay Sliding Window (64-bit Bitmask)
Tracks arriving frame counters to block network replay attacks without storing unbounded state.
Max Counter Seen: 0
Replay Status: Nominal (No Replays)
RFC 9605 HKDF Key Derivation
MLS Epoch Secret: 0x9e8a...31b2
SFrame Base Key: 0x4f12...cc01
SFrame Salt (96b): 0x11223344556677889900aabb
Nonce/IV (Salt XOR CTR): 0x11223344556677889900aab8
Nonce construction:
IV = sframe_salt ^ (CTR padded to 96 bits)
// sframe_transform_worker.ts
// RFC 9605 Secure Frame (SFrame) RTCRtpScriptTransform Web Worker
// 100% Native WebCrypto AES-GCM Pipeline with MLS Epoch Key Rotation
interface SFrameConfig {
cipherSuite: 'AES_128_GCM' | 'AES_256_GCM';
keyId: number;
metadataLength: number; // e.g., VP9 Payload Descriptor size
}
class SFrameEncryptor {
private frameCounter: bigint = 0n;
private key: CryptoKey | null = null;
private salt: Uint8Array = new Uint8Array(12);
async setEpochSecret(epochSecret: Uint8Array, keyId: number): Promise<void> {
const hkdfKey = await crypto.subtle.importKey('raw', epochSecret, 'HKDF', false, ['deriveKey', 'deriveBits']);
// Derive SFrame Encryption Key
this.key = await crypto.subtle.deriveKey(
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(), info: new TextEncoder().encode('sframe key') },
hkdfKey,
{ name: 'AES-GCM', length: 128 },
false,
['encrypt']
);
// Derive SFrame 96-bit Salt
const saltBits = await crypto.subtle.deriveBits(
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(), info: new TextEncoder().encode('sframe salt') },
hkdfKey,
96
);
this.salt = new Uint8Array(saltBits);
}
encodeHeader(keyId: number, counter: bigint): Uint8Array {
const ctrBytes: number[] = [];
let tempCtr = counter;
while (tempCtr > 0n || ctrBytes.length === 0) {
ctrBytes.unshift(Number(tempCtr & 0xffn));
tempCtr >>= 8n;
}
if (keyId < 8) {
// Compact Header: [0 | LEN (3) | KeyID (4)]
const firstByte = (ctrBytes.length << 4) | (keyId & 0x0f);
return new Uint8Array([firstByte, ...ctrBytes]);
} else {
// Extended Header: [1 | LEN (3) | KeyID_LEN (4)]
const keyBytes: number[] = [];
let tempK = keyId;
while (tempK > 0) { keyBytes.unshift(tempK & 0xff); tempK >>= 8; }
const firstByte = 0x80 | (ctrBytes.length << 4) | ((keyBytes.length - 1) & 0x0f);
return new Uint8Array([firstByte, ...keyBytes, ...ctrBytes]);
}
}
async transform(frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController): Promise<void> {
if (!this.key) return;
const data = new Uint8Array(frame.data);
const metadataLength = 12; // Unencrypted VP9 descriptor
const metadata = data.subarray(0, metadataLength);
const plaintextPayload = data.subarray(metadataLength);
const sframeHeader = this.encodeHeader(3, this.frameCounter);
// Compute Nonce: IV = salt XOR CTR (padded to 12 bytes)
const iv = new Uint8Array(this.salt);
let c = this.frameCounter;
for (let i = 11; i >= 4; i--) {
iv[i] ^= Number(c & 0xffn);
c >>= 8n;
}
// AAD binds SFrame Header + Unencrypted Metadata
const aad = new Uint8Array(sframeHeader.length + metadata.length);
aad.set(sframeHeader, 0);
aad.set(metadata, sframeHeader.length);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, additionalData: aad, tagLength: 128 },
this.key,
plaintextPayload
);
// Assemble final frame: [Metadata] + [SFrame Header] + [Ciphertext + Tag]
const encryptedFrame = new Uint8Array(metadata.length + sframeHeader.length + ciphertext.byteLength);
encryptedFrame.set(metadata, 0);
encryptedFrame.set(sframeHeader, metadata.length);
encryptedFrame.set(new Uint8Array(ciphertext), metadata.length + sframeHeader.length);
frame.data = encryptedFrame.buffer;
this.frameCounter++;
controller.enqueue(frame);
}
}
Frequently Asked Technical Questions
What is RFC 9605 SFrame and how does it differ from standard DTLS-SRTP in WebRTC?+
DTLS-SRTP encrypts RTP packets hop-by-hop between an endpoint and a Selective Forwarding Unit (SFU). Because DTLS-SRTP terminates at the SFU, untrusted or cloud-hosted SFUs hold cleartext access to video and audio. RFC 9605 SFrame (Secure Frame) defines an authenticated encryption format that wraps individual encoded video and audio frames before RTP packetization. SFrame ciphertext passes through the SFU unchanged, while RTP headers remain unencrypted so the SFU can route packets, adjust bitrate, and inspect sequence numbers without possessing decryption keys.
How does SFrame preserve unencrypted codec headers for video selective forwarding?+
Video codecs (VP9, AV1, H.264) contain frame headers necessary for SFUs to perform spatial and temporal scalability (SVC) and keyframe routing. RFC 9605 allows a sender to leave a defined prefix of the encoded frame unencrypted (metadata). SFrame computes the Authenticated Additional Data (AAD) over both the SFrame header and the unencrypted codec metadata, binding them cryptographically into the AES-GCM or HMAC tag so adversaries or malicious SFUs cannot tamper with resolution or temporal layer flags.
How does SFrame integrate with MLS (Messaging Layer Security) and key ratcheting?+
SFrame does not define key negotiation itself; instead, it consumes keys from group key establishment protocols like MLS (RFC 9420). Each group epoch or ratchet step provides an epoch secret. Using HKDF-Expand-Label, endpoints derive sender base keys, SFrame encryption keys, and 96-bit base salts. The 96-bit IV is generated by XORing the base salt with the frame counter padded to 12 bytes. SFrame clients buffer keys across adjacent epochs (E and E-1) to gracefully decrypt in-flight media packets arriving across epoch boundaries.
What are the RFC 9605 cipher suites and header wire formats?+
RFC 9605 standardizes several cipher suites: AES_128_GCM (16-byte tag), AES_256_GCM (16-byte tag), AES_CM_128_HMAC_SHA256_80 (10-byte tag), and AES_CM_128_HMAC_SHA256_64 (8-byte tag). The SFrame header starts with a 1-byte descriptor: bit 7 (S/Extended flag), bits 4-6 (Counter length LEN 0-7), and bits 0-3 (KeyID 0-7, or if S=1, length of extended KeyID). This achieves a compact 3 to 5 byte header overhead per frame.
Sponsored Utility
While You're Here
Sponsored Recommendations
Advertisement