Skip to main content
The LiveKit Swift SDK supports end-to-end encryption (E2EE) for both media tracks and data messages, ensuring that only participants in the room can decrypt the content.

Overview

E2EE in LiveKit:
  • Encrypts media (audio/video) using frame encryption
  • Encrypts data messages using data packet encryption
  • Uses AES-GCM encryption algorithm
  • Supports both shared key and per-participant key modes
  • Keys are never sent to the LiveKit server

Setting Up E2EE

1
Create a Key Provider
2
Create a BaseKeyProvider with a shared key:
3
let keyProvider = BaseKeyProvider(
    isSharedKey: true,
    sharedKey: "your-secret-key-here"
)
4
For per-participant keys:
5
let keyProvider = BaseKeyProvider(isSharedKey: false)
6
Create E2EE Manager
7
Create an E2EEManager with encryption options:
8
let encryptionOptions = EncryptionOptions(
    keyProvider: keyProvider,
    encryptionType: .gcm  // AES-GCM encryption
)

let e2eeManager = E2EEManager(options: encryptionOptions)
9
Setup E2EE with Room
10
Attach the E2EE manager to the room before connecting:
11
let room = Room()
room.e2eeManager = e2eeManager
e2eeManager.setup(room: room)

try await room.connect(url: wsURL, token: token)

Complete Example

import LiveKit

class E2EEExample {
    let room = Room()
    
    func connectWithE2EE() async throws {
        // Create key provider
        let keyProvider = BaseKeyProvider(
            isSharedKey: true,
            sharedKey: "my-secret-key"
        )
        
        // Create encryption options
        let encryptionOptions = EncryptionOptions(
            keyProvider: keyProvider,
            encryptionType: .gcm
        )
        
        // Create and setup E2EE manager
        let e2eeManager = E2EEManager(options: encryptionOptions)
        room.e2eeManager = e2eeManager
        e2eeManager.setup(room: room)
        
        // Connect to room
        try await room.connect(
            url: "wss://your-livekit-server.com",
            token: "your-token"
        )
        
        // Publish encrypted tracks
        try await room.localParticipant.setCamera(enabled: true)
        try await room.localParticipant.setMicrophone(enabled: true)
    }
}

Key Management

Setting Keys

For shared key mode:
keyProvider.setKey(
    key: "new-shared-key",
    index: 0  // Key index in the key ring
)
For per-participant mode:
keyProvider.setKey(
    key: "participant-key",
    participantId: "participant-identity",
    index: 0
)

Ratcheting Keys

Ratchet keys for forward secrecy:
// Shared key mode
let newKey = keyProvider.ratchetKey(index: 0)

// Per-participant mode
let newKey = keyProvider.ratchetKey(
    participantId: "participant-identity",
    index: 0
)

Exporting Keys

Export keys for backup or sharing:
// Shared key mode
if let keyData = keyProvider.exportKey(index: 0) {
    // Store or transmit keyData securely
}

// Per-participant mode
if let keyData = keyProvider.exportKey(
    participantId: "participant-identity",
    index: 0
) {
    // Store or transmit keyData securely
}

Key Ring and Index

The key provider maintains a key ring (default size: 16):
// Get current key index
let currentIndex = keyProvider.getCurrentKeyIndex()

// Set current key index
keyProvider.setCurrentKeyIndex(5)

Advanced Configuration

Key Provider Options

Customize the key provider behavior:
let options = KeyProviderOptions(
    sharedKey: true,
    ratchetSalt: "custom-salt".data(using: .utf8)!,
    ratchetWindowSize: 0,  // Disable automatic ratcheting
    uncryptedMagicBytes: "LK-ROCKS".data(using: .utf8)!,
    failureTolerance: -1,
    keyRingSize: 16
)

let keyProvider = BaseKeyProvider(options: options)
Options:
  • sharedKey: Use shared key mode (true) or per-participant mode (false)
  • ratchetSalt: Salt for key ratcheting
  • ratchetWindowSize: Window size for automatic ratcheting (0 = disabled)
  • uncryptedMagicBytes: Magic bytes to identify unencrypted frames
  • failureTolerance: Number of decryption failures to tolerate
  • keyRingSize: Maximum number of keys in the ring

Enable/Disable Encryption

Toggle encryption at runtime:
// Disable encryption
e2eeManager.enableE2EE(enabled: false)

// Re-enable encryption
e2eeManager.enableE2EE(enabled: true)

Data Message Encryption

With E2EE enabled, data messages are automatically encrypted:
// Data will be encrypted if E2EE is enabled
let data = "Secret message".data(using: .utf8)!
try await room.localParticipant.publish(data: data)
Check if data encryption is enabled:
if e2eeManager.isDataChannelEncryptionEnabled {
    print("Data messages are encrypted")
}

Monitoring Encryption State

Implement RoomDelegate to monitor encryption state changes:
class MyRoomDelegate: RoomDelegate {
    func room(
        _ room: Room,
        trackPublication: TrackPublication,
        didUpdateE2EEState state: E2EEState
    ) {
        switch state {
        case .new:
            print("Encryption initialized")
        case .ok:
            print("Encryption working")
        case .encryptionFailed:
            print("Failed to encrypt")
        case .decryptionFailed:
            print("Failed to decrypt")
        case .missingKey:
            print("Missing encryption key")
        case .keyRatcheted:
            print("Key was ratcheted")
        case .internalError:
            print("Internal encryption error")
        @unknown default:
            break
        }
    }
}

room.add(delegate: MyRoomDelegate())

Encryption Types

The SDK supports different encryption types:
public enum EncryptionType {
    case none     // No encryption
    case gcm      // AES-GCM (default)
    case custom   // Custom encryption
}
Access the current encryption type:
let frameEncryption = e2eeManager.frameEncryptionType
let dataEncryption = e2eeManager.dataChannelEncryptionType

Best Practices

  • Never transmit keys through the LiveKit server
  • Use a secure out-of-band channel for key distribution
  • Consider using a key management service
  • Rotate keys periodically for forward secrecy
  • Store keys securely using iOS Keychain or similar
  • Never hardcode keys in your application
  • Clear keys from memory when no longer needed
  • Monitor E2EE state changes via delegate
  • Handle missing key scenarios gracefully
  • Implement retry logic for transient failures
  • Log encryption errors for debugging
  • E2EE adds minimal latency (~1-2ms)
  • CPU usage increases slightly for encryption/decryption
  • No impact on network bandwidth
  • Test on target devices to measure impact

Troubleshooting

Decryption Failures

If participants can’t decrypt media:
  1. Verify all participants use the same shared key
  2. Check that E2EE manager is set up before connecting
  3. Ensure key index matches across participants
  4. Monitor E2EE state changes for specific errors

Missing Keys

If you see missingKey state:
func room(
    _ room: Room,
    trackPublication: TrackPublication,
    didUpdateE2EEState state: E2EEState
) {
    if state == .missingKey {
        // Set the key for the participant
        keyProvider.setKey(
            key: "participant-key",
            participantId: "participant-identity"
        )
    }
}

Legacy E2EEOptions

For backward compatibility, you can use E2EEOptions:
let e2eeOptions = E2EEOptions(
    keyProvider: keyProvider,
    encryptionType: .gcm
)

let e2eeManager = E2EEManager(e2eeOptions: e2eeOptions)

See Also

  • E2EE/E2EEManager.swift
  • E2EE/KeyProvider.swift

Build docs developers (and LLMs) love