Documentation Index Fetch the complete documentation index at: https://mintlify.com/jitsi/lib-jitsi-meet/llms.txt
Use this file to discover all available pages before exploring further.
This guide covers end-to-end encryption (E2EE) implementation in lib-jitsi-meet using insertable streams and the JFrame protocol.
Overview
lib-jitsi-meet implements E2EE using:
Insertable Streams API : Intercepts and transforms media frames
JFrame Protocol : SFrame variant optimized for Jitsi
Olm : End-to-end encrypted key exchange
AES-GCM : 128-bit encryption for media frames
Web Workers : Offloads encryption to background thread
E2EE is currently supported only in browsers with insertable streams support (Chrome, Edge, Safari).
Architecture
Key Components
E2EEContext : Per-participant encryption context
Worker : Web Worker that performs encryption/decryption
OlmAdapter : Handles key distribution via Olm
Key Ratcheting : Automatic key rotation on participant changes
Encryption Flow
Local Media → Insertable Stream → Worker (Encrypt) → Encrypted Frame → Network
Network → Encrypted Frame → Worker (Decrypt) → Insertable Stream → Remote Media
Enabling E2EE
Check browser support
if ( ! conference . isE2EESupported ()) {
console . error ( 'E2EE not supported in this browser' );
return ;
}
Enable E2EE in conference
// E2EE is automatically initialized if supported
const conference = connection . initJitsiConference ( 'room' , {
e2eping: {
enabled: true // Optional: enables E2EE ping for monitoring
}
});
Set encryption key
// Generate or retrieve encryption key
const key = await generateEncryptionKey ();
// Enable E2EE with the key
await conference . toggleE2EE ( true );
await conference . setE2EEKey ( key );
console . log ( 'E2EE enabled' );
Key Management
Generate Encryption Key
// Generate random 256-bit key
function generateEncryptionKey () {
const key = new Uint8Array ( 32 );
crypto . getRandomValues ( key );
return key ;
}
// Or derive from passphrase
async function deriveKeyFromPassphrase ( passphrase ) {
const encoder = new TextEncoder ();
const data = encoder . encode ( passphrase );
const hash = await crypto . subtle . digest ( 'SHA-256' , data );
return new Uint8Array ( hash );
}
Set Encryption Key
// Set key for the conference
const key = await generateEncryptionKey ();
await conference . setE2EEKey ( key );
// All participants must use the same key
// Share key securely out-of-band
Key Rotation
// The library automatically rotates keys when:
// 1. A participant leaves (new key generated)
// 2. A participant joins (key ratcheted)
// Manual key rotation
async function rotateKey () {
const newKey = await generateEncryptionKey ();
await conference . setE2EEKey ( newKey );
// Share new key with participants
// (via secure channel)
}
// Listen for key rotation events
conference . on (
JitsiMeetJS . events . conference . E2EE_VERIFICATION_AVAILABLE ,
( participantId ) => {
console . log ( 'E2EE verification available for' , participantId );
}
);
Key Ratcheting
Keys are automatically ratcheted (derived) when participants join:
// Key ratcheting happens automatically:
// 1. Participant joins → All participants ratchet their keys
// 2. New key derived using HKDF
// 3. No key exchange needed (all derive same key)
// This maintains forward secrecy without signaling
E2EE Context Management
Per-Participant Context
// Each participant has their own E2EEContext
// The context manages:
// - Encryption keys (key ring)
// - Send counters (for IV generation)
// - Ratchet state
// Context is created automatically for each participant
Encryption Parameters
// The encryption uses:
// - Algorithm: AES-GCM
// - Key size: 128 bits
// - IV size: 96 bits (12 bytes)
// - Tag size: 128 bits (16 bytes)
// IV construction:
// [SSRC (4 bytes)][Timestamp (4 bytes)][Counter (4 bytes)]
Frame Encryption
Encrypted frames use a custom trailer format:
+------------------+-------------------------+-+---------+-----+
| Unencrypted | Encrypted Payload |I|IV_LENGTH| KID |
| Header | |V| | |
+------------------+-------------------------+-+---------+-----+
Unencrypted Header:
- VP8: 10 bytes (keyframe) or 3 bytes (delta)
- Opus: 1 byte (TOC)
Trailer:
- IV: Variable length (12 bytes typical)
- IV_LENGTH: 1 byte
- KID: 1 byte (key index in ring buffer)
Selective Encryption
// Only the payload is encrypted, not:
// - VP8 payload descriptor (for SFU compatibility)
// - Opus TOC byte
// - RTP headers (handled by SRTP)
// This allows:
// - SFU to detect keyframes
// - Bridge to forward without decryption
// - Decoder to show visual artifacts instead of errors
E2EE Events
Monitor E2EE Status
// E2EE enabled/disabled
conference . on (
JitsiMeetJS . events . conference . E2EE_ENABLED ,
() => {
console . log ( 'E2EE enabled' );
updateUI ({ e2eeActive: true });
}
);
conference . on (
JitsiMeetJS . events . conference . E2EE_DISABLED ,
() => {
console . log ( 'E2EE disabled' );
updateUI ({ e2eeActive: false });
}
);
// Verification available
conference . on (
JitsiMeetJS . events . conference . E2EE_VERIFICATION_AVAILABLE ,
( participantId ) => {
console . log ( 'Can verify' , participantId );
// Show verification UI
}
);
// Verification completed
conference . on (
JitsiMeetJS . events . conference . E2EE_VERIFICATION_COMPLETED ,
( participantId , success ) => {
if ( success ) {
console . log ( 'Verified' , participantId );
} else {
console . warn ( 'Verification failed for' , participantId );
}
}
);
Verification (SAS)
Short Authentication String
Verify that participants share the same key using SAS:
// Get SAS for participant
const sas = await conference . getE2EESAS ( participantId );
console . log ( 'SAS:' , sas ); // e.g., "🐶 🌮 🚀 🎨 🌈 🎭 🎪"
// Both participants compare SAS
// If they match, keys are synchronized
// Mark as verified
await conference . markE2EEVerified ( participantId , true );
Verification Flow
Request verification
const participantId = 'user123' ;
const sas = await conference . getE2EESAS ( participantId );
Display SAS to both users
// Show emoji SAS: 🐶 🌮 🚀 🎨 🌈 🎭 🎪
// Both users compare visually or read over voice
Confirm match
if ( userConfirmsMatch ) {
await conference . markE2EEVerified ( participantId , true );
console . log ( 'Participant verified' );
}
E2EE with Visitors (Experimental)
// E2EE works differently with visitors:
// - Visitors use a shared key
// - No individual key ratcheting
// - Limited verification
const conferenceOptions = {
e2ee: {
visitorKey: visitorSharedKey // Separate key for visitors
}
};
Web Worker Usage
// Encryption runs in Web Worker to avoid blocking main thread
// - Worker handles all encrypt/decrypt operations
// - Frames transferred via Transferable objects
// - Minimal impact on main thread performance
// Worker is loaded from: lib-jitsi-meet/dist/lib-jitsi-meet.e2ee-worker.js
Encryption Overhead
// Performance impact:
// - CPU: ~5-10% increase for encryption/decryption
// - Bandwidth: +28 bytes per frame (IV + tag + trailer)
// - Latency: <1ms per frame (in worker)
// Optimizations:
// - Hardware crypto APIs (AES-GCM in WebCrypto)
// - Frame batching in worker
// - Efficient IV generation
Limitations
E2EE has several current limitations:
Browser support : Requires insertable streams (no Firefox support)
Recording : Cannot record encrypted media on server
SFU processing : Bridge cannot do quality adaptation
Screen sharing : Works but may have compatibility issues
Simulcast : Limited support
Known Issues
// E2EE currently does not work with:
// - Server-side recording (Jibri)
// - Server-side transcription
// - Some advanced quality features
// - Live streaming
// Workarounds:
// - Use client-side recording
// - Disable E2EE for recorded sessions
// - Use E2EE selectively
Debugging E2EE
Enable E2EE Logging
// Set log level for E2EE
JitsiMeetJS . setLogLevel ( JitsiMeetJS . logLevels . TRACE );
// E2EE-specific logs will show:
// - Key generation
// - Encryption/decryption operations
// - Key rotation events
// - Verification status
E2EE Ping
Monitor E2EE health using ping:
const conferenceOptions = {
e2eping: {
enabled: true
}
};
// Periodically sends encrypted ping messages
// Verifies encryption is working end-to-end
conference . on (
JitsiMeetJS . events . conference . E2EE_PING_SUCCESS ,
( participantId , rtt ) => {
console . log ( `E2EE ping to ${ participantId } : ${ rtt } ms` );
}
);
conference . on (
JitsiMeetJS . events . conference . E2EE_PING_FAILED ,
( participantId ) => {
console . error ( `E2EE ping failed to ${ participantId } ` );
}
);
Security Best Practices
Generate keys using cryptographically secure random number generator: const key = new Uint8Array ( 32 );
crypto . getRandomValues ( key );
Never send keys over the conference signaling: // Use out-of-band channels:
// - Secure messaging app
// - QR code (in-person)
// - Pre-shared key
// - Key server with authentication
Always verify critical participants using SAS: const sas = await conference . getE2EESAS ( participantId );
// Compare SAS out-of-band (voice, video, etc.)
await conference . markE2EEVerified ( participantId , true );
Rotate keys when participants leave: conference . on (
JitsiMeetJS . events . conference . USER_LEFT ,
async ( participantId ) => {
// Key automatically rotates
console . log ( 'Key rotated due to participant leaving' );
}
);
Complete E2EE Example
class E2EEManager {
constructor ( conference ) {
this . conference = conference ;
this . key = null ;
this . enabled = false ;
this . setupListeners ();
}
setupListeners () {
this . conference . on (
JitsiMeetJS . events . conference . E2EE_ENABLED ,
() => this . onE2EEEnabled ()
);
this . conference . on (
JitsiMeetJS . events . conference . E2EE_DISABLED ,
() => this . onE2EEDisabled ()
);
this . conference . on (
JitsiMeetJS . events . conference . E2EE_VERIFICATION_AVAILABLE ,
( id ) => this . onVerificationAvailable ( id )
);
}
async enable ( passphrase ) {
if ( ! this . conference . isE2EESupported ()) {
throw new Error ( 'E2EE not supported' );
}
// Derive key from passphrase
this . key = await this . deriveKey ( passphrase );
// Enable E2EE
await this . conference . toggleE2EE ( true );
await this . conference . setE2EEKey ( this . key );
this . enabled = true ;
}
async disable () {
await this . conference . toggleE2EE ( false );
this . key = null ;
this . enabled = false ;
}
async deriveKey ( passphrase ) {
const encoder = new TextEncoder ();
const data = encoder . encode ( passphrase );
const hash = await crypto . subtle . digest ( 'SHA-256' , data );
return new Uint8Array ( hash );
}
async verifyParticipant ( participantId ) {
const sas = await this . conference . getE2EESAS ( participantId );
// Show SAS to user for comparison
const confirmed = await this . showSASVerification ( participantId , sas );
if ( confirmed ) {
await this . conference . markE2EEVerified ( participantId , true );
}
}
onE2EEEnabled () {
console . log ( 'E2EE is now active' );
this . updateUI ({ encrypted: true });
}
onE2EEDisabled () {
console . log ( 'E2EE is now inactive' );
this . updateUI ({ encrypted: false });
}
onVerificationAvailable ( participantId ) {
console . log ( 'Can verify' , participantId );
// Optionally auto-verify or prompt user
}
updateUI ( state ) {
// Update UI to show E2EE status
}
async showSASVerification ( participantId , sas ) {
// Show SAS UI and get user confirmation
return new Promise (( resolve ) => {
// Show modal with SAS emoji
// Return true if user confirms match
});
}
}
// Usage
const e2eeManager = new E2EEManager ( conference );
// Enable E2EE with passphrase
await e2eeManager . enable ( 'my-secure-passphrase' );
// Verify a participant
await e2eeManager . verifyParticipant ( 'participant-id' );
Next Steps
Recording Record conferences (note: incompatible with E2EE)
Statistics & Analytics Monitor conference metrics